来自:
https://blog.csdn.net/dawn_sf/article/details/74177899
信号与中断的区别:
信号与中断的相似点:
(1)采用了相同的异步通信方式;(2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
(3)都在处理完毕后返回到原来的断点;
(4)对信号或中断都可进行屏蔽。
信号与中断的区别:
(1)中断有优先级,而信号没有优先级,所有的信号都是平等的;
(2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
(3)中断响应是及时的,而信号响应通常都有较大的时间延迟。
在Linux下当我们想强制结束一个程序的时候,我们通常会给它发送一个信号然后该进程捕捉到信号,再然后该进程执行一定操作最
终被终止.信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。通常信号是由一
个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生
成,接收到一个信号叫捕获。信号的捕捉这篇可能不会详细的说到,因为我想给它专门讲一个博客,因为信号捕捉的实例也是蛮多的.
首先我们认识一下这些信号:
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 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
列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠
信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。我们这些新手就着重
究前31个,后面的对于现在来说用处不大.
信号的产生:
产生信号的条件主要有:
1.用户在终端按下某些建时,终端驱动程序会发送信号给前台进程,例如ctrl -c 产生SIGINT信号.ctrl-\产生SIGQUIT信号,
Ctrl-Z产生SIGTSTP信号(可使前台进程停止)
2.硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号.例如当前进程执行了除以0的
指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程.在比如当前进程访问了非法内存地址,MMU会
产生异常,内核将这个异常解释为SIGSEGV信号发送给进程.
3,一个进程调用kill(2)函数可以发送信号到另一个进程.可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill
(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理是终止进程.当内核检测到某种软件条件发生时也
可以通过信号通知进程. 例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号.如果不想按默认动作
处理信号,用户程序可以调用sigacttion(2)函数告诉内核处理某种信号.
再然后,你接收到信号的处理动作有一下三种:
1.忽略此信号
2.执行该信号的默认处理动作
3.提供一个信号处理函数,要求内核处理该信号时切换到用户执行这个处理函数,这种方式称为捕捉一个信号.
信号的阻塞
主要信号的函数:
例题:
- #include<stdio.h>
- #include<signal.h>
- #include<unistd.h>
- void printsigset(sigset_t *set)
- {
- int i = 0;
- for(;i<32;i++){
- if(sigismember(set,i))
- putchar('1');
- else
- putchar('0');
- }
- puts("");
- }
- int main()
- {
- sigset_t s,p;
- sigemptyset(&s);
- sigaddset(&s,SIGINT);
- sigprocmask(SIG_BLOCK,&s,NULL);
- while(1)
- {
- sigpending(&p);
- printsigset(&p);
- sleep(1);
- }
- return 0;
- }
我们加上注释:
这个程序的大概意思就是 我们阻塞一个信号集,让它一直处于未决状态,并把它里面的信号编号显示出来,比如中途我们加入了һ
个ctrl+c,后面信号集里面就会出现这个信号,然后他们还是一直处于未决状态.
特别提醒:如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待
处理信号的阻塞时,待处理信号就会立刻被处理。
二 接口
2.1 pthread_sigmask
线程可调用pthread_sigmask()设置本线程的信号屏蔽字,以屏蔽该线程对某些信号的响应处理。
#include <signal.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); |
该函数检查和(或)更改本线程的信号屏蔽字。若参数oset为非空指针,则该指针返回调用前本线程的信号屏蔽字。若参数set为非空指针,则参数how指示如何修改当前信号屏蔽字;否则不改变本线程信号屏蔽字,并忽略how值。该函数执行成功时返回0,否则返回错误编号(errno)。
下表给出参数how可选用的值。其中,SIG_ BLOCK为“或”操作,而SIG_SETMASK为赋值操作。
参数how | 描述 |
SIG_BLOCK | 将set中包含的信号加入本线程的当前信号屏蔽字 |
SIG_UNBLOCK | 从本线程的当前信号屏蔽字中移除set中包含的信号(哪怕该信号并未被阻塞) |
SIG_SETMASK | 将set指向的信号集设置为本线程的信号屏蔽字 |
主线程调用pthread_sigmask()设置信号屏蔽字后,其创建的新线程将继承主线程的信号屏蔽字。然而,新线程对信号屏蔽字的更改不会影响创建者和其他线程。
通常,被阻塞的信号将不能中断本线程的执行,除非该信号指示致命的程序错误(如SIGSEGV)。此外,不能被忽略处理的信号(SIGKILL 和SIGSTOP )无法被阻塞。
注意,pthread_sigmask()与sigprocmask()函数功能类似。两者的区别在于,pthread_sigmask()是线程库函数,用于多线程进程,且失败时返回errno;而sigprocmask()针对单线程的进程,其行为在多线程的进程中没有定义,且失败时设置errno并返回-1。
2.2 sigwait
线程可通过调用sigwait()函数等待一个或多个信号发生。
#include <signal.h> int sigwait(const sigset_t *restrict sigset, int *restrict signop); |
参数sigset指定线程等待的信号集,signop指向的整数表明接收到的信号值。该函数将调用线程挂起,直到信号集中的任何一个信号被递送。该函数接收递送的信号后,将其从未决队列中移除(以防返回时信号被signal/sigaction安装的处理函数捕获),然后唤醒线程并返回。该函数执行成功时返回0,并将接收到的信号值存入signop所指向的内存空间;失败时返回错误编号(errno)。失败原因通常为EINVAL(指定信号无效或不支持),但并不返回EINTR错误。
给定线程的未决信号集是整个进程未决信号集与该线程未决信号集的并集。若等待信号集中某个信号在sigwait()调用时处于未决状态,则该函数将无阻塞地返回。若同时有多个等待中的信号处于未决状态,则对这些信号的选择规则和顺序未定义。在返回之前,sigwait()将从进程中原子性地移除所选定的未决信号。
若已阻塞等待信号集中的信号,则sigwait()会自动解除信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait()将恢复线程的信号屏蔽字。因此,sigwait()并不改变信号的阻塞状态。可见,sigwait()的这种“解阻-等待-阻塞”特性,与条件变量非常相似。
为避免错误发生,调用sigwait()前必须阻塞那些它正在等待的信号。在单线程环境中,调用程序首先调用sigprocmask()阻塞等待信号集中的信号,以防这些信号在连续的sigwait()调用之间进入未决状态,从而触发默认动作或信号处理函数。在多线程程序中,所有线程(包括调用线程)都必须阻塞等待信号集中的信号,否则信号可能被递送到调用线程之外的其他线程。建议在创建线程前调用pthread_sigmask()阻塞这些信号(新线程继承信号屏蔽字),然后绝不显式解除阻塞(sigwait会自动解除信号集的阻塞状态)。
若多个线程调用sigwait()等待同一信号,只有一个(但不确定哪个)线程可从sigwait()中返回。若信号被捕获(通过sigaction安装信号处理函数),且线程正在sigwait()调用中等待同一信号,则由系统实现来决定以何种方式递送信号。操作系统实现可让sigwait返回(通常优先级较高),也可激活信号处理程序,但不可能出现两者皆可的情况。
注意,sigwait()与sigwaitinfo()函数功能类似。两者的区别在于,sigwait()成功时返回0并传回信号值,且失败时返回errno;而sigwaitinfo()成功时返回信号值并传回siginfo_t结构(信息更多),且失败时设置errno并返回-1。此外, 当产生等待信号集以外的信号时,该信号的处理函数可中断sigwaitinfo(),此时errno被设置为EINTR。
对SIGKILL (杀死进程)和 SIGSTOP(暂停进程)信号的等待将被系统忽略。
使用sigwait()可简化多线程环境中的信号处理,允许在指定线程中以同步方式等待并处理异步产生的信号。为了防止信号中断线程,可将信号加到每个线程的信号屏蔽字中,然后安排专用线程作信号处理。该专用线程可进行任何函数调用,而不必考虑函数的可重入性和异步信号安全性,因为这些函数调用来自正常的线程环境,能够知道在何处被中断并继续执行。这样,信号到来时就不会打断其他线程的工作。
这种采用专用线程同步处理信号的模型如下图所示:
其设计步骤如下:
1) 主线程设置信号屏蔽字,阻塞希望同步处理的信号;
2) 主线程创建一个信号处理线程,该线程将希望同步处理的信号集作为 sigwait()的参数;
3) 主线程创建若干工作线程。
主线程的信号屏蔽字会被其创建的新线程继承,故工作线程将不会收到信号。
注意,因程序逻辑需要而产生的信号(如SIGUSR1/ SIGUSR2和实时信号),被处理后程序继续正常运行,可考虑使用sigwait同步模型规避信号处理函数执行上下文不确定性带来的潜在风险。而对于硬件致命错误等导致程序运行终止的信号(如SIGSEGV),必须按照传统的异步方式使用 signal()或sigaction()注册信号处理函数进行非阻塞处理,以提高响应的实时性。在应用程序中,可根据所处理信号的不同而同时使用这两种信号处理模型。
因为sigwait()以阻塞方式同步处理信号,为避免信号处理滞后或非实时信号丢失的情况,处理每个信号的代码应尽量简洁快速,避免调用会产生阻塞的库函数。
2.3 pthread_kill
应用程序可调用pthread_kill(),将信号发送给同一进程内指定的线程(包括自己)。
#include <signal.h> int pthread_kill(pthread_t thread, int signo); |
该函数将signo信号异步发送至调用者所在进程内的thread线程。该函数执行成功时返回0,否则返回错误编号(errno),且不发送信号。失败原因包括ESRCH(指定线程不存在)和EINVAL(指定信号无效或不支持),但绝不返回EINTR错误。
若signo信号取值为0(空信号),则pthread_kill()仍执行错误检查并返回ESRCH,但不发送信号。因此,可利用这种特性来判断指定线程是否存在。类似地,kill(pid, 0)可用来判断指定进程是否存在(返回-1并设置errno为ESRCH)。例如:
1 int ThreadKill(pthread_t tThrdId, int dwSigNo) 2 { 3 int dwRet = pthread_kill(tThrdId, dwSigNo); 4 if(dwRet == ESRCH) 5 printf("Thread %x is non-existent(Never Created or Already Quit)!\n", 6 (unsigned int)tThrdId); 7 else if(dwRet == EINVAL) 8 printf("Signal %d is invalid!\n", dwSigNo); 9 else 10 printf("Thread %x is alive!\n", (unsigned int)tThrdId); 11 12 return dwRet; 13 }
但应注意,系统在经过一段时间后会重新使用进程号,故当前拥有指定进程号的进程可能并非期望的进程。此外,进程存在性的测试并非原子操作。kill()向调用者返回测试结果时,被测试进程可能已终止。
线程号仅在进程内可用且唯一,使用另一进程内的线程号时其行为未定义。当对线程调用pthread_join()成功或已分离线程终止后,该线程生命周期结束,其线程号不再有效(可能已被新线程重用)。程序试图使用该无效线程号时,其行为未定义。标准并未限制具体实现中如何定义pthread_t类型,而该类型可能被定义为指针,当其指向的内存已被释放时,对线程号的访问将导致程序崩溃。因此,通过pthread_kill()测试已分离的线程时,也存在与kill()相似的局限性。仅当未分离线程退出但不被回收(join)时,才能期望pthread_kill()必然返回ESRCH错误。同理,通过pthread_cancel()取消线程时也不安全。
若要避免无效线程号的问题,线程退出时就不应直接调用pthread_kill(),而应按照如下步骤:
1) 为每个线程维护一个Running标志和相应的互斥量;
2) 创建线程时,在新线程启动例程ThrdFunc内设置Running标志为真;
3) 从新线程启动例程ThrdFunc返回(return)、退出(pthread_exit)前,或在响应取消请求时的清理函数内,获取互斥量并设置Running标志为假,再释放互斥量并继续;
4) 其他线程先获取目标线程的互斥量,若Running标志为真则调用pthread_kill(),然后释放互斥量。
信号发送成功后,信号处理函数会在指定线程的上下文中执行。若该线程未注册信号处理函数,则该信号的默认处理动作将影响整个进程。当信号默认动作是终止进程时,将信号发送给某个线程仍然会杀掉整个进程。因此,信号值非0时必须实现线程的信号处理函数,否则调用pthread_kill()将毫无意义。
三 示例
本节将通过一组基于NPTL线程库的代码示例,展示多线程环境中信号处理的若干细节。
首先定义两个信号处理函数:
1 static void SigHandler(int dwSigNo) 2 { 3 printf("++Thread %x Received Signal %2d(%s)!\n", 4 (unsigned int)pthread_self(), dwSigNo, strsignal(dwSigNo)); 5 } 6 static void sighandler(int dwSigNo) 7 { //非异步信号安全,仅为示例 8 printf("--Thread %x Received Signal %2d(%s)!\n", 9 (unsigned int)pthread_self(), dwSigNo, strsignal(dwSigNo)); 10 }
其中,SigHandler()用于同步处理,sighandler()则用于同步处理。
3.1 示例1
本示例对比单线程中,sigwait()和sigwaitinfo()函数的可中断性。
1 int main(void) 2 { 3 sigset_t tBlockSigs; 4 sigemptyset(&tBlockSigs); 5 sigaddset(&tBlockSigs, SIGINT); 6 sigprocmask(SIG_BLOCK, &tBlockSigs, NULL); 7 8 signal(SIGQUIT, sighandler); 9 10 int dwRet; 11 #ifdef USE_SIGWAIT 12 int dwSigNo; 13 dwRet = sigwait(&tBlockSigs, &dwSigNo); 14 printf("sigwait returns %d(%s), signo = %d\n", dwRet, strerror(errno), dwSigNo); 15 #else 16 siginfo_t tSigInfo; 17 dwRet = sigwaitinfo(&tBlockSigs, &tSigInfo); 18 printf("sigwaitinfo returns %d(%s), signo = %d\n", dwRet, strerror(errno), tSigInfo.si_signo); 19 #endif 20 21 return 0; 22 }
编译链接(加-pthread选项)后,执行结果如下:
1 //定义USE_SIGWAIT时 2 --Thread b7f316c0 Received Signal 3(Quit)! //Ctrl+\ 3 sigwait returns 0(Success), signo = 2 //Ctrl+C 4 //未定义USE_SIGWAIT时 5 --Thread b7fb66c0 Received Signal 3(Quit)! //Ctrl+\ 6 sigwaitinfo returns -1(Interrupted system call), signo = 0
对比可见,sigwaitinfo()可被等待信号集以外的信号中断,而sigwait()不会被中断。
3.2 示例2
本示例测试多线程中,sigwait()和sigwaitinfo()函数对信号的同步等待。
1 void *SigMgrThread(void *pvArg) 2 { 3 pthread_detach(pthread_self()); 4 5 //捕获SIGQUIT信号,以免程序收到该信号后退出 6 signal(SIGQUIT, sighandler); 7 8 //使用创建线程时的pvArg传递信号屏蔽字 9 int dwRet; 10 while(1) 11 { 12 #ifdef USE_SIGWAIT 13 int dwSigNo; 14 dwRet = sigwait((sigset_t*)pvArg, &dwSigNo); 15 if(dwRet == 0) 16 SigHandler(dwSigNo); 17 else 18 printf("sigwait() failed, errno: %d(%s)!\n", dwRet, strerror(dwRet)); 19 #else 20 siginfo_t tSigInfo; 21 dwRet = sigwaitinfo((sigset_t*)pvArg, &tSigInfo); 22 if(dwRet != -1) //dwRet与tSigInfo.si_signo值相同 23 SigHandler(tSigInfo.si_signo); 24 else 25 { 26 if(errno == EINTR) //被其他信号中断 27 printf("sigwaitinfo() was interrupted by a signal handler!\n"); 28 else 29 printf("sigwaitinfo() failed, errno: %d(%s)!\n", errno, strerror(errno)); 30 } 31 } 32 #endif 33 } 34 35 void *WorkerThread(void *pvArg) 36 { 37 pthread_t tThrdId = pthread_self(); 38 pthread_detach(tThrdId); 39 40 printf("Thread %x starts to work!\n", (unsigned int)tThrdId); 41 //working... 42 int dwVal = 1; 43 while(1) 44 dwVal += 5; 45 } 46 47 int main(void) 48 { 49 printf("Main thread %x is running!\n", (unsigned int)pthread_self()); 50 51 //屏蔽SIGUSR1等信号,新创建的线程将继承该屏蔽字 52 sigset_t tBlockSigs; 53 sigemptyset(&tBlockSigs); 54 sigaddset(&tBlockSigs, SIGRTMIN); 55 sigaddset(&tBlockSigs, SIGRTMIN+2); 56 sigaddset(&tBlockSigs, SIGRTMAX); 57 sigaddset(&tBlockSigs, SIGUSR1); 58 sigaddset(&tBlockSigs, SIGUSR2); 59 sigaddset(&tBlockSigs, SIGINT); 60 61 sigaddset(&tBlockSigs, SIGSEGV); //试图阻塞SIGSEGV信号 62 63 //设置线程信号屏蔽字 64 pthread_sigmask(SIG_BLOCK, &tBlockSigs, NULL); 65 66 signal(SIGINT, sighandler); //试图捕捉SIGINT信号 67 68 //创建一个管理线程,该线程负责信号的同步处理 69 pthread_t tMgrThrdId; 70 pthread_create(&tMgrThrdId, NULL, SigMgrThread, &tBlockSigs); 71 printf("Create a signal manager thread %x!\n", (unsigned int)tMgrThrdId); 72 //创建另一个管理线程,该线程试图与tMgrThrdId对应的管理线程竞争信号 73 pthread_t tMgrThrdId2; 74 pthread_create(&tMgrThrdId2, NULL, SigMgrThread, &tBlockSigs); 75 printf("Create another signal manager thread %x!\n", (unsigned int)tMgrThrdId2); 76 77 //创建一个工作线程,该线程继承主线程(创建者)的信号屏蔽字 78 pthread_t WkrThrdId; 79 pthread_create(&WkrThrdId, NULL, WorkerThread, NULL); 80 printf("Create a worker thread %x!\n", (unsigned int)WkrThrdId); 81 82 pid_t tPid = getpid(); 83 //向进程自身发送信号,这些信号将由tMgrThrdId线程统一处理 84 //信号发送时若tMgrThrdId尚未启动,则这些信号将一直阻塞 85 printf("Send signals...\n"); 86 kill(tPid, SIGRTMAX); 87 kill(tPid, SIGRTMAX); 88 kill(tPid, SIGRTMIN+2); 89 kill(tPid, SIGRTMIN); 90 kill(tPid, SIGRTMIN+2); 91 kill(tPid, SIGRTMIN); 92 kill(tPid, SIGUSR2); 93 kill(tPid, SIGUSR2); 94 kill(tPid, SIGUSR1); 95 kill(tPid, SIGUSR1); 96 97 int dwRet = sleep(1000); 98 printf("%d seconds left to sleep!\n", dwRet); 99 100 ThreadKill(WkrThrdId, 0); //不建议向已经分离的线程发送信号 101 102 sleep(1000); 103 int *p=NULL; *p=0; //触发段错误(SIGSEGV) 104 105 return 0; 106 }
注意,线程创建和启动之间存在时间窗口。因此创建线程时通过pvArg参数传递的某块内存空间值,在线程启动例程中读取该指针所指向的内存时,该内存值可能已被主线程或其他新线程修改。为安全起见,可为每个需要传值的线程分配堆内存,创建时传递该内存地址(线程私有),而在新线程内部释放该内存。
本节示例中,主线程仅向SigMgrThread线程传递信号屏蔽字,且主线程结束时进程退出。因此,尽管SigMgrThread线程已分离,但仍可直接使用创建线程时pvArg传递的信号屏蔽字。否则应使用全局屏蔽字变量,或在本函数内再次设置屏蔽字自动变量
编译链接后,执行结果如下(无论是否定义USE_SIGWAIT):
1 Main thread b7fcd6c0 is running! 2 Create a signal manager thread b7fccb90! 3 Create another signal manager thread b75cbb90! 4 Create a worker thread b6bcab90! 5 Send signals... 6 ++Thread b7fccb90 Received Signal 10(User defined signal 1)! 7 ++Thread b7fccb90 Received Signal 12(User defined signal 2)! 8 ++Thread b7fccb90 Received Signal 34(Real-time signal 0)! 9 ++Thread b7fccb90 Received Signal 34(Real-time signal 0)! 10 ++Thread b7fccb90 Received Signal 36(Real-time signal 2)! 11 ++Thread b7fccb90 Received Signal 36(Real-time signal 2)! 12 ++Thread b7fccb90 Received Signal 64(Real-time signal 30)! 13 ++Thread b7fccb90 Received Signal 64(Real-time signal 30)! 14 Thread b6bcab90 starts to work! 15 --Thread b7fcd6c0 Received Signal 3(Quit)! //Ctrl+\ 16 997 seconds left to sleep! 17 Thread b6bcab90 is alive! 18 ++Thread b7fccb90 Received Signal 2(Interrupt)! //Ctrl+C 19 ++Thread b7fccb90 Received Signal 2(Interrupt)! //Ctrl+C 20 --Thread b7fcd6c0 Received Signal 3(Quit)! //Ctrl+\ 21 Segmentation fault
以下按行解释和分析上述执行结果:
【6~13行】相同的非实时信号(编号小于SIGRTMIN)不会在信号队列中排队,只被递送一次;相同的实时信号(编号范围为SIGRTMIN~SIGRTMAX)则会在信号队列中排队,并按照顺序全部递送。若信号队列中有多个非实时和实时信号排队,则先递送编号较小的信号,如SIGUSR1(10)先于SIGUSR2(12),SIGRTMIN(34)先于SIGRTMAX(64)。但实际上,仅规定多个未决的实时信号中,优先递送编号最小者。而实时信号和非实时信号之间,或多个非实时信号之间,递送顺序未定义。
注意,SIGRTMIN/SIGRTMAX在不同的类Unix系统中可能取值不同。NPTL线程库的内部实现使用两个实时信号,而LinuxThreads线程库则使用三个实时信号。系统会根据线程库适当调整SIGRTMIN的取值,故应使用SIGRTMIN+N/SIGRTMAX-N(N为常量表达式)来指代实时信号。用户空间不可将SIGRTMIN/SIGRTMAX视为常量,若用于switch…case语句会导致编译错误。
【6~13行】sigwait()函数是线程安全(thread-safe)的。但当tMgrThrdId和tMgrThrdId2同时等待信号时,只有先创建的tMgrThrdId(SigMgrThread)线程等到信号。因此,不要使用多个线程等待同一信号。
【14行】调用pthread_create()返回后,新创建的线程可能还未启动;反之,该函数返回前新创建线程可能已经启动。
【15行】SIGQUIT信号被主线程捕获,因此不会中断SigMgrThread中的sigwaitinfo()调用。虽然SIGQUIT安装(signal语句)在SigMgrThread内,由于主线程共享该处理行为,SIGQUIT信号仍将被主线程捕获。
【16行】sleep()函数使调用进程被挂起。当调用进程捕获某个信号时,sleep()提前返回,其返回值为未睡够时间(所要求的时间减去实际休眠时间)。注意,sigwait()等到的信号并不会导致sleep()提前返回。因此,示例中发送SIGQUIT信号可使sleep()提前返回,而SIGINT信号不行。
在线程中尽量避免使用sleep()或usleep(),而应使用nanosleep()。前者可能基于SIGALARM信号实现(易受干扰),后者则非常安全。此外,usleep()在POSIX 2008中被废弃。
【17行】WorkerThread线程启动后调用pthread_detach()进入分离状态,主线程将无法得知其何时终止。示例中WorkerThread线程一直运行,故可通过ThreadKill()检查其是否存在。但需注意,这种方法并不安全。
【18行】已注册信号处理捕获SIGINT信号,同时又调用sigwait()等待该信号。最终后者等到该信号,可见sigwait()优先级更高。
【19行】sigwait()调用从未决队列中删除该信号,但并不改变信号屏蔽字。当sigwait()函数返回时,它所等待的信号仍旧被阻塞。因此,再次发送SIGINT信号时,仍被sigwait()函数等到。
【21行】通过pthread_sigmask()阻塞SIGSEGV信号后,sigwait()并未等到该信号。系统输出"Segmentation fault"错误后,程序直接退出。因此,不要试图阻塞或等待SIGSEGV等硬件致命错误。若按照传统异步方式使用 signal()/sigaction()注册信号处理函数进行处理,则需要跳过引发异常的指令(longjmp)或直接退出进程(exit)。注意,SIGSEGV信号发送至引起该事件的线程中。例如,若在主线程内解除对该信号的阻塞并安装处理函数sighandler(),则当SigMgrThread线程内发生段错误时,执行结果将显示该线程捕获SIGSEGV信号。
本示例剔除用于测试的干扰代码后,即为“主线程-信号处理线程-工作线程”的标准结构。
3.3 示例3
本示例结合信号的同步处理与条件变量,以通过信号安全地唤醒线程。为简化实现,未作错误处理。
1 int gWorkFlag = 0; //设置退出标志为假 2 sigset_t gBlkSigs; //信号屏蔽字(等待信号集) 3 4 pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; 5 pthread_cond_t gCond = PTHREAD_COND_INITIALIZER; 6 7 void *SigThread(void *pvArg) 8 { 9 pthread_detach(pthread_self()); 10 11 int dwSigNo; 12 sigwait(&gBlkSigs, &dwSigNo); 13 if(dwSigNo != SIGUSR1) 14 { 15 printf("Unexpected signal %d!\n", dwSigNo); 16 exit(1); 17 } 18 pthread_mutex_lock(&gLock); 19 gWorkFlag = 1; //设置退出标志为真 20 pthread_mutex_unlock(&gLock); 21 pthread_cond_signal(&gCond); 22 23 return 0; 24 } 25 26 void *WkrThread(void *pvArg) 27 { 28 pthread_detach(pthread_self()); 29 printf("Worker thread starts!\n"); 30 31 pthread_mutex_lock(&gLock); 32 while(gWorkFlag == 0) 33 pthread_cond_wait(&gCond, &gLock); 34 pthread_mutex_unlock(&gLock); 35 //以下代码不含共享数据,故不需要锁定 36 printf("Worker thread starts working...\n"); 37 int dwVal = 1; 38 while(1) 39 dwVal += 5; 40 } 41 42 int main(void) 43 { 44 sigemptyset(&gBlkSigs); 45 sigaddset(&gBlkSigs, SIGUSR1); 46 pthread_sigmask(SIG_BLOCK, &gBlkSigs, NULL); 47 48 pthread_t tSigThrdId, tWkrThrdId; 49 pthread_create(&tSigThrdId, NULL, SigThread, NULL); 50 pthread_create(&tWkrThrdId, NULL, WkrThread, NULL); 51 52 while(1); 53 exit(0); 54 }
本示例中,SigThread专用线程等待SIGUSR1信号。线程接收到该信号后,在互斥量的保护下修改全局标志gWorkFlag,然后调用pthread_cond_signal()唤醒WkrThread线程。WkrThread线程使用相同的互斥量检查全局标志的值,并原子地释放互斥量,等待条件发生。当条件满足时,该线程进入工作状态。
编译链接后,执行结果如下:
1 [wangxiaoyuan_@localhost~ ]$ ./Sigwait & 2 [1] 3940 3 [wangxiaoyuan_@localhost~ ]$ Worker thread starts! 4 kill -USR1 3940 5 Worker thread starts working... 6 [wangxiaoyuan_@localhost~ ]$ ps 7 PID TTY TIME CMD 8 3940 pts/12 00:00:31 Sigwait 9 4836 pts/12 00:00:00 ps 10 32206 pts/12 00:00:00 bash 11 [wangxiaoyuan_@localhost~ ]$ kill -KILL 3940 12 [wangxiaoyuan_@localhost~ ]$ ps 13 PID TTY TIME CMD 14 5664 pts/12 00:00:00 ps 15 32206 pts/12 00:00:00 bash 16 [1]+ Killed ./Sigwait
其中,命令kill -USR1和kill -KILL分别等同于kill -10和kill -9。
这种唤醒方式也可用于线程退出,而且比轮询方式高效。
3.4 示例4
本示例将sigwait()可用于主线程,即可正常捕捉信号,又不必考虑异步信号安全性。
1 int main(void) 2 { 3 //1. 创建工作线程(pthread_create) 4 //2. 等待终端键入的SIGINT信号(sigwait) 5 //3. 执行清理操作 6 //4. 程序退出(exit) 7 }
该例中主要等待SIGINT/SIGQUIT等终端信号,然后退出程序。
四 总结
Linux线程编程中,需谨记两点:1)信号处理由进程中所有线程共享;2)一个信号只能被一个线程处理。具体编程实践中,需注意以下事项:
- 不要在线程信号屏蔽字中阻塞、等待和捕获不可忽略的信号(不起作用),如SIGKILL和SIGSTOP。
- 不要在线程中阻塞或等待SIGFPE/SIGILL/SIGSEGV/SIGBUS等硬件致命错误,而应捕获它们。
- 在创建线程前阻塞这些信号(新线程继承信号屏蔽字),然后仅在sigwait()中隐式地解除信号集的阻塞。
- 不要在多个线程中调用sigwait()等待同一信号,应设置一个调用该函数的专用线程。
- 闹钟定时器是进程资源,且进程内所有线程共享相同的SIGALARM信号处理,故它们不可能互不干扰地使用闹钟定时器。
- 当一个线程试图唤醒另一线程时,应使用条件变量,而不是信号。