1、信號注冊函數: signal
#include
void (*signal(int signum, void (*sighandler_t)(int))) (int); typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
入參:
signum 哪個信號
handle 信號所對應的處理函數;SIG_IGN:忽略此信號;SIG_DFL:按系統默認
方式處理
該函數由ANSI定義,由于歷史原因在不同版本的Unix和不同版本的Linux中可能有不同的行為。因此應該盡量避免使用它,取而代之使用sigaction函數。
2、修改信號處理動作(通常在Linux用其來注冊一個信號的捕捉函數)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功:0;失敗:-1,設置errno
參數:
act:傳入參數,新的處理方式。
oldact:傳出參數,舊的處理方式。
struct sigaction結構體
struct sigaction {
void
void
sigset_t
int
void
(*sa_handler)(int);
(*sa_sigaction)(int, siginfo_t *, void *);
sa_mask;
sa_flags;
(*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個實時信號支持排
隊)
3、pause函數 : 掛起當前的進程,直到收到一個信號,才會接著執行
調用該函數可以造成進程主動掛起,等待信號喚醒。調用該系統調用的進程將處于阻塞狀態(主動放棄cpu) 直到有信號遞達將其喚醒。
int pause(void);
返回值:-1 并設置errno為EINTR
① 如果信號的默認處理動作是終止進程,則進程終止,pause函數么有機會返回。 ② 如果信號的默認處理動作是忽略,進程繼續處于掛起狀態,pause函數不返回。 ③ 如果信號的處理動作是捕捉,則【調用完信號處理函數之后,pause返回-1】errno設置為 EINTR,表示“被信號中斷”。想想我們還有哪個函數只有出錯返回
值。
④ pause收到的信號不能被屏蔽,如果被屏蔽,那么pause就不能被喚醒。
4、alarm函數
設置定時器(鬧鐘)。在指定seconds后,內核會給當前進程發送14)SIGALRM信號。進程收到該信號,默認動作終止。
每個進程都有且只有唯一個定時器。
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒數,無失敗。
常用:取消定時器alarm(0),返回舊鬧鐘余下秒數。
例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)
定時,與進程狀態無關(自然定時法)!就緒、運行、掛起(阻塞、暫停)、終止、僵尸...無論進程處于何種狀態,alarm都計時。
使用time命令查看程序執行的時間。程序運行的瓶頸在于IO,優化程序,首選優化IO。
實際執行時間 = 系統時間 + 用戶時間 + 等待時間
5、raise函數: 自己給自己發送信號 raise(signo) == kill(getpid(), signo);
int raise(int sig);
成功:0,失敗非0值
入參: 發送的信號
6、abort 函數:給自己發送異常終止信號 6) SIGABRT 信號,終止并產生core文件 void abort(void); //該函數無返回
kill函數/命令產生信號
1、kill命令產生信號:kill -SIGKILL pid
2、kill函數:給指定進程發送指定信號(不一定殺死)
7、int kill(pid_t pid, int sig);
成功:0;失敗:-1 (ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno sig:不推薦直接使用數字,應使用宏名,因為不同操作系統信號編號可能不同,但名稱一致。
pid > 0: 發送信號給指定的進程。
pid = = 0: 發送信號給 與調用kill函數進程屬于同一進程組的所有進程。
pid < 0: 取|pid|發給對應進程組。
pid = = -1:發送給進程有權限發送的系統中所有進程。
進程組:每個進程都屬于一個進程組,進程組是一個或多個進程集合,他們相互關聯,共同完成一個實體任務,每個進程組都有一個進程組長,默認進程組ID與進程組長ID
相同。
權限保護:super用戶(root)可以發送信號給任意用戶,普通用戶是不能向系統用戶發送信號的。 kill -9 (root用戶的pid) 是不可以的。同樣,普通用戶也不能向其他
普通用戶發送信號,終止其進程。 只能向自己創建的進程發送信號。普通用戶基本規則是:發送者實際或有效用戶ID == 接收者實際或有效用戶ID
時序競態
設想如下場景:
欲睡覺,定鬧鐘10分鐘,希望10分鐘后鬧鈴將自己喚醒。
正常:定時,睡覺,10分鐘后被鬧鐘喚醒。
異常:鬧鐘定好后,被喚走,外出勞動,20分鐘后勞動結束。回來繼續睡覺計劃,但勞動期間鬧鐘已經響過,不會再將我喚醒。
時序問題分析
回顧,借助pause和alarm實現的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不會等到。
解決時序問題
可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與“掛起等待信號”這個兩個操作間隙失去cpu資源。除非將這兩步驟合并成一個“原子操作”。sigsuspend函數具備這個功能。在對時序要求嚴格的場
合下都應該使用sigsuspend替換pause。
int sigsuspend(const sigset_t *mask);//掛起等待信號。
sigsuspend函數調用期間,進程信號屏蔽字由其參數mask指定。
可將某個信號(如SIGALRM)從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然后掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值。如果原來對該信號是屏蔽態,sigsuspend函數返回后仍然屏蔽該信號。
競態條件,跟系統負載有很緊密的關系,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。
不可靠由其實現原理所致。信號是通過軟件方式實現(跟內核調度高度依賴,延時性強),每次系統調用結束后,或中斷處理處理結束后,需通過掃描PCB中的未決信號集,來判斷是否應處理某個信號。當系統負載過重時,會出現時序混亂。
這種意外情況只能在編寫程序過程中,提早預見,主動規避,而無法通過gdb程序調試等其他手段彌補。且由于該錯誤不具規律性,后期捕捉和重現十分困難。
8、setitimer函數
設置定時器(鬧鐘)。 可代替alarm函數。精度微秒us,可以實現周期定時。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
成功:0;失敗:-1,設置errno
參數:which:指定定時方式
① 自然定時:ITIMER_REAL → 14)SIGLARM 計算自然時間
② 虛擬空間計時(用戶空間):ITIMER_VIRTUAL → 26)SIGVTALRM 只計算進程占
用cpu的時間
③ 運行時計時(用戶+內核):ITIMER_PROF → 27)SIGPROF 計算占用cpu及執行系
統調用的時間
信號集操作函數:
內核通過讀取未決信號集來判斷信號是否應被處理。信號屏蔽字mask可以影響未決信號集。而我們可以在應用程序中自定義set來改變mask。已達到屏蔽指定信號的目的。
信號集設定:
sigset_t set; // typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); //將某個信號集清0
成功:0;失敗:-1
int sigfillset(sigset_t *set); //將某個信號集置
1
成功:0;失敗:-1
int sigaddset(sigset_t *set, int signum); //將某個信號加入信號集
成功:0;失敗:-1
int sigdelset(sigset_t *set, int signum); //將某個信號清出信號集
成功:0;失敗:-1
int sigismember(const sigset_t *set, int signum);//判斷某個信號是否在信號
集中
返回值:在集合:1;不在:0;出錯:-1
sigset_t類型的本質是位圖。但不應該直接使用位操作,而應該使用上述函數,保證
跨系統操作有效。
對比認知select 函數。
9、sigprocmask函數
用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取或修改進程的信號屏蔽字(PCB
中)
嚴格注意,屏蔽信號:只是將信號處理延后執行(延至解除屏蔽);而忽略表示將信號
丟處理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失敗:-1,設置errno
參數:
set:傳入參數,是一個位圖,set中哪位置1,就表示當前進程屏蔽哪個信號。
oldset:傳出參數,保存舊的信號屏蔽集。
how參數取值:假設當前的信號屏蔽字為mask
SIG_BLOCK: 當how設置為此值,set表示需要屏蔽的信號。相當于 mask = mask|set
SIG_UNBLOCK: 當how設置為此,set表示需要解除屏蔽的信號。相當于 mask = mask & ~set
SIG_SETMASK: 當how設置為此,set表示用于替代原始屏蔽及的新屏蔽集。相當于 mask = set若,調用sigprocmask解除了對當前若干個信號的阻塞,則在sigprocmask返回
前,至少將其中一個信號遞達。
10、sigpending函數
讀取當前進程的未決信號集
int sigpending(sigset_t *set); //set傳出參數。
返回值:成功:0;失敗:-1,設置errno