Linux信号
2022-02-13 # 学习笔记 # Linux

Linux信号

信号处理机制

信号产生方式

在linux系统中,信号由以下五种方式产生

  • 按键产生
  • 系统调用产生
  • 软件条件产生
  • 硬件异常产生
  • 命令产生

信号的两种状态

信号存在以下两种状态

  • 递达:信号产生并且送达进程。被内核直接处理
  • 未决:介于产生和递达之间的状态

信号的三种处理方式

  • 执行默认处理动作
  • 忽略
  • 捕捉(自定义)

信号列表

使用如下命令即可查看linux支持的信号列表

1
kill -l

信号集

  • 阻塞信号集(信号屏蔽字): 本质是位图,用来记录信号的屏蔽状态。一旦被屏蔽的信号, 在解除屏蔽前,一直处于未决态。
  • 未决信号集:本质是位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生, 但尚未被处理

信号处理流程

以向屏蔽了SIGINT信号的程序发送Ctrl+C信号为例,系统在接收到硬件发来的信号后,产生中断,进入内核态处理,发现为SIGINT信号,将前台应用的PCB中的未决信号集里的2号位置为1,在处理完中断后,返回用户态之前,检查未决信号集,发现2号位为1,此时检查阻塞信号集,因为已经屏蔽了SIGINT信号,因此阻塞信号集中该位也为1,因此不对此未决信号做出处理,返回用户态继续执行。

若程序没有屏蔽SIGINT信号,那么内核发现阻塞信号集2号位为0,未决信号集2号位为1,则会直接结束进程,不会继续返回用户态执行剩余代码。

注意:

linux中由信号产生的中断为一种软件中断,严格来说并不会立即进入内核处理,而是在因为其他因素产生中断进入内核后,在返回用户态之前,会对信号集进行检查,并处理信号。

也就是说,linux并不会因为信号的产生而特意进入内核处理,而是在处理其他中断出内核的时候顺便检查一下信号,如果有的话就处理一下。但在宏观上由于中断的产生很频繁,所以也可以按照信号产生后立刻就会被处理来理解。

信号集操作方式

信号集操作函数

1
2
3
4
5
6
sigset_t set; //自定义信号集。
sigemptyset(sigset_t *set); //清空信号集
sigfillset(sigset_t *set); //全部置 1
sigaddset(sigset_t *set, int signum); //将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); //将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); //判断一个信号是否在集合中。 在返回1,不在返回0
1
2
//设置信号屏蔽字和解除屏蔽:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义 set 替换 mask。
set: 自定义 set
oldset:旧有的 mask。

1
2
//查看当前的未决信号集:
int sigpending(sigset_t *set);

set: 传出的 未决信号集。

信号的发送与捕捉

信号的发送

kill函数

向指定进程发送指定的信号。

1
int kill(pid_t pid,int signum)

参数:

pid:

  • 0:发送信号给指定进程
  • = 0:发送信号给跟调用 kill 函数的那个进程处于同一进程组的进程。
  • < -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
  • = -1:发送信号给,有权限发送的所有进程。

signum:待发送的信号

返回值:成功返回0,失败返回-1

注意:kill函数虽然名字叫做kill,但其功能本质上是向指定进程发送一个信号,而并不一定是杀死该进程。只是大多数信号的默认处理动作都是结束进程,以及它也时常被用于结束进程,因此得名。

alarm函数

定时发送 SIGALRM 给当前进程。

1
unsigned int alarm(unsigned int seconds);

seconds为要设定的秒数

返回值:上次定时剩余的时间

alarm(0)为取消定时

注意:alarm函数采用的计时为自然时间,即所记的时间为内核空间时间加上用户空间时间。若想要单独记用户空间或内核空间时间,或者想要将单位精确到微秒,则需使用settimer函数。

其他几个发送信号的函数

1
2
3
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
int raise(int sig);
void abort(void);

信号的捕捉

信号捕捉函数

linux中每个信号都有自己对应的默认处理函数,而想修改收到信号后的处理方式,则需要我们自己定义信号的捕捉处理函数,这类函数的格式为:

1
void function(int)

即传入参数为int型的信号编号,而传出参数为void。

这一捕捉函数无需我们自己调用,我们只需使用signal函数或sigaction函数在linux内核中注册这一函数,即可将想捕捉的信号的处理方式指定为这一函数。当程序收到这一信号时便会自动调用我们指定的这一函数。

signal函数

该函数为ANSI定义,在不同Linux版本中可能有着不同的行为,应尽量避免使用

sigaction函数

修改信号处理动作,在内核中为指定型号注册捕捉处理函数

1
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

signum:要修改处理动作的信号

act:传入参数,新的处理方式

oldact:传出参数,旧的处理方式。如果不需要也可以传入NULL.

1
2
3
4
5
6
7
8
9
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_handler 为要指定的信号处理函数。

sa_mask 为只在信号捕捉函数调用时才起作用的信号屏蔽字

sa_flags 默认传0,表示在当前信号的捕捉函数执行时,屏蔽这一信号。

需求1:用信号集操作屏蔽ctrl+c操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void print_set(sigset_t *set)
{
for (int i = 1; i < 32; i++)
{
if (sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}

int main(int argc, char* argv)
{
sigset_t set, pendingSet, oldset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
int ret = sigprocmask(SIG_BLOCK, &set, &oldset);

while (1) {
ret = sigpending(&pendingSet);
print_set(&pendingSet);
sleep(1);
}

}

需求2:在不影响父进程执行的情况下回收子进程

**补充—SIGCHLD 的产生条件: **

  • 子进程终止时

  • 子进程接收到 SIGSTOP

  • 子进程处于停止态,接收到 SIGCONT 后唤醒时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>

void sys_error(const char* error)
{
perror(error);
exit(1);
}

void catch_sigchld(int signo)
{
pid_t pid;
int status;
printf("in\n");
//(pid = waitpid(-1,&status,WNOHANG)) > 0 不阻塞回收方式
//(pid = wait(NULL)) != -1 阻塞回收方式
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("wait process id = %d\n", pid);
}
return;
}

int main(int argc, char* argv)
{
pid_t pid;
int i;
//阻塞SIGCHLD信号
sigset_t set,oldSet;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, &oldSet);


//循环创建子进程
for (i = 0; i < 5; i++) {
if ((pid = fork()) == 0)break;
if (pid == -1)sys_error("fork error");
}

if (pid == 0){
//子进程
printf("I'm %dth child,id = %d\n", i + 1,getpid());

}
else {
//父进程
printf("I'm parent\n");

//注册SIGCHLD的捕捉函数
struct sigaction act;
act.sa_handler = catch_sigchld;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);

//取消阻塞
sigprocmask(SIG_UNBLOCK, &set, &oldSet);

//判断是否还有子进程,若子进程全部退出再关闭
int status,pid;
while ((pid = wait(NULL)) != -1) {
printf("wait process id = %d\n", pid);
}
}

return 0;
}

说明:

可能遇到的问题:

Q1:信号捕捉函数执行的过程中可能有多个子进程同时结束

A1:在捕捉函数中循环回收,直到没有结束的子进程

Q2:若在捕捉函数中采用wait(NULL),会使得父进程一直阻塞等待。

A2:在循环中使用waitpid(-1,&status,WNOHANG),判断没有已经退出的子进程之后就结束捕捉函数(如下图是两种不同的执行结果)

图一为采用wait(NULL)阻塞等待的方式,信号处理函数只被调用了两次,说明父进程一直在被阻塞。之所以是两次而不是一次是因为在阻塞回收的过程中,不断有子进程结束,父进程的未决信号集中SIGCHLD被置为了1,但该信号在信号处理函数期间处于被屏蔽的状态,当回收完所有子进程之后,屏蔽解除,再次进入处理函数处理这一信号。因此为两次。

图二采用非阻塞的调用方式,可以看到四次调用了捕捉函数,说明回收子进程的工作并没有让父进程一直等待,达成了我们的目的。