Linux訊號程式設計實踐(三) 訊號在核心中的表示(sigaction&sigqueue)
訊號在核心中的表示
實際執行訊號的處理動作稱為訊號遞達(Delivery),訊號從產生到遞達之間的狀態,稱為訊號未決(Pending)。程序可以選擇阻塞(Block)某個訊號。被阻塞的訊號產生時將保持在未決狀態,直到程序解除對此訊號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要訊號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作。訊號在核心中的表示可以看作是這樣的:
1)block集(阻塞集、遮蔽集):一個程序所要遮蔽的訊號,在對應要遮蔽的訊號位置1
2)pending集(未決訊號集):如果某個訊號在程序的阻塞集中,則也在未決集中對應位置1,表示該訊號不能被遞達,不會被處理3)handler(訊號處理函式集):表示每個訊號所對應的訊號處理函式,當訊號不在未決集中時,將被呼叫。
4)block狀態字、pending狀態字均64位(bit);
5)block狀態字使用者可以讀寫,pending狀態字使用者只能讀;這是訊號設計機制。
那麼我們該如何對訊號的遮蔽字狀態進行改變和讀取呢?接下來我們介紹一組訊號集操作函式:
#include <signal.h> int sigemptyset(sigset_t *set); //把訊號集清零;(64bit/8=8位元組) int sigfillset(sigset_t *set); //把訊號集64bit全部置為1 int sigaddset(sigset_t *set, int signo); //根據signo,把訊號集中的對應位置成1 int sigdelset(sigset_t *set, int signo); //根據signo,把訊號集中的對應位置成0 int sigismember(const sigset_t *set, int signo); //判斷signo是否在訊號集中
sigprocmask 功能:讀取或者更改程序的訊號遮蔽字(Block)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功則為0,若出錯則為-1
讀取:如果oset是非空指標,則讀取程序的當前訊號遮蔽字通過oset引數傳出。
更改:如果set是非空指標,則更改程序的訊號遮蔽字,引數how指示如何更改。如果oset和set都是非空指標,則先將原來的訊號遮蔽字備份到oset裡,然後根據set和how引數更改訊號遮蔽字。假設當前的訊號遮蔽字為mask,下表說明了how引數的可選值。
sigpending獲取訊號未決狀態字(pending)資訊,儲存至set態,NSIG訊號的最大值=64。
#include <signal.h>
int sigpending(sigset_t *set);
sigismember函式
用來測試引數signum 代表的訊號是否已加入至引數set訊號集裡。如果訊號集裡已有該訊號則返回1,否則返回0。如果有錯誤則返回-1。出錯的情況及其錯誤程式碼見下:
EFAULT 引數set指標地址無法存取
EINVAL 引數signum 非合法的訊號編號
int sigismember(const sigset_t *set,int signum);
我們註冊一個SIGINT訊號,打印出pending的狀態,結果如下:
void handler(int sig)
{
printf("recv a sig=%d\n",sig);
}
void printsigset(sigset_t *set)
{
int i;
for(i=1;i<NSIG;i++)
{
if(sigismember(set,i))
putchar('1'); //打印出未決態
else
putchar('0');
}
printf("\n");
}
int main()
{
sigset_t pset;
if(signal(SIGINT,handler)==SIG_ERR)
ERR_EXIT("signal error!");
while(1)
{
sigpending(&pset);
printsigset(&pset);
sleep(1);
}
return 0;
}
訊號沒有阻塞,不會發生未決狀態,直接遞達。
在接下來的例子中,我們先遮蔽SIGINT訊號, 但是如果該程序接收到了SIGQUIT訊號, 則將對SIGINT訊號的遮蔽節解除,當然,我們需要先註冊SIGINT和SIGQUIT訊號。
/*開始阻塞訊號的程式,產生未決狀態*/
void handler(int sig)
{
if(sig==SIGINT)
printf("recv a sig=%d\n",sig);
else if(sig==SIGQUIT) //解除SIGINT的遮蔽
{
sigset_t uset;
sigemptyset(&uset);
sigaddset(&uset,SIGINT);
sigprocmask(SIG_UNBLOCK,&uset,NULL);
}
// printf("recv a sig=%d\n",sig);
}
void printsigset(sigset_t *set)
{
int i;
for(i=1;i<NSIG;i++)
{
if(sigismember(set,i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main()
{
sigset_t pset;
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset,SIGINT);
if(signal(SIGINT,handler)==SIG_ERR)
ERR_EXIT("signal error!");
if(signal(SIGQUIT,handler)==SIG_ERR)
ERR_EXIT("signal error!");
sigprocmask(SIG_BLOCK,&bset,NULL);//遮蔽SIGINT訊號
while(1)
{
sigpending(&pset);
printsigset(&pset);
sleep(1);
}
return 0;
}
當我們按下ctrl+c產生訊號時,訊號被阻塞,處於未決狀態。接收到SIGQUIT訊號時,解除阻塞,進入遞達狀態,但是隻會對訊號做出一次反應,即使你按了很多次ctrl+c,原因就在於,SIGINT是不可靠訊號,不支援排隊,只保留了一個。
如果我們採用實時訊號的話,例如SIGRTMIN,那麼對訊號來說是支援排隊的,不會發生丟失的情況,在解除阻塞後,會對每個訊號做出處理。
Sigaction
前面我們講過了使用signal安裝不可靠訊號,雖然signal不如sigaction功能豐富,但是也可以安裝可靠訊號;
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
功能:
sigaction函式用於改變程序接收到特定訊號後的行為。
簡而言之引數就是(訊號,指標,原行為)
關於sigaction結構體
第二個引數最為重要,其中包含了對指定訊號的處理、訊號所傳遞的資訊、訊號處理函式執行過程中應遮蔽掉哪些函式等
struct sigaction {
//訊號處理程式 不接受額外資料(比較過時)
void (*sa_handler)(int);
//訊號處理程式能接受額外資料,和sigqueue配合使用(支援訊號排隊,訊號傳送其他資訊),推薦使用
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //遮蔽
int sa_flags; //表示訊號的行為:SA_SIGINFO表示能接受資料
void (*sa_restorer)(void); //廢棄不用了
};
sa_handler的原型是一個引數為int,返回型別為void的函式指標。引數即為訊號值,所以訊號不能傳遞除訊號值之外的任何資訊;
sa_sigaction的原型是一個帶三個引數,型別分別為int,struct siginfo *,void *,返回型別為void的函式指標。第一個引數為訊號值;第二個引數是一個指向struct siginfo結構的指標,此結構中包含訊號攜帶的資料值;第三個引數沒有使用。
sa_mask指定在訊號處理程式執行過程中,哪些訊號應當被阻塞。預設當前訊號本身被阻塞。
sa_flags包含了許多標誌位,比較重要的一個是SA_SIGINFO,當設定了該標誌位時,表示訊號附帶的引數可以傳遞到訊號處理函式中。即使sa_sigaction指定訊號處理函式,如果不設定SA_SIGINFO,訊號處理函式同樣不能得到訊號傳遞過來的資料,在訊號處理函式中對這些資訊的訪問都將導致段錯誤。
sa_restorer已過時,POSIX不支援它,不應再使用。
注意:回撥函式sa_handler和sa_sigaction只能選一個
因此,當你的訊號需要接收附加資訊的時候,你必須給sa_sigaction賦訊號處理函式指標,同時還要給sa_flags賦SA_SIGINFO,
例項1:利用sigaction實現了signal函式的功能__sighandler_t my_signal(int sig,__sighandler_t handler)
{
struct sigaction act;
struct sigaction oldact;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
if(sigaction(sig,&act,&oldact)<0)
return SIG_ERR;
return oldact.sa_handler;
}
void handler(int sig)
{
printf("recv a sig=%d\n",sig);
}
int main()
{
/* struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
if(sigaction(SIGINT,&act,NULL)<0)
ERR_EXIT("sigaction error\n");
*/
my_signal(SIGINT,handler);
while(1)
pause();
return 0;
}
sa_mask選項
在執行handler 的時候, 如果此時程序收到了sa_mask所包含的訊號, 則這些訊號將不會被響應, 直到handler函式執行完畢。
sigprocmask使其即使發生了也不能遞達,但是sa_mask 僅是在處理handler是遮蔽外來的訊號;兩者的區別還是要好好搞一搞的。
void handler(int sig)
{
printf("recv a sig=%d\n",sig);
sleep(5);
}
int main()
{
struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);//遮蔽SIGQUIT訊號
act.sa_flags=0;
if(sigaction(SIGINT,&act,NULL)<0)
ERR_EXIT("sigaction error\n");
while(1)
pause();
return 0;
}
在響應SIGINT訊號即handler處理時,SIGQUIT暫時被遮蔽,但是一旦handler函式處理完後,立即對SIGQUIT進行響應。
siginfo_t結構:
siginfo_t{
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
}
sigqueue
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
功能
sigqueue是新的傳送訊號系統呼叫,主要是針對實時訊號提出的支援訊號帶有引數,與函式sigaction()配合使用。
和kill函式相比多了一個引數:const union sigval value(int kill(pid_t pid, int sig)),因此sigqueue()可以比kill()傳遞更多的資訊,但sigqueue()只能向一個程序傳送訊號,而不能傳送訊號給一個程序組。
引數
引數1是指定接收訊號的程序id,引數2確定即將傳送的訊號;
引數3是一個聯合資料結構union sigval,指定了訊號傳遞的引數,即通常所說的4位元組值。
注意:要想在程序間通訊的話,sa_flags要置為SA_SIGINFOsigval聯合體
typedef union sigval{
int sival_int;
void *sival_ptr;
} sigval_t;
接下來我們模擬一下程序間通訊的例項:
先執行hello開啟接收,然後使用send傳送訊號;通過這種方式可以達到程序見通訊的目的。
Hello.c
void handler(int sig,siginfo_t *info,void *ctx)
{
printf("recv a sig=%d data=%d\n",sig,info->si_value.sival_int);
}
int main()
{
struct sigaction act;
act.sa_sigaction=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
if(sigaction(SIGINT,&act,NULL)<0)
ERR_EXIT("sigaction error\n");
while(1)
pause();
return 0;
}
Send
int main(int argc,char *argv[])
{
if(argc!=2)
{
fprintf(stderr,"Usage %s pid\n",argv[0]);
exit(EXIT_FAILURE);
}
pid_t pid=atoi(argv[1]);
union sigval v;
v.sival_int=100;
sigqueue(pid,SIGINT,v);
return 0;
}