UNIX/Linux进程间通信IPC系列(五)信号
信号
信号是软件中断。它允许进程中断其他进程。
信号是异步处理事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(例如errno)来判别是否出现了一个信号,而是必须告诉内核“在此信号出现时,请执行下列操作”。
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下对用户进程而言是不可见的。
信号提供了一种机制,通知用户进程发生了这些异常(由内核发送给用户进程)。如果一个进程试图除以0,那么内核就发给它一个SIGFPE信号。
发送信号
内核通过更新目的进程上下文的某个状态,发送(递送)一个信号给目的进程。
发送信号可以有如下两个原因:
⑴内核检测到一个系统事件,比如被零除错误或子进程终止。
⑵一个进程调用了kill函数,显示地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
从键盘发送信号
在键盘上输入ctrl-c会导致一个SIGINT信号被被发送到外壳。外壳捕获该信号,然后发送SIGINT信号到这个前台进程组中的每个进程。(在默认情况下,结果是终止前台作业)
类似地,输入ctrl-z会发送一个SIGSTP信号到外壳,外壳捕获这个信号,并发送SIGSTP信号给前台进程组中的每个进程。(在默认情况下,结果是挂起前台作业)
用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号。
比如,命令 /bin/kill -9 15213 发送一个信号9(SIGKILL)给进程15213。
一个为负的PID会导致信号被发送到进程组PID中的每个进程,比如:
命令 /bin/kill -9 -15213 发送一个SIGKILL信号给进程组15213中的每个进程。
用kill函数发送信号
进程通过调用kill函数发送信号给其他进程(包括它们自己)
如果pid大于0,那么kill函数发送信号sig给进程pid。如果pid小于0,那么kill发送信号sig给进程组abs(pid)中的每个进程。
#include <stdio.h> #include <stdlib.h> #include <signal.h> int main(void) { pid_t pid ; if ((pid = fork()) == 0) { pause() ; printf("control should never reach here!\n") ; exit(0) ; } kill(pid, SIGKILL) ; exit(0) ; }
用alarm函数发送信号
进程可以通过调用alarm函数向它自己发送SIGALRM信号。
#include <unistd.h>
unsigned int alarm(unsigned int secs) ;
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。
如果secs是0则删除闹钟。
//alarm函数可以设置一个计时器。当计时器超时时,产生SIGALRM信号。 //如果不忽略或不捕捉此信号,则其默认动作时终止调用该alarm函数的进程 //每个进程只能有一个闹钟时钟 // //该程序每隔3秒打印一个alarm! // #include <unistd.h> //包含alarm() #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> static void sig_alarm(int) ; int main(void) { int i = 0 ; //绑定信号与信号处理函数 if (signal(SIGALRM, sig_alarm) == SIG_ERR) perror("can‘t catch SIGALARM") ; for (i = 0; i < 3; ++i) { alarm(3) ; pause(); } //取消闹钟 alarm(0) ; return 0 ; } //信号处理函数 static void sig_alarm(int signo) { printf("alarm!\n") ; }
接收信号
★当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号集合。如果这个集合为空(通常情况下),那么内核才将控制传递到p的下一条指令。
然而,如果集合是非空的,那么内核选择集合中的某个信号k,并且强制p接收信号k。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回进程p中的下一条指令。每个信号类型都有一个预定义的默认行为。
比如,收到SIGKILL的默认行为就是终止接收进程。接收到SIGCHLD的默认行为就是忽略这个信号。
信号的处理
1、 忽略此信号。
2、 捕捉此信号。为了做到这一点,要通知内核在某种信号发生时调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
3、 执行系统默认动作。针对大多数信号的系统默认动作是终止进程。
进程可以通过使用signal函数修改和信号相关联的默认行为。唯一例外的是SIGSTOP和SIGKILL。这两种信号既不能被忽略,也不能被捕捉。因为它们向超级用户提供了使进程终止或停止的可靠方法。
#include <signal.h>
typedef void (*sighandler_t)(int) ;
sighandler_t signal(intsignum, sighandler_t handler) ;
●如果handler是SIG_IGN,那么忽略类型为signum的信号。
●如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
●否则,handler就是用户定义的函数的地址,这个函数称为信号处理函数,只要进程接收到一个类型为signum的信号,就会调用这个程序。
//捕获用户在键盘上输入ctrl-c时外壳发送的SIGINT信号,SIGINT的默认行为是立即终止该 //进程。在此示例中,我们将默认行为修改为捕获信号,输出一条信息,然后终止该进程。 #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> void handler(int sig) { printf("Caught SIGINT\n") ; exit(0) ; } int main() { if (signal(SIGINT, handler) == SIG_ERR) perror("signal error") ; pause() ; exit(0) ; }
信号处理问题
对于只捕获一个信号并终止的程序来说,信号处理是简单直接的。然而,当一个程序要捕获多个信号时,一些细微的问题就产生了。
待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。(即同类信号中断不会被嵌套)
比如,假设一个进程捕获了一个SIGINT信号,并且当前正在运行其处理程序。如果另一个SIGINT信号传递到这个进程,那么这个SIGINT将变成待处理的(被挂起),其不会被接收,直到处理程序返回。
待处理信号不会排队等待。任意类型至多只有一个待处理信号。若多个同一类型的信号传送到进程,第二个及其之后的信号会被简单丢弃。关键思想是存在一个待处理的信号仅仅表明至少已经有一个信号到达了。(因为内核是用一个比特位来标记某种信号是否到达的)
故要注意:不可以用信号来对其他进程中发生的事件计数。
系统调用可以被中断。UNIX系统的一个特性是:如果进程在执行一个慢速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno被设置为EINTR。
慢速系统调用包括:
1、I/O操作(读写管道、终端设备、网络设备)阻塞时,例read accept等
2、pause和wait函数
3、某些ioctl操作
现在不同的UNIX系统,有的会重新启动系统调用,有的不重启。SystemV的默认工作方式是从不重启系统调用(测试fedora也是如此)
【★为了编写可移植的信号处理代码,我们必须考虑系统调用过早返回的可能性,然后当它发生时手动重启它们。】
例如:
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> void handler(int sig) { pid_t pid ; //由于信号不能排队,故一次尽可能多的回收子进程 while ((pid = waitpid(-1, NULL, 0)) > 0) printf("Handler reaped child %d \n", (int)pid) ; if (errno != ECHILD) perror("waitpid error") ; sleep(2) ; return ; } int main(int argc, char **argv) { int i, n; char buf[MAXBUF] ; pid_t pid ; if (signal(SIGCHLD, handler) == SIG_ERR) perror("signal error") ; for (i=0; i<3; i++) { pid = fork() ; if (pid == 0) { printf("Hello from child %d\n", (int)getpid()) ; sleep(1) ; exit(0) ; } } //重启被信号中断的IO系统调用 while ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0) if (errno != EINTR) perror("read error") ; while (1) ; exit(0) ; }
可移植的信号处理(sigaction函数)
不同系统之间,信号处理语义的差异是Unix信号处理的一个缺陷。为了处理这个问题,Posix标准定义了sigaction函数,它允许像Linux和Solaris这样与Posix兼容的系统上的用户,明确地指定他们想要的信号处理语义。(因为signal的语义与实现有关,所以最好使用sigaction函数代替signal函数。)
#include <signal.h>
int sigaction(intsigno, const struct sigaction* act, struct sigaction* oact) ;
参数act指向的结构体中包含了对信号signo的动作。参数oact指向对信号的上一个动作。
创建成功返回0,出错返回-1
struct sigaction
{
void (* sa_handler)(int); //信号处理函数
sigset_t sa_mask ; //信号屏蔽字
int sa_flag ; //关于信号的选项
void (*sa_sigaction)(int, siginfo_t*, void *);//一个替代的信号处理函数,当选项中选了SA_SIGINFO标志时,使用该信号处理程序。
} ;
【注意】此函数默认设置:当捕捉到此信号时,在执行其信号处理函数时,系统自动阻塞此信号,直到此信号处理函数结束为止。(防止同一信号处理函数嵌套)signal函数也是如此。
部分信号选项标志(sa_flags)
SA_NOCLDWAIT :若signo是SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程。(关于SIGCHID信号见下面示例代码。关于僵死进程 见UNIX进程篇博文)
SA_NODEFER :当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号。
SA_RESTART :由此信号中断的系统调用会自动重启。(系统默认不重启)
显示地阻塞和取消阻塞信号
当引发信号的事件发生时,向进程发送一个信号。在产生了信号时,内核通常在进程表中设置一个某种形式的标志。我们说内核向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号是未决的(pending)。
进程可以选用信号递送阻塞。如果向进程发送了一个其阻塞的信号,且进程对该信号的动作是捕捉信号或系统默认,则信号暂不递送到此进程(此信号保持为未决状态,即内核已收到此信号,但内核还没有把此信号递送给目的进程)。
直到该进程 ①对此信号解除了阻塞,或②将对此信号的动作更改为忽略。
进程可调用sigpending函数来判定哪些信号是设置为阻塞并处于未决状态的。
若在进程解除某个信号阻塞之前,这种信号发生了多次,UNIX并不对信号排队,内核只递送这种信号一次。
每个进程都有一个信号屏蔽字。它记录了当前要阻塞递送到该进程的信号集,用一个新数据类型sigset_t表示。进程可以调用sigprocmask函数来检测和更改当前信号屏蔽字。
注意:sigprocmask函数改变了信号屏蔽字,可能会使某些信号解阻塞(在用SIG_SETMASK重置时),故注意在调用sigprocmask之处可能会触发信号的投递。
#include <signal.h>
int sigemptyset(sigset_t *set) ; //初始化set为空集
int sigfillset(sigset_t *set) ; //将每个信号添加到set中
int sigaddset(sigset_t *set, int signum) ; //添加signum到set
int sigdelset(sigset_t *set, int signum) ; //从set中删除signum
int sigismember(sigset_t *set, int signum) ; //若signum是set的成员则为1,若不是则为0
函数sigprocmask可以检测或更改进程的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const sigset_t * set, sigset_t * oset) ;
参数how指示如何修改当前的信号屏蔽字。
SIG_BLOCK :“或”操作。把set中的信号添加到进程信号屏蔽字中。
SIG_UNBLOCK :set包含了我们希望解除阻塞的信号。(一般不用此法解除阻塞)
SIG_SETMASK :赋值操作。把set信号集复制到进程信号屏蔽字中。(可用于解阻塞)
----------注意:sigprocmask仅为单线程的进程定义的,至于多线程进程中的信号屏蔽,见UNIX线程博文----------
★避免并发错误
//例:一个典型的Unix外壳结构。当父进程创建一个新的子进程时,它就把这个子进程添 //加到作业列表中。当父进程在SIGCHLD处理程序中回收一个终止的(僵死)子进程时,他就 //从作业列表中删除这个子进程。 //这是一个称为竞争的经典同步错误的示例。 void handler(int sig) { pid_t pid ; while ((pid = waitpid(-1, NULL, 0)) > 0) deletejob(pid) ; if (errno != ECHILD) perror("waitpid ") ; } int main(int argc, char **argv) { int pid ; Signal(SIGCHLD, handler) ; //自己定义的sigaction的包装函数 initjobs() ; //初始化作业列表 while(1) { if ((pid = fork()) == 0) execve("/bin/date", argv, NULL) ; addjob(pid) ; } exit(0) ; }
//同是父进程中的addjob和处理程序中调用deletejob之间存在竞争。若子进程启动之后马上结束触发信号,调用了信号处理程序中的deletejob,此时addjob还未被调用,那么结果就是错误的。
(由于在调用fork之后,子进程就可能马上结束进而触发信号,导致先调用deletjob。
我们在调用fork之前,阻塞SIGCHLD信号,然后在调用了addjob之后就取消这些信号,这样我们就保证了addjob先被调用,deletejob后调用。)
void handler(int sig) { pid_t pid ; while ((pid = waitpid(-1, NULL, 0)) > 0) deletejob(pid) ; if (errno != ECHILD) perror("waitpid error") ; } int main(int argc, char **argv) { int pid ; sigset_t mask ; Signal(SIGCHLD, handler) ; initjobs() ; while(1) { sigemptyset(&mask) ; sigaddset(&mask, SIGCHLD) ; sigprocmask(SIG_BLOCK, &mask, NULL) ; //阻塞SIGCHLD信号,这样就算子进程先运行结束,其触发的信号也不会被投递到父进程。 if ((pid = fork()) == 0) { sigprocmask(SIG_UNBLOCK, &mask, NULL) ; execve("/bin/date", argv, NULL) ; } addjob(pid) ; sigprocmask(SIG_UNBLOCK, &mask, NULL) ; //解除SIGCHLD信号的阻塞,让此信号投递进来 } exit(0) ; }
//注意:子进程继承了它们父进程的被阻塞集合,所以我们必须在调用execve之前,小心地解除子进程中阻塞的SIGCHLD信号。
sigsuspend函数
#include <signal.h>
int sigsuspend(const sigset_t * sigmask) ;
将进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。
如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并将该进程的信号屏蔽字设置为调用sigsuspend之前的值。
(sigsuspend使恢复信号屏蔽字,然后进程休眠,这两个动作成一个原子操作。)
【sigsuspend作用是:使进程挂起,以等待特定的信号(除了sigmask之外的信号)。】
那为何不用sigprocmask与pause实现呢?
因为若在sigprocmask与pause之间发生我们要等待的信号,信号在pause之前投递,但进程继续调用pause阻塞,这就好像信号丢失一样。故我们要把sigprocmask与pause做成原子操作。
【技巧】
若用sigsuspend使进程挂起只为等待一个信号,则可设置一全局变量。在信号处理函数中改变它的值,让sigsuspend等待全局变量值的改变。
while (sigflag == 0)
sigsuspend(&zeromask) ;
sigflag = 0 ;
(注意:若是终端交互的信号,要把上述代码放在基于此信号的临界区中)
---可重入函数
可重入函数,即在信号处理函数中可安全调用的函数。(即当信号处理函数在执行时,再次产生信号,重入此信号处理程序,也不会产生问题)
例如:在信号处理函数中正在执行malloc,在进程的堆中分配另外的存储空间,而此时由于捕捉到信号而再次执行信号处理函数,它又调用malloc。这样可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表。
以下几类函数是不可重入的:
1、 使用静态数据结构的
2、 调用malloc或free的
3、 标准I/O函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。(系统低级I/O read write是可重入的)
还要注意:
即使信号处理函数调用的是可重入的函数,但是这些函数有可能会修改errno全局变量的值。故:作为一个通用的规则,应当在信号处理函数开始处保存errno的值,在其结尾处恢复errno的值。
信号的用途:
与终端交互
保护临界区使其不被特定信号中断(与线程间的锁不同)
sigprocmask(SIG_BLOCK, &newmask,&oldmask) ;
/*临界区*/
sigprocmask(SIG_SETMASK, & oldmask,NULL) ;
父子进程间的同步
例子:
以下代码在linux发行版fedora系统测试通过
【向进程发送信号示例】
//使用说明: //./a.out & 在后台启动进程,会显示进程ID //kill -USER1 进程ID 用kill命令向该进程发送信号SIGUSR1 //kill -USER2 进程ID 用kill命令向该进程发送信号SIGUSR2 //kill 进程ID 用kill命令向该进程发送信号SIGTERM 使进程终止 // #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> static void sig_usr(int) ; int main(void) { //绑定信号与信号处理函数 if (signal(SIGUSR1, sig_usr) == SIG_ERR) perror("can‘t catch SIGUSR1") ; if (signal(SIGUSR2, sig_usr) == SIG_ERR) perror("can‘t catch SIGUSR2") ; //使进程阻塞,等待信号 for (; ;) pause() ; exit(0) ; } //信号处理函数 static void sig_usr(int signo) { if (signo == SIGUSR1) printf("received SIGUSR1\n") ; else if (signo == SIGUSR2) printf("received SIGUSR2\n") ; else printf("received signal %d\n", signo) ; }
【子进程终止信号SIGCLD】
//SIGCLD是SystemV定义的子进程终止信号,POSIX定义的是SIGCHLD //子进程状态改变后产生此信号,父进程需要调用一个wait函数以确定发生了什么(获取子进程的终止状态,防止其变为僵尸进程) // #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> #include <sys/wait.h> static void sig_cld(int) ; int main(void) { pid_t pid ; //绑定信号与信号处理函数 if (signal(SIGCLD, sig_cld) == SIG_ERR) perror("can‘t catch SIGCLD") ; if ((pid = fork()) < 0) perror("fork error") ; else if (pid == 0) { sleep(2) ; _Exit(0) ; } pause() ; exit(0) ; } //信号处理函数 static void sig_cld(int signo) { pid_t pid ; int status ; printf("received SIGCLD\n") ; if ((pid = wait(&status)) < 0) perror("wait error\n") ; printf("pid = %d\n", pid) ; }
【父子进程同步】
//<源程序synchronous.c> //信号可用来实现父子进程间的同步 // // #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> //把变量sigflag设置为原子类型(即对其写操作时 不会被中断) //sigflag 就相当于是一个指示灯,由于子进程会复制父进程的进程空间, //故这一个指示灯会由于复制 而分别存在父子进程中。指示来自对方的信号 static volatile sig_atomic_t sigflag ; static sigset_t newmask, oldmask, zeromask ; //我们用到的两个用户信号的信号处理函数 static void sig_usr(int signo) { sigflag = 1 ; } void TELL_WAIT(void) { if (signal(SIGUSR1, sig_usr) == SIG_ERR) perror("signal(SIGUSR1) error\n") ; if (signal(SIGUSR2, sig_usr) == SIG_ERR) perror("signal(SIGUSR2) error\n") ; //把信号SIGUSR1和SIGUSR2 设置为阻塞 sigemptyset(&zeromask) ; sigemptyset(&newmask) ; sigaddset(&newmask, SIGUSR1) ; sigaddset(&newmask, SIGUSR2) ; if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) perror("SIG_BLOCK error\n") ; } //通知父进程 void TELL_PARENT(pid_t pid) { kill(pid, SIGUSR2) ; } //等待父进程 void WAIT_PARENT(void) { while (sigflag == 0) sigsuspend(&zeromask) ; sigflag = 0 ; //把信号SIGUSR1和SIGUSR2解阻塞 if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) perror("SIG_SETMASK error\n") ; } //通知子进程 void TELL_CHILD(pid_t pid) { kill(pid, SIGUSR1) ; } //等待子进程 void WAIT_CHILD(void) { while (sigflag == 0) sigsuspend(&zeromask) ; sigflag = 0 ; //把信号SIGUSR1和SIGUSR2解阻塞 if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) perror("SIG_SETMASK error\n") ; } -------------------- //<源程序main.c> 测试上面的父子进程同步机制 #include <stdio.h> #include <stdlib.h> #include <errno.h> int main(void) { int i = 0 ; int cnt = 0 ; pid_t pid ; //为同步机制初始化 TELL_WAIT() ; if ((pid = fork()) < 0) perror("fork error") ; else if (pid > 0) //父进程 { for (i = 0; i < 3; ++i) { printf("From parent : %d\n", i) ; } TELL_CHILD(pid) ; WAIT_CHILD(pid) ; for (; i < 6; ++i) { printf("From parent : %d\n", i) ; } TELL_CHILD(pid) ; return 0 ; } else //子进程 { WAIT_PARENT(getppid()) ; for (i = 0; i < 3; ++i) { printf("From child : %d\n", i) ; } TELL_PARENT(getppid()) ; WAIT_PARENT(getppid()) ; for (; i < 6; ++i) { printf("From child : %d\n", i) ; } return 0 ; } }
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。