進程間通信---信號


信號的概念

信號在我們的生活中隨處可見, 如:古代戰爭中摔杯為號;現代戰爭中的信號彈;體育比賽中使用的信號槍......他們都有共性:1. 簡單 2. 不能攜帶大量信息 3. 滿足某個特設條件才發送。

    信號是信息的載體,Linux/UNIX 環境下,古老、經典的通信方式, 現下依然是主要的通信手段。

Unix早期版本就提供了信號機制,但不可靠,信號可能丟失。Berkeley AT&T都對信號模型做了更改,增加了可靠信號機制。但彼此不兼容。POSIX.1對可靠信號例程進行了標准化。

信號的機制

AB發送信號,B收到信號之前執行自己的代碼,收到信號后,不管執行到程序的什么位置,都要暫停運行,去處理信號,處理完畢再繼續執行。與硬件中斷類似——異步模式。但信號是軟件層面上實現的中斷,早期常被稱為“軟中斷”。

每個進程收到的所有信號,都是由內核負責發送的。

與信號相關的事件和狀態

產生信號:

1. 按鍵產生,如:Ctrl+cCtrl+zCtrl+\

2. 系統調用產生,如:killraiseabort

3. 軟件條件產生,如:定時器alarm

4. 硬件異常產生,如:非法訪問內存(段錯誤)、除0(浮點數例外)、內存對齊出錯(總線錯誤)

5. 命令產生,如:kill命令

遞達:遞送並且到達進程。

未決:產生和遞達之間的狀態。主要由於阻塞(屏蔽)導致該狀態。

信號的處理方式: 

1. 執行默認動作

2. 忽略(丟棄)

3. 捕捉(調用戶處理函數)

    信號的特質:信號的實現手段導致信號有很強的延時性,但對於用戶來說,時間非常短,不易察覺。

Linux內核的進程控制塊PCB是一個結構體,task_struct, 除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,還包含了信號相關的信息,主要指阻塞信號集和未決信號集。

    阻塞信號集(信號屏蔽字) 將某些信號加入集合,對他們設置屏蔽,當屏蔽x信號后,再收到該信號,該信號的處理將推后(解除屏蔽后,再進行處理)

未決信號集:

1. 信號產生,未決信號集中描述該信號的位立刻翻轉為1,表信號處於未決狀態。當信號被處理對應位翻轉回為0。這一時刻往往非常短暫。

2. 信號產生后由於某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處於未決狀態。

信號的編號

可以使用kill –l命令查看當前系統可使用的信號有哪些

1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL   5) SIGTRAP

  6) SIGABRT  7) SIGBUS  8) SIGFPE  9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

63) SIGRTMAX-1 64) SIGRTMAX

不存在編號為0的信號。其中1-31號信號稱之為常規信號也叫普通信號或標准信號),34-64稱之為實時信號,驅動編程與硬件相關。名字上區別不大。而前32個名字各不相同。

信號4要素

與變量三要素類似的,每個信號也有其必備4要素,分別是:

1. 編號 2. 名稱 3. 事件 4. 默認處理動作

可通過man 7 signal查看幫助文檔獲取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h

Signal      Value     Action   Comment

────────────────────────────────────────────

SIGHUP       1       Term    Hangup detected on controlling terminal or death of controlling process

SIGINT        2       Term    Interrupt from keyboard

SIGQUIT       3       Core    Quit from keyboard

SIGILL         4       Core    Illegal Instruction

SIGFPE        8       Core    Floating point exception

SIGKILL        9       Term    Kill signal

SIGSEGV      11      Core    Invalid memory reference

SIGPIPE    13      Term    Broken pipe: write to pipe with no readers

SIGALRM     14      Term    Timer signal from alarm(2)

SIGTERM      15      Term    Termination signal

SIGUSR1   30,10,16    Term    User-defined signal 1

SIGUSR2   31,12,17    Term    User-defined signal 2

SIGCHLD   20,17,18    Ign     Child stopped or terminated

SIGCONT   19,18,25    Cont    Continue if stopped

SIGSTOP   17,19,23    Stop    Stop process

SIGTSTP   18,20,24    Stop    Stop typed at terminal

SIGTTIN   21,21,26    Stop    Terminal input for background process

SIGTTOU   22,22,27   Stop    Terminal output for background process

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.   

在標准信號中,有一些信號是有三個Value”,第一個值通常對alphasparc架構有效,中間值針對x86arm和其他架構,最后一個應用於mips架構。一個‘-’表示在對應架構上尚未定義該信號。

不同的操作系統定義了不同的系統信號。因此有些信號出現在Unix系統內,也出現在Linux中,而有的信號出現在FreeBSDMac OS中卻沒有出現在Linux下。這里我們只研究Linux系統中的信號。

    默認動作:

Term:終止進程

Ign: 忽略信號 (默認即時對該種信號忽略操作)

Core:終止進程,生成Core文件。(查驗進程死亡原因, 用於gdb調試)

Stop:停止(暫停)進程

Cont:繼續運行進程

   注意從man 7 signal幫助文檔中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

   這里特別強調了9) SIGKILL 19) SIGSTOP信號,不允許忽略和捕捉,只能執行默認動作。甚至不能將其設置為阻塞。

另外需清楚只有每個信號所對應的事件發生了,該信號才會被遞送(但不一定遞達),不應亂發信號!!

Linux常規信號一覽表

1) SIGHUP: 當用戶退出shell時,由該shell啟動的所有進程將收到這個信號,默認動作為終止進程

2) SIGINT:當用戶按下了<Ctrl+C>組合鍵時,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動

作為終止進程。

3) SIGQUIT:當用戶按下<ctrl+\>組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出些信

號。默認動作為終止進程。

4) SIGILLCPU檢測到某進程執行了非法指令。默認動作為終止進程並產生core文件

5) SIGTRAP:該信號由斷點指令或其他 trap指令產生。默認動作為終止里程 並產生core文件。

6) IGABRT: 調用abort函數時產生該信號。默認動作為終止進程並產生core文件。

7) SIGBUS:非法訪問內存地址,包括內存對齊出錯,默認動作為終止進程並產生core文件。

8) SIGFPE:在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的算法錯誤。默認動作為終止進程並產生core文件。

9) SIGKILL:無條件終止進程。本信號不能被忽略,處理和阻塞。默認動作為終止進程。它向系統管理員提供了可以殺死任何進程的方法。

10) SIGUSE1:用戶定義 的信號。即程序員可以在程序中定義並使用該信號。默認動作為終止進程。

11) SIGSEGV:指示進程進行了無效內存訪問。默認動作為終止進程並產生core文件。

12) SIGUSR2:另外一個用戶自定義信號,程序員可以在程序中定義並使用該信號。默認動作為終止進程。

13) SIGPIPEBroken pipe向一個沒有讀端的管道寫數據。默認動作為終止進程。

14) SIGALRM: 定時器超時,超時的時間 由系統調用alarm設置。默認動作為終止進程。

15) SIGTERM:程序結束信號,與SIGKILL不同的是,該信號可以被阻塞和終止。通常用來要示程序正常退出。執行shell命令Kill時,缺省產生這個信號。默認動作為終止進程。

16) SIGSTKFLTLinux早期版本出現的信號,現仍保留向后兼容。默認動作為終止進程。

17) SIGCHLD:子進程結束時,父進程會收到這個信號。默認動作為忽略這個信號。

18) SIGCONT:如果進程已停止,則使其繼續運行。默認動作為繼續/忽略。

19) SIGSTOP:停止進程的執行。信號不能被忽略,處理和阻塞。默認動作為終止進程。

20) SIGTSTP:停止終端交互進程的運行。按下<ctrl+z>組合鍵時發出這個信號。默認動作為暫停進程。

21) SIGTTIN:后台進程讀終端控制台。默認動作為暫停進程。

22) SIGTTOU: 該信號類似於SIGTTIN,在后台進程要向終端輸出數據時發生。默認動作為暫停進程。

23) SIGURG:套接字上有緊急數據時,向當前正在運行的進程發出些信號,報告有緊急數據到達。如網絡帶外數據到達,默認動作為忽略該信號。

24) SIGXCPU:進程執行時間超過了分配給該進程的CPU時間 ,系統產生該信號並發送給該進程。默認動作為終止進程。

25) SIGXFSZ:超過文件的最大長度設置。默認動作為終止進程。

26) SIGVTALRM:虛擬時鍾超時時產生該信號。類似於SIGALRM,但是該信號只計算該進程占用CPU的使用時間。默認動作為終止進程。

27) SGIPROF:類似於SIGVTALRM,它不公包括該進程占用CPU時間還包括執行系統調用時間。默認動作為終止進程。

28) SIGWINCH:窗口變化大小時發出。默認動作為忽略該信號。

29) SIGIO:此信號向進程指示發出了一個異步IO事件。默認動作為忽略。

30) SIGPWR:關機。默認動作為終止進程。

31) SIGSYS:無效的系統調用。默認動作為終止進程並產生core文件。

34) SIGRTMIN  (64) SIGRTMAXLINUX的實時信號,它們沒有固定的含義(可以由用戶自定義)。所有的實時信號的默認動作都為終止進程。

信號的產生

終端按鍵產生信號

    Ctrl + c  2) SIGINT(終止/中斷)  "INT" ----Interrupt

    Ctrl + z  20) SIGTSTP(暫停/停止)  "T" ----Terminal 終端。

    Ctrl + \  3) SIGQUIT(退出)

硬件異常產生信號

    0操作   → 8) SIGFPE (浮點數例外) "F" -----float 浮點數。

    非法訪問內存  → 11) SIGSEGV (段錯誤)

    總線錯誤  → 7) SIGBUS

kill函數/命令產生信號

kill命令產生信號:kill -SIGKILL 進程ID

kill函數:給指定進程發送指定信號(不一定殺死)

    int kill(pid_t pid, int sig);  成功:0;失敗:-1 (ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno

sig:不推薦直接使用數字,應使用宏名,因為不同操作系統信號編號可能不同,但名稱一致。

    pid > 0:  發送信號給指定的進程。

pid = 0:  發送信號給 與調用kill函數進程屬於同一進程組的所有進程。

pid < -1:  |pid|發給對應進程組。

pid = -1:發送給進程有權限發送的系統中所有進程。

    進程組:每個進程都屬於一個進程組,進程組是一個或多個進程集合,他們相互關聯,共同完成一個實體任務,每個進程組都有一個進程組長,默認進程組ID與進程組長ID相同。(ps -ajx,可以查看進程組id)

權限保護:super用戶(root)可以發送信號給任意用戶,普通用戶是不能向系統用戶發送信號的。 kill -9 (root用戶的pid)  是不可以的。同樣,普通用戶也不能向其他普通用戶發送信號,終止其進程。 只能向自己創建的進程發送信號。普通用戶基本規則是:發送者實際或有效用戶ID == 接收者實際或有效用戶ID

 例子:循環創建5個子進程,任一子進程用kill函數終止其父進程。 kill.c

 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

#define N 5

int main(void)
{
    int i;                //默認創建5個子進程

    for(i = 0; i < N; i++)    //出口1,父進程專用出口
        if(fork() == 0)
            break;            //出口2,子進程出口,i不自增

    if (i == 3) {
        sleep(1);
        printf("-----------child ---pid = %d, ppid = %d\n", getpid(), getppid());
        kill(getppid(), SIGKILL);

    } else if (i == N) {
        printf("I am parent, pid = %d\n", getpid());
        while(1);
    }

    return 0;
}

 

 

 

 

raiseabort系統函數

raise 函數:給當前進程發送指定信號(自己給自己發) raise(signo) == kill(getpid(), signo);

     int raise(int sig); 成功:0,失敗非0

abort 函數:給自己發送異常終止信號 6) SIGABRT 信號,終止並產生core文件

     void abort(void); 該函數無返回

軟件條件產生信號  

alarm函數

設置定時器(鬧鍾)。在指定seconds后,內核會給當前進程發送14SIGALARM信號。進程收到該信號,默認動作終止。

每個進程都有且只有唯一的一個定時器。

unsigned int alarm(unsigned int seconds); 返回0或剩余的秒數,無失敗。

常用:取消定時器alarm(0),返回舊鬧鍾余下秒數。

例:alarm(5) 3sec alarm(4) 5sec alarm(5) alarm(0)

  定時5s            3秒后返回2,定時4秒 5秒后返回0,定時5秒,取消定時

    定時,與進程狀態無關(自然定時法)!就緒、運行、掛起(阻塞、暫停)、終止、僵屍...無論進程處於何種狀態,alarm都計時。

例:編寫程序,測試你使用的計算機1秒鍾能數多少個數。 alarm .c

 

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int i;
    alarm(1);

    for(i = 0; ; i++)
        printf("%d\n", i);

    return 0;
}

 

1秒后程序終止,被信號中斷,可以考慮不打印打屏幕,因為打印打屏幕需要等待設備,耗時比較長,可以直接寫入文件,可以打印更多的數據

time ./a.out

 

使用time命令查看程序執行的時間。程序運行的瓶頸在於IO,優化程序,首選優化IO

實際執行時間 = 系統時間 + 用戶時間 + 等待時間

setitimer函數

設置定時器(鬧鍾)。 可代替alarm函數。精度微秒us,可以實現周期定時。

    int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 成功:0;失敗:-1,設置errno

參數:which:指定定時方式

① 自然定時:ITIMER_REAL 14SIGLARM   計算自然時間

② 虛擬空間計時(用戶空間)ITIMER_VIRTUAL 26SIGVTALRM    只計算進程占用cpu的時間

③ 運行時計時(用戶+內核)ITIMER_PROF 27SIGPROF  計算占用cpu及執行系統調用的時間

: 使用setitimer函數實現alarm函數,重復計算機1秒數數程序。 setitimer.c

 

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

/*
struct itimerval {

    struct timeval{
        it_value.tv_sec;
        it_value.tv_usec;
    } it_interval;

    struct timeval{
        it_value.tv_sec;
        it_value.tv_usec;
    } it_value;

} it, oldit;
*/

unsigned int my_alarm(unsigned int sec)
{
    struct itimerval it, oldit;
    int ret;

    it.it_value.tv_sec = sec;
    it.it_value.tv_usec = 0;
    it.it_interval.tv_sec = 0;
    it.it_interval.tv_usec = 0;

    ret = setitimer(ITIMER_REAL, &it, &oldit);
    if (ret == -1) {
        perror("setitimer");
        exit(1);
    }
    return oldit.it_value.tv_sec;
}

int main(void)
{
    int i;
    my_alarm(1);

    for(i = 0; ; i++)
        printf("%d\n", i);

    return 0;
}

 

例:結合man page編寫程序,測試it_intervalit_value這兩個參數的作用。 setitimer1.c

 

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void myfunc(int signo)
{
    printf("hello world\n");
}

int main(void)
{
    struct itimerval it, oldit;
    signal(SIGALRM, myfunc);
    //sighandler_t tml = signal(SIGALRM, myfunc);

    it.it_value.tv_sec = 1;
    it.it_value.tv_usec = 0;

    it.it_interval.tv_sec = 3;
    it.it_interval.tv_usec = 0;

    if(setitimer(ITIMER_REAL, &it, &oldit) == -1){
        perror("setitimer error");
        return -1;
    }

    while(1);

    return 0;
}

 

上面程序為定時1秒,產生sigalrm信號,並調用信號處理函數,然后每隔3秒再產生sigalrm信號,再調用信號處理函數

 

提示: it_interval:用來設定兩次定時任務之間間隔的時間。

  it_value:定時的時長

兩個參數都設置為0,即清0操作。

信號集操作函數

PCB中有兩個非常重要的信號集一個稱之為阻塞信號集”,另一個稱之為“未決信號集”。這兩個信號集都是內核使用位圖機制來實現的。但操作系統不允許我們直接對其進行位操作。而需自定義另外一個集合,借助信號集操作函數來對PCB中的這兩個信號集進行修改。

 

 

自定義信號集設定

int sigemptyset(sigset_t *set); 將某個信號集清0   成功:0;失敗:-1,設置errno

    int sigfillset(sigset_t *set); 將某個信號集置1    成功:0;失敗:-1,設置errno

    int sigaddset(sigset_t *set, int signum); 將某個信號加入信號集合中   成功:0;失敗:-1,設置errno

    int sigdelset(sigset_t *set, int signum); 將某信號從信號清出信號集    成功:0;失敗:-1,設置errno

int sigismember(const sigset_t *set, int signum);

判斷某個信號是否在信號集中:在:1;不在:0;出錯:-1,設置errno

    sigismember其余操作函數中的set均為傳出參數。sigset_t類型的本質是位圖。但不應該直接使用位操作,而應該使用上述函數,保證跨系統操作有效。

    對比認知select 函數。

sigprocmask函數

用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取或修改進程控制塊中的信號屏蔽字。

    嚴格注意,屏蔽信號:只是將信號處理延后執行(延至解除屏蔽);而忽略表示將信號丟棄處理。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失敗:-1,設置errno

參數

set:傳入參數,是一個自定義信號集合。由參數how來指示如何修改當前信號屏蔽字。

oldset:傳出參數,保存舊的信號屏蔽字。

how參數取值:假設當前的信號屏蔽字為mask

  1. SIG_BLOCK: how設置為此值,set表示需要屏蔽的信號。相當於 mask = mask|set
  2. SIG_UNBLOCK: how設置為此,set表示需要解除屏蔽的信號。相當於 mask = mask & ~set
  3. SIG_SETMASK: how設置為此,set表示用於替代原始屏蔽及的新屏蔽集。相當於mask = set若,調用sigprocmask解除了對當前若干個信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。

sigpending函數

讀取當前進程的未決信號集

int sigpending(sigset_t *set); set傳出參數。   返回值:成功:0;失敗:-1,設置errno

例:編寫程序。把所有常規信號的未決狀態打印至屏幕。 sigpending.c

 

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void printset(sigset_t *ped)
{
	int i;
	for(i = 1; i < 32; i++){
		if((sigismember(ped, i) == 1)){
			putchar('1');
		} else {
			putchar('0');
		}
	}
	printf("\n");
}

int main(void)
{
	sigset_t set, ped;
#if 1
	sigemptyset(&set);
	sigaddset(&set, SIGINT);
#else
	sigaddset(&set, SIGSEGV);
	sigaddset(&set, SIGKILL);
	sigaddset(&set, SIGQUIT);
	sigfillset(&set);
#endif
	sigprocmask(SIG_BLOCK, &set, NULL);	//不獲取原屏蔽字

	while (1) {
		sigpending(&ped);               //獲取未決信號集
		printset(&ped);
		sleep(1);
	}

	return 0;
}

 

  

 

信號捕捉

signal函數

注冊一個信號捕捉函數

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler); 成功返回函數指針失敗返回SIG_ERR,設置errno

該函數由ANSI定義,由於歷史原因在不同版本的Unix和不同版本的Linux中可能有不同的行為因此應該盡量避免使用它取而代之使用sigaction函數

    void (*signal(int signum, void (*sighandler_t)(int))) (int);

能看出這個函數代表什么意思嗎?  注意多在復雜結構中使用typedef

sigaction函數

修改信號處理動作(通常在Linux用其來注冊一個信號的捕捉函數)

    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);  成功:0;失敗:-1,設置errno

參數:

act:傳入參數,新的處理方式。

oldact:傳出參數,舊的處理方式。 signal.c

struct sigaction結構體

    struct sigaction {

        void     (*sa_handler)(int);

        void     (*sa_sigaction)(int, siginfo_t *, void *);

        sigset_t   sa_mask;

        int       sa_flags;

        void     (*sa_restorer)(void);

    };

sa_restorer:該元素是過時的,不應該使用,POSIX.1標准將不指定該元素。(棄用)

sa_sigaction:當sa_flags被指定為SA_SIGINFO標志時,使用該信號處理程序。(很少使用)  

重點掌握:

sa_handler:指定信號捕捉后的處理函數名(即注冊函數)。也可賦值為SIG_IGN表忽略 或 SIG_DFL表執行默認動作

sa_mask: 調用信號處理函數時,所要屏蔽的信號集合(信號屏蔽字)。注意:僅在處理函數被調用期間屏蔽生效,是臨時性設置。

sa_flags:通常設置為0,表使用默認屬性。

信號捕捉特性

  1. 進程正常運行時,默認PCB中有一個信號屏蔽字,假定為☆,它決定了進程自動屏蔽哪些信號。當注冊了某個信號捕捉函數,捕捉到該信號以后,要調用該函數。而該函數有可能執行很長時間,在這期間所屏蔽的信號不由☆來指定。而是用sa_mask來指定。調用完信號處理函數,再恢復為☆。
  2. XXX信號捕捉函數執行期間XXX信號自動被屏蔽
  3. 阻塞的常規信號不支持排隊,產生多次只記錄一次。(后32個實時信號支持排隊)

1:為某個信號設置捕捉函數 sigaction1.c

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

/*自定義的信號捕捉函數*/
void sig_int(int signo)
{
    printf("catch signal SIGINT\n");//單次打印
    sleep(10);
    printf("----slept 10 s\n");
}

int main(void)
{
    struct sigaction act;        

    act.sa_handler = sig_int;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);        //不屏蔽任何信號
    sigaddset(&act.sa_mask, SIGQUIT);

    sigaction(SIGINT, &act, NULL);

    printf("------------main slept 10\n");
    sleep(10);

    while(1);//該循環只是為了保證有足夠的時間來測試函數特性

    return 0;
}

 

 

 

 

例2: 驗證在信號處理函數執行期間,該信號多次遞送,那么只在處理函數之行結束后,處理一次。  【sigaction2.c】

 

/*自動屏蔽本信號,調用完畢后屏蔽自動解除*/

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/*自定義的信號捕捉函數*/
void sig_int(int signo)
{
    printf("catch signal SIGINT\n");
    sleep(10);            //模擬信號處理函數執行很長時間
    printf("end of handler\n");
}

int main(void)
{
    struct sigaction act;        

    act.sa_handler = sig_int;
    sigemptyset(&act.sa_mask);        //依然不屏蔽任何信號
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);    //注冊信號處理函數

    while(1);

    return 0;
}

 

 

3:驗證sa_mask在捕捉函數執行期間的屏蔽作用。 sigaction3.c

 

/*當執行SIGINT信號處理函數期間
 *多次收到SIGQUIT信號都將被屏蔽(阻塞)
 *SIGINT信號處理函數處理完,立刻解除對
 *SIGQUIT信號的屏蔽,由於沒有捕捉該信號,
 *將立刻執行該信號的默認動作,程序退出
 */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_int(int signo)
{
    printf("catch signal SIGINT\n");
    sleep(10);            //模擬信號處理函數執行很長時間
    printf("end of handler\n");
}

int main(void)
{
    struct sigaction act;        

    act.sa_handler = sig_int;
    sigemptyset(&act.sa_mask);        
    sigaddset(&act.sa_mask, SIGQUIT);    

    /*將SIGQUIT加入信號屏蔽集,這就導致,在調用信號處理函數期間
     *不僅不響應SIGINT信號本身,還不響應SIGQUIT*/
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);        //注冊信號SIGINT捕捉函數

    while(1);

    return 0;
}

 

 

 

內核實現信號捕捉過程:

 

 

信號引起的時序競態

pause函數

操作系統內唯一一個主動造成進程掛起的系統調用。調用該系統調用的進程將處於阻塞狀態(主動放棄cpu) 直到有信號遞達將其喚醒。

    int pause(void); 返回值:-1 並設置errnoEINTR

返回值:

① 如果信號的默認處理動作是終止進程,則進程終止,pause函數么有機會返回。

② 如果信號的默認處理動作是忽略,進程繼續處於掛起狀態,pause函數不返回。

③ 如果信號的處理動作是捕捉,則【調用完信號處理函數之后,pause返回-1

   errno設置為EINTR,表示“被信號中斷”。想想我們還有哪個函數只有出錯返回值。

pause收到的信號不能被屏蔽,如果被屏蔽,那么pause就不能被喚醒。

例:使用pausealarm來實現sleep函數。 pause_sleep.c

 

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/*所有信號處理函數的原型,都類此,
 *無返回值(void),只有一個參數,表示信號編號*/
void sig_alrm(int signo)
{
    /*用來占位,可以不做任何事,但這個函數存在
     *SIGALRM信號
     *就不執行默認動作終止進程,而做其它事情*/
}

unsigned int mysleep(unsigned int sec)
{
    struct sigaction act, old;
    unsigned int unslept;            //保存未休眠夠的時間

    act.sa_handler = sig_alrm;
    sigemptyset(&act.sa_mask);        //清空
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, &old);    //注冊信號處理函數sig_alrm
                                    //同時要保存舊的處理方式

    alarm(sec);                        //設置sec秒鬧鍾
    pause();            //進程阻塞,收到一個信號后,pause返回-1,解除阻塞

    unslept = alarm(0);    //取消舊的定時器,將剩余時間保存
    /*
     *正常情況下,鬧鍾到sec秒后發送SIGALRM信號,
     *pause函數收到信號,調用信號處理函數sig_alrm
     *pause函數返回,此時定時器已經到時,
     *執行unslept=alarm(0)不起作用,unslept為0
     
     *如果是異常情況下,定時器還沒到sec秒,
     *pause函數被別的信號喚醒,需要將定時器取消
     *定時器返回剩余時間,也就是未休眠夠的時間
     */

    sigaction(SIGALRM, &old, NULL);    //恢復SIGALRM信號原來的處理方式
    /*因為是在實現庫函數,有可能用戶之前設置過SIGALRM信號的處理方式*/

    return unslept;
}

int main(void)
{
    while(1){
        mysleep(5);
        printf("Five seconds passed\n");
    }

    return 0;
}

 

 

 

注意,unslept = alarm(0)的用法。

例如:睡覺,alarm(10)鬧鈴。

正常: 10后鬧鈴將我喚醒,這時額外設置alarm(0)取消鬧鈴,不會出錯。

異常: 5分鍾,被其他事物吵醒,alarm(0)取消鬧鈴防止打擾。

時序競態

前導例

設想如下場景

欲睡覺,定鬧鍾10分鍾,希望10分鍾后鬧鈴將自己喚醒。

正常:定時,睡覺,10分鍾后被鬧鍾喚醒。

異常:鬧鍾定好后,被喚走,外出勞動,20分鍾后勞動結束。回來繼續睡覺計划,但勞動期間鬧鍾已經響過,不會再將我喚醒。

時序問題分析

回顧,借助pausealarm實現的mysleep函數。設想如下時序:

1. 注冊SIGALRM信號處理函數 sigaction...)

2. 調用alarm(1) 函數設定鬧鍾1秒。

3. 函數調用剛結束,開始倒計時1秒。當前進程失去cpu,內核調度優先級高的進程(有多個)取代當前進程。當前進程無法獲得cpu,進入就緒態等待cpu

4. 1秒后,鬧鍾超時,內核向當前進程發送SIGALRM信號(自然定時法,與進程狀態無關),高優先級進程尚未執行完,當前進程仍處於就緒態,信號無法處理(未決)

5. 優先級高的進程執行完,當前進程獲得cpu資源,內核調度回當前進程執行。SIGALRM信號遞達,信號設置捕捉,執行處理函數sig_alarm

6. 信號處理函數執行結束,返回當前進程主控流程,pause()被調用掛起等待。(欲等待alarm函數發送的SIGALRM信號將自己喚醒)

7. SIGALRM信號已經處理完畢,pause不會等到。

 

主要原因就是在

 

 alarm(sec);                        //設置sec秒鬧鍾
//失去cpu,內核調度優先級高的進程(有多個)取代當前進程,而alarm函數時間到達,優先級高的進程執行完,當前進程獲得cpu資源,內核調度回當前進程執行。SIGALRM信號遞達,信號設置捕捉,執行處理函數sig_alarm(優先被執行),信號處理函數執行結束,返回當前進程主控流程,pause()被調用掛起等待,而 SIGALRM信號已經處理完畢,pause不會等到
pause(); //進程阻塞,收到一個信號后,pause返回-1,解除阻塞

 

解決時序問題

可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與“掛起等待信號”這個兩個操作間隙失去cpu資源。除非將這兩步驟合並成一個“原子操作”。sigsuspend函數具備這個功能。在對時序要求嚴格的場合下都應該使用sigsuspend替換pause

int sigsuspend(const sigset_t *mask); 掛起等待信號

sigsuspend函數調用期間,進程信號屏蔽字由其參數mask指定。

可將某個信號(如SIGALRM)從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然后掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值。如果原來對該信號是屏蔽態,sigsuspend函數返回后仍然屏蔽該信號。

 

 

 

   

 

改進版mysleep sigsuspend.c

 

#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    alarm(nsecs);

    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    sigsuspend(&suspmask); //原子操作,等價於解除屏蔽SIGALRM,並執行pause,不會由於時序靜態導致永久阻

    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL);
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return(unslept);
}

int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }

    return 0;
}

 

 

 

總結

競態條件,跟系統負載有很緊密的關系,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。

    不可靠由其實現原理所致。信號是通過軟件方式實現(跟內核調度高度依賴,延時性強),每次系統調用結束后,或中斷處理處理結束后,需通過掃描PCB中的未決信號集,來判斷是否應處理某個信號。當系統負載過重時,會出現時序混亂。

這種意外情況只能在編寫程序過程中提早預見主動規避,而無法通過gdb程序調試等其他手段彌補。且由於該錯誤不具規律性,后期捕捉和重現十分困難。

全局變量異步I/O

   分析如下父子進程交替數數程序。當捕捉函數里面的sleep取消,程序即會出現問題。請分析原因。

 sync_process.c

 

#include <stdio.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

 

int n = 0, flag = 0;

void sys_err(char *str)

{

    perror(str);

    exit(1);

}

void do_sig_child(int num)

{

    printf("I am child  %d\t%d\n", getpid(), n);

    n += 2;

    flag = 1;

    sleep(1);

}

void do_sig_parent(int num)

{

    printf("I am parent %d\t%d\n", getpid(), n);

    n += 2;

    flag = 1;

    sleep(1);

}

int main(void)

{

    pid_t pid;

    struct sigaction act;

    if ((pid = fork()) < 0)

        sys_err("fork");

    else if (pid > 0) {     

        n = 1;

        sleep(1);

        act.sa_handler = do_sig_parent;

        sigemptyset(&act.sa_mask);

        act.sa_flags = 0;

        sigaction(SIGUSR2, &act, NULL);             //注冊自己的信號捕捉函數  父使用SIGUSR2信號

        do_sig_parent(0);   

        while (1) {

            /* wait for signal */;

           if (flag == 1) {                         //父進程數數完成

                kill(pid, SIGUSR1);

                flag = 0;                        //標志已經給子進程發送完信號

            }

        }

    } else if (pid == 0) {       

        n = 2;

        act.sa_handler = do_sig_child;

        sigemptyset(&act.sa_mask);

        act.sa_flags = 0;

        sigaction(SIGUSR1, &act, NULL);

        while (1) {

            /* wait for signal */;

            if (flag == 1) {

                kill(getppid(), SIGUSR2);
        //這期間很有可能被kernel調度,失去執行權利,而對方獲取了執行時間,通過發送信號回調捕捉函數,從而修改了全局的flag。
         //當對方發生信號過來后立即執行信號處理函數,子進程flag=1,然后回到此次繼續執向下執行,flag=0,然后條件不滿足,不會給
        //父進程發生信號,然后條件都得不到滿足,永遠阻塞(測試請把父子進程中sleep去掉)
flag = 0; } } } return 0; }

 

  

 

示例中通過flag變量標記程序實行進度。flag1表示數數完成。flag0表示給對方發送信號完成。

問題出現的位置,在父子進程kill函數之后需要緊接着調用 flag,將其置0,標記信號已經發送。但,在這期間很有可能被kernel調度,失去執行權利,而對方獲取了執行時間,通過發送信號回調捕捉函數,從而修改了全局的flag

    如何解決該問題呢?可以使用后續課程講到的“鎖”機制。當操作全局變量的時候,通過加鎖、解鎖來解決該問題。現階段,我們在編程期間如若使用全局變量,應在主觀上注意全局變量的異步IO可能造成的問題。

/不可重入函數

一個函數在被調用執行期間(尚未調用結束)由於某種時序又被重復調用稱之為“重入”。根據函數實現的方法可分為“可重入函數”和“不可重入函數”兩種。看如下時序。

 

 

顯然,insert函數是不可重入函數,重入調用,會導致意外結果呈現。究其原因,是該函數內部實現使用了全局變量。

注意事項

  1. 封裝自定義可重入函數該函數內不能含有全局變量及static變量,不能使用malloc、free
  2. 信號捕捉函數應設計為可重入函數
  3. 信號處理程序可以調用的可重入函數可參閱man 7 signal
  4. 沒有包含在上述列表中的函數大多是不可重入的其原因為

a) 使用靜態數據結構

b) 調用了mallocfree

c) 是標准I/O函數

SIGCHLD信號

SIGCHLD的產生條件

子進程終止時

子進程接收到SIGSTOP信號停止時

子進程處在停止態,接受到SIGCONT后喚醒時

借助SIGCHLD信號回收子進程

子進程結束運行其父進程會收到SIGCHLD信號該信號的默認處理動作是忽略可以捕捉該信號在捕捉函數中完成子進程狀態的回收

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/wait.h>

#include <signal.h>

 

void sys_err(char *str)

{

    perror(str);

    exit(1);

}

void do_sig_child(int signo)

{

    int status;    pid_t pid;

    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {

        if (WIFEXITED(status))

            printf("child %d exit %d\n", pid, WEXITSTATUS(status));

        else if (WIFSIGNALED(status))

            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));

    }

}

int main(void)

{

    pid_t pid;    int i;
//
//阻塞信號sigchld,原因在於,父進程還沒注冊完回調函數,子進程就退出了,然后執行默認動作造成子進程未被回收資源處於僵屍

for (i = 0; i < 10; i++) { if ((pid = fork()) == 0) break; else if (pid < 0) sys_err("fork"); } if (pid == 0) { int n = 1; while (n--) { printf("child ID %d\n", getpid()); sleep(1); } return i+1; } else if (pid > 0) {    struct sigaction act; act.sa_handler = do_sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); //恢復信號sigchld while (1) { printf("Parent ID %d\n", getpid()); sleep(1); } } return 0; }

 

如果每創建一個子進程后不使用sleep可以嗎?可不可以將程序中,捕捉函數內部的while替換為if?為什么? 

  if ((pid = waitpid(0, &status, WNOHANG)) > 0) { ... }

  思考:信號不支持排隊,當正在執行SIGCHLD捕捉函數時,再過來一個或多個SIGCHLD信號怎么辦?

使用while(pid = waitpid(0, &status, WNOHANG)) > 0)循環回收子進程資源,避免當正在執行SIGCHLD捕捉函數時,再過來一個或多個SIGCHLD信號(來多個信號只會把未決信號集的某一位設置為1,多個同一信號同時到達,只會被執行一次)被忽略的情況

子進程結束status處理方式

pid_t waitpid(pid_t pid, int *status, int options)

options

WNOHANG

沒有子進程結束,立即返回

WUNTRACED

如果子進程由於被停止產生的SIGCHLDwaitpid則立即返回

WCONTINUED

如果子進程由於被SIGCONT喚醒而產生的SIGCHLDwaitpid則立即返回

獲取status

WIFEXITED(status)

子進程正常exit終止,返回真

WEXITSTATUS(status)返回子進程正常退出值

WIFSIGNALED(status)

子進程被信號終止,返回真

WTERMSIG(status)返回終止子進程的信號值

WIFSTOPPED(status)

子進程被停止,返回真

WSTOPSIG(status)返回停止子進程的信號值

WIFCONTINUED(status)

SIGCHLD信號注意問題

  1. 子進程繼承了父進程的信號屏蔽字和信號處理動作,但子進程沒有繼承未決信號集spending
  2. 注意注冊信號捕捉函數的位置。
  3. 應該在fork之前,阻塞SIGCHLD信號。注冊完捕捉函數后解除阻塞。

信號傳參

發送信號傳參

sigqueue函數對應kill函數但可在向指定進程發送信號的同時攜帶參數

int sigqueue(pid_t pid, int sig, const union sigval value);成功0;失敗:-1,設置errno

           union sigval {

               int   sival_int;

               void *sival_ptr;

           };

向指定進程發送指定信號的同時攜帶數據如傳地址需注意不同進程之間虛擬地址空間各自獨立將當前進程地址傳遞給另一進程沒有實際意義

捕捉函數傳參

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

           struct sigaction {

               void     (*sa_handler)(int);

               void     (*sa_sigaction)(int, siginfo_t *, void *);

               sigset_t   sa_mask;

               int       sa_flags;

               void     (*sa_restorer)(void);

           };

當注冊信號捕捉函數希望獲取更多信號相關信息,不應使用sa_handler而應該使用sa_sigaction。但此時的sa_flags必須指定為SA_SIGINFOsiginfo_t是一個成員十分豐富的結構體類型可以攜帶各種與信號相關的數據

中斷系統調用

系統調用可分為兩類:慢速系統調用和其他系統調用。

  1. 慢速系統調用:可能會使進程永遠阻塞的一類。如果在阻塞期間收到一個信號,該系統調用就被中斷,不再繼續執行(早期);也可以設定系統調用是否重啟。如,readwritepausewait...
  2. 其他系統調用:getpidgetppidfork...

結合pause,回顧慢速系統調用:

慢速系統調用被中斷的相關行為,實際上就是pause的行為: 如,read

① 想中斷pause,信號不能被屏蔽。

② 信號的處理方式必須是捕捉 (默認、忽略都不可以)

③ 中斷后返回-1, 設置errnoEINTR(表“被信號中斷”)

可修改sa_flags參數來設置被信號中斷后系統調用是否重啟。SA_INTERRURT不重啟。 SA_RESTART重啟。

擴展了解

sa_flags還有很多可選參數適用於不同情況:捕捉到信號后,在執行捕捉函數期間,不希望自動阻塞該信號,可將sa_flags設置為SA_NODEFER,除非sa_mask中包含該信號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM