🔥博客主頁:?我要成為C++領域大神
🎥系列專欄:【C++核心編程】?【計算機網絡】?【Linux編程】?【操作系統】
??感謝大家點贊👍收藏?評論??本博客致力于知識分享,與更多的人進行學習交流
?
關于阻塞函數和信號處理沖突
阻塞函數處于等待狀態,等待系統通知或事件消息,如果接收到信號,信號處理函數會中斷阻塞函數的執行。這可能導致阻塞函數提前返回。
下面是這一現象的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>void sigint(int signo)
{printf("捕捉到了SIGINT %d號信號\n",signo);
}int main()
{struct sigaction act,oact;act.sa_handler=sigint;act.sa_flags=0;sigemptyset(&act.sa_mask);sigaction(SIGINT,&act,&oact);char buffer[1024];bzero(buffer,sizeof(buffer));int len;
TryAgain:while((len=read(STDIN_FILENO,buffer,sizeof(buffer)))>0){write(STDOUT_FILENO,buffer,sizeof(buffer));bzero(buffer,sizeof(buffer));}if(len==-1){printf("阻塞函數被信號中斷,TryAgain。。。\n");goto TryAgain;}return 0;
}
時序競態
什么是時序競態?將同一個程序執行兩次,正常情況下,前后兩次執行得到的結果應該是一樣的。但由于系統資源競爭的原因,前后兩次執行的結果有可能得到不一樣的結果,這個現象就是時序競態。
pause函數
進程調用pause函數時,會造成進程主動掛起(處于阻塞狀態,并主動放棄CPU),并且等待信號將其喚醒。
信號的處理方式有三種:1. 默認;2. 忽略;3. 捕捉。進程收到一個信號后,會先處理響應信號,再喚醒pause函數。于是有下面幾種情況:
① 如果信號的默認處理動作是終止進程,則進程將被終止,也就是說一收到信號進程就終止了,pause函數根本就沒有機會返回;
② 如果信號的默認處理動作是忽略,則進程將直接忽略該信號,相當于沒收到這個信號,進程繼續處于掛起狀態,pause函數不返回;
③ 如果信號的處理動作是捕捉,則進程調用完信號處理函數之后,pause返回-1,errno設置為EINTR,表示“被信號中斷”。
④ pause收到的信號不能被屏蔽,如果被屏蔽,那么pause就不能被喚醒。
因為alarm函數可以在設定的時間之后發送SIGALRM信號,pause函數又可以將進程掛起等待信號,則二者結合可以自己寫一個sleep函數
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void sig_alrm(int signo)
{/*空調用,僅負責捕捉,防止信號殺死進程*/
}unsigned int mysleep(unsigned int sec)
{signal(SIGALRM,sig_alrm);unsigned int sleptSec=alarm(sec);//設置一個定時器,定時器結束后發送SIGALRM信號int retNum=pause();//設置進程主動掛起,等待信號將其喚醒。信號的處理行為是捕捉,調用完捕捉函數后返回-1printf("%d\n",retNum);return sleptSec;
}int main()
{while(1){mysleep(2);printf("2s Passed\n");}return 0;
}
pause
的返回值是-1
時序競態問題分析
SIGALRM
默認動作是終止進程,因此我們要將其捕捉,對SIGALRM
注冊信號處理函數;
調用alarm(1)
函數定時1秒鐘;
alarm(1)
調用結束,定時器開始計時。調用完alarm
后立即調用了sleep
函數,因為調用了系統函數,所以會發生內核層切換,進程失去CPU,進入就緒態等待CPU。使得當前進程無法獲得CPU;
alarm
函數采用自然定時法,定時器將一直計時,與進程狀態無關。于是,1秒后,鬧鐘定時時間到,內核向當前進程發送SIGALRM
信號。
在sleep
函數調用完畢后,返回進程前,會檢測一下是否有未處理的信號,檢測到了SIGALRM信號并進行捕捉處理。
信號處理完畢后,返回當前主控流程,并調用pause()
函數,掛起等待alarm
函數發送的SIGALRM
信號將自己喚醒;
但實際SIGALRM
信號已經處理完畢,pause()
函數永遠不會等到,于是就永遠掛起了。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void sig_alrm(int signo)
{/*空調用,僅負責捕捉,防止信號殺死進程*/
}unsigned int mysleep(unsigned int sec)
{signal(SIGALRM,sig_alrm);unsigned int sleptSec=alarm(sec);//設置一個定時器,定時器結束后發送SIGALRM信號sleep(3);//對SIGALRM進行了處理,導致了進程被永遠掛起pause();//設置進程主動掛起,等待信號將其喚醒。信號的處理行為是捕捉,調用完捕捉函數后返回-1return sleptSec;
}int main()
{while(1){mysleep(2);printf("2s Passed\n");}return 0;
}
運行結果:由于pause
收不到SIGALRM
信號,所以被一直掛起了
解決時序競態問題
在調用sleep
函數之前,屏蔽SIGALRM
信號,防止sleep對信號進行處理。在sleep
調用完成之后,解除對SIGALRM
的屏蔽,然后pause()
掛起進程等待SIGALRM
將其喚醒。
但在解除屏蔽與pause等待掛起信號之間,還是有可能進行其他操作處理SIGALRM
,除非將這兩個步驟做成一個“原子操作”。Linux系統提供的sigsuspend
函數就具備這個功能。所以,在時序要求比較嚴格的場合下都應該使用sigsuspend
函數,而非pause
函數。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>void sig_alarm(int signo)
{//null 空調用
}unsigned int mysleep(unsigned int seconds)
{//設置捕捉函數struct sigaction act,oldact;act.sa_handler=sig_alarm;act.sa_flags=0;sigemptyset(&act.sa_mask);sigaction(SIGALRM,&act,&oldact);//設置信號屏蔽sigset_t set,oldset;sigemptyset(&set);sigaddset(&set,SIGALRM);sigprocmask(SIG_SETMASK,&set,&oldset);alarm(2);sleep(3);sigsuspend(&act.sa_mask);
}int main()
{while(1){mysleep(2);printf("two seconds\n");}return 0;
}
Shell腳本實現進程的外部控制
實現一個Shell腳本來對進程進行外部控制,可以使用信號機制來控制進程的啟動、停止和繼續運行 。Windows常用的任務管理器就是這個機制,利用外部控制的信號來實現進程的掛起、終止、提高優先級等。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>void sig_stop(int n)//捕捉SIGUSR1信號
{printf("進程已被殺死\n");exit(0);
}void sig_count(int n)
{printf("進程已被掛起\n");pause();
}void test_sigaction(void)
{struct sigaction act,bct,oact;act.sa_handler=sig_stop;act.sa_flags=0;sigemptyset(&act.sa_mask);sigaction(SIGUSR1,&act,&oact);bct.sa_handler=sig_count;bct.sa_flags=0;sigemptyset(&bct.sa_mask);sigaction(SIGUSR2,&bct,NULL);
}void output_pid(void)
{int fd=open("config.conf",O_RDWR|O_CREAT,0664);pid_t pid=getpid();char id[10];bzero(id,sizeof(id));sprintf(id,"%d",pid);write(fd,id,strlen(id));close(fd);
}
int main()
{test_sigaction();//設定捕捉output_pid();while(1){printf("this is test...\n");sleep(2);}return 0;
}
#!/bin/bashPID=$(cat config.conf)if [ $1=="stop" ]
then kill -10 $PID #發送SIGUSR1信號
elif [ $1=="cont" ]
then kill -12 $PID #發送SIGUSR2信號
else print "control call faild\n"
fi
運行結果:通過外部腳本成功殺死進程