Linux进程间通信-信号 原创 Linux平台 2022年2月14日 18:18 夏至未至 2548 当前内容 6689 字,在路上,马上到,马上到 ### 目录 [TOC] ### 信号概念 信号(signal)是一种软件中断,是UNIX系统中最为古老的进程之间的通信机制。用于在一个或多个进程之间传递异步信号。它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。在Linux中,根据POSIX标准扩展以后的信号机制,不仅可以用来通知某种程序发生了什么事件,还可以给进程传递数据 ### 信号来源 信号的来源可以有很多种方式,按照产生条件的不同可以分为`硬件方式`和`软件方式`两种。 #### 硬件方式 当用户在终端上按下某键或某种组合键时,将产生信号;硬件异常产生信号。这些事件通常由硬件检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时的正在运行的程序。 #### 软件方式 用户在终端下调用kill命令向进程发送任务信号;进程调用kill或sigqueue函数发送信号;当检测到某种软件条件已经具备时发出信号,如SIGALARM信号 ### 信号举例 #### 实际场景 1. 用户输入命令,在Shell下启动一个前台进程。 2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。 3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件断。 4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。 5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。 注意,Ctrl-C产生的信号只能发给前台进程。一个命令 后面加个&可以放到后台运行,这样 Shell不必等待进程结束就可以接受新的命令,启动新的进程。Shell可以同时运行一个前台进 程和任意多个后台进程,只有前台进程才能接到Ctrl-C这种控制键产生的信号。前台进程 在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的 #### 信号列表 用kill -l命令可以察看系统定义的信号列表  每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有 定 义#define SIGINT 2。 ### 信号的产生和发送 函数可以向一个进程主动地发出一个信号,我们可以通过两个函数kill和alarm来发送一个信号 #### kill 先来看看kill函数,进程可以通过kill函数向包括它本身在内的其他进程发送一个信号,如果程序没有发送这个信号的权限,对kill函数的调用就将失败,而失败的常见原因是目标进程由另一个用户所拥有。想一想也是容易明白的,你总不能控制别人的程序吧,当然超级用户root。 kill函数的原型为: #include #include int kill(pid_t pid, int sig); 它的作用把信号sig发送给进程号为pid的进程,成功时返回0。 #### alarm 这个函数跟它的名字一样,给我们提供了一个闹钟的功能,进程可以调用alarm函数在经过预定时间后向发送一个SIGALRM信号。 alarm函数的型如下 #include unsigned int alarm(unsigned int seconds); alarm函数用来在seconds秒之后安排发送一个SIGALRM信号,如果seconds为0,将取消所有已设置的闹钟请求。alarm函数的返回值是以前设置的闹钟时间的余留秒数,如果返回失败返回-1 #### kill 举例 下面就给合fork、sleep和signal函数,用一个例子来说明kill函数的用法,代码如下: #include #include #include #include #include static int alarm_fired = 0; void ouch(int sig) { alarm_fired = 1; } int main() { pid_t pid; pid = fork(); switch(pid) { case -1: perror("fork failed\n"); exit(1); case 0: //子进程 sleep(5); //向父进程发送信号 kill(getppid(), SIGALRM); exit(0); default:; } //设置处理函数 signal(SIGALRM, ouch); while(!alarm_fired) { printf("Hello World!\n"); sleep(1); } if(alarm_fired) printf("\nI got a signal %d\n", SIGALRM); exit(0); } 在代码中我们使用fork调用复制了一个新进程,在子进程中,5秒后向父进程中发送一个SIGALRM信号,父进程中捕获这个信号,并用ouch函数来处理,变改alarm_fired的值,然后退出循环。 > 注:如果父进程在子进程的信号到来之前没有事情可做,我们可以用函数pause()来挂起父进程,直到父进程接收到信号。当进程接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。这样可以节省CPU的资源,因为可以避免使用一个循环来等待。以本例子为例,则可以把while循环改为一句pause(); 下面再以一个小小的例子来说明alarm函数和pause函数的用法,代码如下 #include #include #include #include #include static int alarm_fired = 0; void ouch(int sig) { alarm_fired = 1; } int main() { //关联信号处理函数 signal(SIGALRM, ouch); //调用alarm函数,5秒后发送信号SIGALRM alarm(5); //挂起进程 pause(); //接收到信号后,恢复正常执行 if(alarm_fired == 1) printf("Receive a signal %d\n", SIGALRM); exit(0); } 进程在5秒后接收到一个SIGALRM,进程恢复运行,打印信息并退出。 ### 信号的处理 #### signal函数 程序可用使用signal函数来处理指定的信号,主要通过忽略和恢复其默认行为来工作 signal函数的原型如下: #include void (*signal(int sig, void (*func)(int)))(int); 这是一个相当复杂的声明,仔细看可以知道signal是一个带有sig和func两个参数的函数,func是一个类型为void (*)(int)的函数指针。该函数返回一个与func相同类型的指针,指向先前指定信号处理函数的函数指针。准备捕获的信号的参数由sig给出,接收到的指定信号后要调用的函数由参数func给出。其实这个函数的使用是相当简单的,通过下面的例子就可以知道。注意信号处理函数的原型必须为void func(int),或者是下面的特殊值: SIG_IGN:忽略信号 SIG_DFL:恢复信号的默认行为 说了这么多,还是给出一个例子来说明一下吧,代码如下: #include #include #include void ouch(int sig) { printf("\nOUCH! - I got signal %d\n", sig); // 恢复中断中断信号 SIGINT 的默认行为 (void)signal(SIGINT, GIG_DEL); } int main() { // 不是终止程序的执行 (void)signal(SIGINT, ouch); // 改变终端中断信号 SIGINT 的默认行为,使之执行 ouch 函数 while(1) { printf("Hello World!\n"); sleep(1); } return 0; } 执行输出:  可以看到,第一次按下终止命令(ctrl+c)时,进程并没有被终止,面是输出OUCH! - I got signal 2,因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改 变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。 #### sigaction函数 前面我们看到了signal函数对信号的处理,但是一般情况下我们可以使用一个更加健壮的信号接口—sigaction函数。它的原型为: #include int sigaction(int sig, const struct sigaction *act, struct sigaction *oact); 该函数与signal函数一样,用于设置与信号sig关联的动作,而oact如果不是空指针的话,就用它来保存原先对该信号的动作的位置,act则用于设置指定信号的动作。 sigaction结构体定义在signal.h中,但是它至少包括以下成员: 1. void (*) (int) sa_handler;处理函数指针,相当于signal函数的func参数。 2. sigset_t sa_mask; 指定一个,信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。 3. int sa_flags;信号处理修改器; 4. sa_mask的值通常是通过使用信号集函数来设置的。 5. sa_flags,通常可以取以下的值:  现在有一个这样的问题,我们使用signal或sigaction函数来指定处理信号的函数,但是如果这个信号处理函数建立之前就接收到要处理的信号的话,进程会有怎样的反应呢?它就不会像我们想像的那样用我们设定的处理函数来处理了。sa_mask就可以解决这样的问题,sa_mask指定了一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中,设置信号屏蔽字可以防止信号在它的处理函数还未运行结束时就被接收到的情况,即使用sa_mask字段可以消除这一竞态条件。 承接上面的例子,下面给出用sigaction函数重写的例子代码,代码如下: #include #include #include void ouch(int sig) { printf("\nOUCH! - I got signal %d\n", sig); } int main() { struct sigaction act; act.sa_handler = ouch; //创建空的信号屏蔽字,即不屏蔽任何信息 sigemptyset(&act.sa_mask); //使sigaction函数重置为默认行为 act.sa_flags = SA_RESETHAND; sigaction(SIGINT, &act, 0); while(1) { printf("Hello World!\n"); sleep(1); } return 0; } 运行结果与前一个例子中的相同。注意sigaction函数在默认情况下是不被重置的,如果要想它重置,则sa_flags就要为SA_RESETHAND。 本文标题: Linux进程间通信-信号 本文作者: 夏至未至 发布时间: 2022年2月14日 18:18 最近更新: 2022年2月14日 18:25 原文链接: 许可协议: 署名-非商业性-禁止演绎 4.0 国际(CC BY-NC-ND 4.0) 请按协议转载并保留原文链接及作者 进程间通信(8) 信号(1) 上一个 谷歌浏览器离线安装标签自动刷新插件安装 下一个 Linux进程间通信-管道之命名管道 当前文章评论暂未开放,请移步至留言处留言。