一文搞定Linux信号

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 一文搞定Linux信号,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

Linux信号概述

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。

Linux信号可由如下条件产生:

对于前台进程,用户可以通过输人特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进系统异常。

比如浮点异常和非法内存段访问。

系统状态变化。比如 alarm定时器到期将引起SIGALRM信号。

运行kill命令或调用kill函数。

发送信号

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);

kill函数的pid参数以及意义

pid参数 含义
pid>0 信号发送给PID为pid的进程
pid=0 信号发送给本进程组内的所有进程
pid=-1 信号发送给楚init进程外所有进程,但发送者需要拥有对目标进程发送信号的权限
pid<-1 信号发送给组ID为-pid的进程组中所有成员

Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。但将sig设置为О可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的。一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID:

另一方面,这种检测方法不是原子操作

函数成功返回0,失败则返回errno,几种可能如下表格

kill出错情况
error 含义
EINVAL 无效的信号
EPERM 该进程没有权限发送信号给任何一个目标进程

ESRCH

目标进程或进程组不存在

信号处理方式

接受信号就要处理信号

#include<signal.h>
typedef void (*_sighandler_t)(int);

#include<bits/signum.h>
#define SIG_DFL ((_sighandler_t) 0);
#define SIG_ING ((_sighandler_t) 1);

信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。

SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件( Core)、暂停进程( Stop),以及继续进程(Cont)。

中断系统的调用

如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用sigaction函数(见后文)为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如 SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如 connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。

信号函数

signal的系统调用

#include<signal.h>

_sighandler_t signal(int sig,_sighandler_t  _handler);

sig 参数指出要捕获的信号类型。_handler参数是_sighandler_t类型的函数指针,用于指定信号sig 的处理函数。

signal函数成功时返回一个函数指针,该函数指针的类型也是_sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal 的话)。

sigaction

#include<siganl.h>
int sigaction(int sig,const struct sigaction* act,struct sigaction* oact);

          struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

该结构体中的sa_hander成员指定信号处理函数。sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask 是信号集sigset_t (_sigset_t的同义词)类型,该类型指定一组信号。关于信号集,我们将在后面介绍。sa_flags 成员用于设置程序收到信号时的行为,其可选值如表下图所示。

sa_flags
选项 含义
SA_NOCLDSTOP 设置成表示该子进程暂停时不生成SIGCHLD
SA_NOCLDWAIT 子进程结束时不产生僵尸进程
SA_SIGINFO 使用sa_sigaction作为信号处理函数(而不是默认sa_hanler),提供更多的信息
SA_ONSTACK 调用由sigaltstack函数设置的可选信号栈上的信号函数
SA_RESTART 重新调用被信号终止的系统调用
SA_NODEFER 当接收到信号并进入其信号处理函数,不屏蔽信号,默认情况下,我们期望在进程处理一个信号时不在接受到同种信号
SA_RESETHAND 信号处理函数执行完,恢复信号的默认处理方式
SA_INTERRUPT 中断系统调用
SA_NOMSAK 同SA_NODEFER
SA_ONESHOT 同SA_RESETHAND
SA_STACK 同SA_ONSTACK

 

sa_restorer成员已经过时啦

信号集

信号集函数

信号集原型:

 #include <signal.h>

       int sigemptyset(sigset_t *set);

       int sigfillset(sigset_t *set);

       int sigaddset(sigset_t *set, int signum);

       int sigdelset(sigset_t *set, int signum);

       int sigismember(const sigset_t *set, int signum);

进程信号掩码

 #include <signal.h>

  int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码(如果不为NULL的话)。如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式,

hou参数
_how参数 含义
SIG_BLOCK 新的进程信号就是当前值和_set指定信号的并集
SIG_UNBLOCK 新的进程信号就是当前值和_set指定信号的交集
SIG_SETMAsk 直接将进程信号掩码设置成_set

 

注意:如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号集

被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

#include<signal.h>
int sigpending(sigset_t* set);

set参数用于保存被挂起的信号集。显然,进程即使多次接收到同一个被挂起的信号,sigpending 函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。

关于信号和信号集,Linux还提供了很多有用的API,这里就不一一介绍了。需要提醒读者的是,要始终清楚地知道进程在每个运行时刻的信号掩码,以及如何适当地处理捕获到的信号。在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如,fork调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集

同一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它〉太久。

一种典型的解决方案是;把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环﹔信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢﹖这很简单,我们只需要使用IO复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源

网络编程相关的信号

SIGHUP

控制终端被挂起

当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。一个典型的例子是xinetd超级服务程序。

SIGPIPE

往读端被关闭的管道写入数据或socket连接写数据

默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。

这个博客,我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发

SIGURG

检测带外数据到达

附录:LINUX信号大全

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/129701.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!