信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常,Linux中有很多种不同的信号。可以在终端输入kill -l 来查看Linux支持的信号,如下图:
1、在程序中发送信号Linux信号可由如下条件产生:
1、对于前台进程,用户可以通过输入特殊的终端字符来发送,比如输入Ctrl+C通常会给正在运行的进程发送一个中断信号。
2、系统异常。比如浮点异常和非法内存段访问。
3、系统状态变化。比如alarm定时器到期时将引起SIGALRM信号。
4、在终端运行kill命令或在程序中调用kill函数,例如:如果要杀死一个进程,我们可以使用 kill -9 pid 来杀死进程,-9表示发送9号信号,也就是SIGKILL信号,pid为发送信号的目标进程的进程ID;9号信号无法被忽略以及改变默认处理方式,因此发送9号信号一定能杀死进程。
Linux中,给进程发送信号的系统调用为kill,定义如下:
#include#include int kill(pid_t pid, int sig); // 给进程ID为pid的进程发送sig信号
| pid参数 | 含义 |
|---|---|
| pid > 0 | 信号发送给进程ID为pid的进程。 |
| pid = 0 | 信号发送给本进程组内的其它进程。 |
| pid = -1 | 信号发送给除init进程外的所有进程,需要有权限 |
| pid < -1 | 信号发送给ID为-pid的进程组中的所有成员 |
kill函数在成功时返回0, 失败时返回-1,并设置errno。
2、信号的处理方式每个信号都有默认的处理方式,有的信号的默认处理方式是终止进程,有的是忽略信号以及结束进程并生成核心转储文件、暂停进程以及继续进程等。我们也可以在程序中修改信号的处理方式,注意:无法修改9号信号SIGKILL的默认处理方式,也无法忽略该信号。
信号处理函数的原型为:
#includetypedef void (*sighandler_t)(int);
除了用户自定义信号处理函数外,bits/signum.h头文件还定义了信号的两种其它处理方式:
#include#define SIG_DFL ((sighandler_t) 0) // 使用信号的默认处理方式 #define SIG_IGN ((sighandler_t) 1) // 忽略目标信号
要为一个信号设置处理函数,可以使用signal系统调用:
#includesighandler_t signal(int signum, sighandler_t handler);
signum为要捕获的信号类型,handler用于指定新的信号处理函数,也就是程序在收到signum类型信号后执行的回调函数。返回值为旧的信号处理函数。
例如下面代码:
代码中修改了2号信号SIGINT的处理函数,SIGINT信号可以由终端中按Ctrl+C来产生,因此当我们运行该程序后按Ctrl+C就会输出hello,world,为了结束该进程我们可以按Ctrl+,这个按键组合会产生SIGQUIT信号,该信号的默认处理函数是结束进程并产生转储文件。如下图所示:
#include#include #include // 信号处理回调函数 void handleSignal(int signum) { std::cout << "hello, world!" << std::endl; } int main() { signal(SIGINT, handleSignal); //修改SIGINT信号的处理方式 while(1) { sleep(1); } return 0; }
在下面的代码中修改9号信号SIGKILL的信号处理函数进行一个测试:
#include#include #include #include #include typedef void (*sighandler_t)(int); void sig_handler(int signum) { std::cout << "hello, world!" << std::endl; } int main() { sighandler_t ret = signal(SIGKILL, sig_handler); if( ret == SIG_ERR) { std::cout << "ignore SIGKILL failed, reason: " << strerror(errno) << std::endl; } while(1) { sleep(1); } return 0; }
然后编译运行该程序,如下图:
发现signal系统调用失败了,打开另一个终端查看该进程ID,并发送9号信号给此进程,发现进程还是被杀死了。说明9号信号SIGKILL的默认处理动作是无法被修改的。而且该信号也是不能被忽略的。
sigaction系统调用:
#include// act为新的处理方式,odlact为旧的处理方式 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 第二种形式的信号处理函数 // 屏蔽信号集,调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。 sigset_t sa_mask; int sa_flags; // 通常设置为0,表使用默认属性 void (*sa_restorer)(void); // 过时的元素,弃用 };
使用sigaction函数时可以指定在信号处理函数被调用的过程中要屏蔽的信号。
3、信号的机制进程或用户A给一个进程B发送信号,B在收到信号之前执行自己的代码,当B进程收到信号后,不管程序执行到什么位置,都要暂停运行,去处理信号,也就是调用信号处理函数,处理完再继续执行。与硬件中断类似——异步模式。但信号是软件层面实现的中断,早期常被成为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
内核实现信号捕捉过程:
4、信号集Linux使用数据结构sigset_t来表示一组信号,定义如下:
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
unsigned long int _val[_SIGSET_NWORDS];
} sigset_t;
// sigset_t 实际上是一个长整型数组,数组中每个元素的每个位表示一个信号,Linux提供了如下一组函数来设置、修改、删除和查询信号集:
int sigemptyset(sigset_t *set); //将信号集清0 成功:0;失败:-1
int sigfillset(sigset_t *set); //将信号集置1 成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); //将信号加入信号集 成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); //将信号清出信号集 成功:0;失败:-1
int sigismember(const sigset_t *set, int signum); //判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错:-1
进程信号掩码
我们可以利用sigprocmask来设置进程的信号掩码。该函数可以用来设置进程要屏蔽的信号或者解除屏蔽的信号,其本质,读取或修改进程的信号掩码(也叫信号屏蔽集)(PCB中)。进程的PCB中保存了该进程的信号屏蔽集,该屏蔽集用来指示哪些信号在产生时会被屏蔽。
#include8.4、SIGCHLDint sigprocmask(int how, const sigset_t *set, sigset_t *oldset); struct timeval it_value; }; struct timeval { time_t tv_sec; suseconds_t tv_usec; };
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。



