信号
# 信号
# 一、信号的概念
- 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也 称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号 可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
- 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给 相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的 内存区域。
- 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
- 使用信号的两个主要目的是:
- 让进程知道已经发生了一个特定的事情。
- 强迫进程执行它自己代码中的信号处理程序。
- 信号的特点:
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
- 优先级比较高
- 查看系统定义的信号列表:kill –l
- 前 31 个信号为常规信号,其余为实时信号(预定义好的信号)。
- 注意,信号列表总共其实是62个
# 二、Linux 信号表
# 三、信号的5种默认处理动作
- 查看信号的详细信息:
man 7 signal
- 信号的 5 中默认处理动作
- Term 终止进程
- Ign 当前进程忽略掉这个信号
- Core 终止进程,并生成一个Core文件
- Stop 暂停当前进程
- Cont 继续执行当前被暂停的进程
- 信号的几种状态:产生、未决、递达
- 未决:信号还没到达进程, 信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间;
- 递达:信号到达进程;
- SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
# 补充:core文件
# 1、生成 core 文件
- core文件会保存我们程序异常退出的错误信息
//core.c
#include <stdio.h>
#include <string.h>
int main()
{
//创建了指针但是没有指向一个合法的内存,所以执行肯定会出错
char *buf;
strcpy(buf, "hello");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
- 第一次执行没有生成 cor文件是因为
core file size
默认为 0,所以我们需要修改它的上限,修改完后再次执行可以看到 core 被生成 - 上图使用
gcc core.c
去编译是不对的,虽然这样可以生成core ,但是我们没办法去查看,应该使用gcc core.c -g
去编译生成可执行文件 (-g 生成调试信息) - 如果修改完
core file size
后还是不生成core:
# 2、查看 core 文件
- 像上图,已经生成了 core 文件
- 然后我们是使用
gcc a.out
进入 gdb 调试 - 在 gdb 里 执行
core-file core
命令,就可以查看 保存在core文件里的错误信息
- 有点奇怪的是,我自己执行后没有上图🟥里的信息,但是视频里老师执行命令后是有的
# 四、信号相关的函数
# 1、kill、raise、abort函数
- int kill(pid_t pid, int sig);
- int raise(int sig);
- void abort(void);
# 1.1 kill 函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 功能:给某个进程pid,发生某个信号 sig
- 参数:
- pid :
> 0 : 将信号发送给指定的进程
= 0 :将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid等于某个进程组的ID取反 (好比-12345,那我们就是给12345这个进程组里所有进程发送信号)
- sig : 需要发送的信号的编号或者是宏值(建议使用宏值,因为在不同的架构里,同一个宏的编号不一定一样)
kill(getppid(),9);
kill(getpid(),9);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.2 raise 函数
#include <signal.h>
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig:要发送的信号
-返回值:
- 成功 0
- 失败 非0
kill(getpid(),sig) // kill 实现 raise
2
3
4
5
6
7
8
9
# 1.3 abort 函数
#include <stdlib.h>
void abort(void);
- 功能:发送 SIGABRT 信号给当前的进程,杀死当前进程
kill(getpid(),SIGABRT); //kill 实现 abort
2
3
4
# 1.4 kill案例及运行结果
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// 子进程
int i = 0;
for (i = 0; i < 5; i++)
{
printf("child process\n");
sleep(1);
}
}
else if (pid > 0)
{
// 父进程
printf("parent process\n");
sleep(2);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
}
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
- 这里子进程有可能输出2次也有可能输出3次,因为父子进程间的运行顺序是不一定的
# 2、alarm 函数
# 2.1 alarm 解释
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds:倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0);
-返回值:
- 之前没有定时器,返回 0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM:默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm(100) -> 该函数是不阻塞的
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2 alarm 案例1
#include <unistd.h>
#include <stdio.h>
int main()
{
int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 0
sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3
// 因为alarm 不阻塞,所以如果我们不加while让进程阻塞,
// 那进程直接就运行到结束了,就看不到我们想看的效果
while (1)
{
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 可以看出,之前没有定时器那返回的是 0
- 之前有定时器那返回的是之前定时器剩余的时间
- 这里总共是4秒后,进程终止,因为我们休眠了2秒,后又重制了定时器为2秒(进程终止中文系统还输出了 ”闹钟“)
# 2.2 alarm 案例2
//alarm1.c
// 1秒钟电脑能输出多少数?
#include <stdio.h>
#include <unistd.h>
int main()
{
alarm(1);
int i = 0;
while (1)
{
printf("%i\n", i++);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 在终端编译执行后,会输出好多数,但是我们输出的时间感觉不止1s,是因为终端也是有缓冲区的,终端输出也需要耗费时间
- 如果我们在终端执行
./alarm1 >> a.txt
指令,那么就会明显感觉到很快就结束了,这是因为往终端上输出是耗费磁盘io的时间,而我们计算机数的时候其实是内核的系统调用的时间,如果我们重定向到文件当中的话其实就只操作了一次(这里目前我也不知道操作一次指的是操作了什么一次😥)- 在vscode里打开
a.txt
文件会发现文件有几十M,里面数字也非常多,比在终端输出多很多
- 在vscode里打开
/*
看完后面的课,突然有点懵了,感觉下面写的和老师讲的有一些冲突因为alarm函数和setitimer(ITIMER_PROF) 共享同一个定时器。即alarm的定时时间包含的是:用户+系统内核的运行时间
所以下面老师说的实际时间应该指的不是alarm的定时时间,而是我们在终端看到的输出的时间也一起算上了
实际的时间 = 内核时间(程序的系统调用)+用户时间(cpu执行普通代码)+ 消耗的时间(操作io的时间)
- 系统调用其实就是调用系统函数,就像下面的main,alarm
- 普通代码 就像下面的 int i=0;这些
- 进行文件IO操作是比较浪费时间的,还有执行代码时内核(系统调用)和用户区的切换也是会消耗时间的
定时器,与进程的状态无关(自然定时法)。
无论进程处于什么状态,alarm都会计时
*/
2
3
4
5
6
7
8
9
10
11
12
13
系统和用户态时间 (opens new window)(上面时间的解释是看视频听老师说的,自己目前还不是特别了解)
# 3、 setitimer 函数
- int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
# 3.1 setitimer 简介
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
- 功能:设置定时器(闹钟),可以替代 alarm函数,精度微妙
- 参数:
- which : 定时器以什么时间计时
ITIMER_REAL: 真实时间,时间到达,发送 SIGALARM (常用)
ITIMER_VIRTUAL: 用户时间 ,时间到达,发送 SIGVTALRM
ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间计算,时间到达,发送 SIGPROF
- new_value:设置定时器的属性
struct itimerval { // 定时器的结果体
struct timeval it_interval; // 每个阶段的时间,(隔时间)
struct timeval it_value; // 延迟多长时间执行定时器
};
过10秒后,每隔2秒定时一次,这里 10 就是 it_value , 2就是it_interval
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微妙
};
-old_value : 记录上一次的定时器的时间参数,一般不使用,我们指NULL就行
- 返回值:
成功 0,失败 -1,并设置错误号
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2 案例
//setitimer.c
// 过3秒以后,每个2秒钟定时一次
#include <sys/time.h>
#include <stdio.h>
int main()
{
struct timeval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
if (ret == -1)
{
perror("setitimer");
exit(0);
}
// 获取键盘录入,阻塞进程,用while也行,主要是阻塞进程,方便看效果
getchar();
return 0;
}
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
- 我们编译运行完上面程序,会发现三秒后,程序就被终止,终端显示
闹钟
,这是因为第一次定时到达时就会发送信号,直接把进程杀死了,而我们这里有没有信号捕捉,来捕捉信号。 - 所以就需要能捕捉信号的函数
signal()
# 五、捕捉信号函数
# 1、signal 函数介绍
#include <signal.h>
typedef void (*sighandler_t)(int);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到信号的值
// 这里如果把(*sighandler_t)的括号去掉,那么就是一个函数 ,*sighandler_t 就成了函数名,所以必须加上才是一个函数指针
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum : 要捕捉的信号
- handler:捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL :使用信号默认的行为
- 回调函数 : 这个函数是内核调用的,程序员只负责写,捕捉信号后如何去处理信号。
-(回调函数)需要程序员实现,提前准备好的,函数的类根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放函数指针的位置就可以了。
-返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号。
//SIGKILL SIGSTOP 不能被捕捉,不能被忽略。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 注意:signal函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
# 2、signal案例
//signal.c
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num)
{
printf("捕捉到了信号的编号:%d\n", num);
printf("xxxxxxx\n");
}
int main()
{
// 注册信号捕捉(写在前面)
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
signal(SIGALRM, myalarm);
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if (ret == -1)
{
perror("setitimer");
exit(0);
}
// 获取键盘录入,阻塞进程,用while也行,主要是阻塞进程,方便看效果
getchar();
return 0;
}
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
- 分别对应上面3个 signal 语句
- 忽略信号,进程被阻塞,不会被杀死信号
- 执行信号默认行为
- 现在回调函数,实现3s后输出一次,之后每隔2s输出一次
# 3、sigaction 函数介绍
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- 功能 : 检查或者改变信号的处理,信号捕捉
- 参数 :
- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act : 捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传递 NULL
- 返回值:
成功 0,失败 -1
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
//不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用 sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
//被废弃掉了
void (*sa_restorer)(void);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 4、sigaction 案例
//sigaction.c
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num)
{
printf("捕捉到了信号的编号:%d\n", num);
printf("xxxxxxx\n");
}
int main()
{
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); // 清空临时阻塞信号集
// 注册信号捕捉(写在前面)
sigaction(SIGALRM, &act, NULL);
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if (ret == -1)
{
perror("setitimer");
exit(0);
}
// 获取键盘录入,阻塞进程,用while也行,主要是阻塞进程,方便看效果
// getchar();
while (1)
{
};
return 0;
}
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
- 这里如果用 getchar() 会出现🟨中的情况,3s 后进程被杀死
- 改成 while 后就正常被阻塞,原因如下:
# 5、信号捕捉的特性
- 在信号捕捉的时候,有如下几个特性:
- 进程正常运行时,默认PCB中有一个信号屏蔽字假设为M,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,在捕捉到该信号以后,就要调用该信号捕捉函数,而该函数有可能执行很长时间,在这期间所要屏蔽的信号不由M来指定,而是用sa_mask(临时屏蔽信号集)来指定,等到调用完信号处理函数,再把信号屏蔽字恢复为M。
- 某个信号sig捕捉函数执行期间,sig信号自动被屏蔽。
- 阻塞的常规信号不支持排队,如果产生多次,只记录一次。实际上是这样的,未决信号集中使用某一位的0和1来记录信号是否被处理的,所以不管这个信号被发送了几次,未决信号集对应位也只能有一个1,后续也只能处理一次,它不会记录信号屏蔽期间总共发送了几次该信号,解除屏蔽后只会处理一次。后32个实时信号支持排队。
# 六、信号集
# 1、信号集概念
- 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为 信号集的数据结构来表示,其系统数据类型为
sigset_t
。 - 在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数 来对 PCB 中的这两个信号集进行修改。
- 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。 (已产生未处理)
- 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
- 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号, 所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
# 2、阻塞信号集和未决信号集
- 注意:
- 未决信号集是系统维护的,我们只能读,不能修改
- 修改阻塞信号集的原理:自定义的信号集只是用户区的数据,通过系统调用能够按照我们自己定义的信号集去修改内核中的信号集。
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 阻塞信号集默认不阻塞任何的信号(都为0) - 如果想要阻塞某些信号需要用户调用系统的API (将0改为1)
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
# 七、信号集相关函数
# 1、基础设置
- 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);
# 1.1 函数解释
//以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0,失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞
-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
# 1.2 代码测试
#include <signal.h>
#include <stdio.h>
int main()
{
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set里
int ret = sigismember(&set, SIGINT);
if (ret == 0)
{
printf("SIGINT 不阻塞\n");
}
else if (ret == 1)
{
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断 SIGINT 是否在信号集 set里
ret = sigismember(&set, SIGINT);
if (ret == 0)
{
printf("SIGINT 不阻塞\n");
}
else if (ret == 1)
{
printf("SIGINT 阻塞\n");
}
// 判断 SIGQUIT 是否在信号集 set里
ret = sigismember(&set, SIGQUIT);
if (ret == 0)
{
printf("SIGQUIT 不阻塞\n");
}
else if (ret == 1)
{
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断 SIGQUIT 是否在信号集 set里
ret = sigismember(&set, SIGQUIT);
if (ret == 0)
{
printf("SIGQUIT 不阻塞\n");
}
else if (ret == 1)
{
printf("SIGQUIT 阻塞\n");
}
return 0;
}
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
# 2、sigprocmask 与 sigpenging 函数
# 2.1 函数解释
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能: 将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
- 参数:
- how : 如何对内核阻塞信号集进行处理
SIG_BLOCK:将用户设置的阻塞信号集添加到内存中,内核原来的数据不变
假设内核中默认的阻塞信号集是mask,那么将自定义信号集设置到内核就是执行了这样的运算 mask | set
SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
mask & ~set
SIG_SETMASK:覆盖内核中原来的值
- set : 已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
-返回值:
成功 :0
失败 :-1
设置错误号: EFAULT 、EINVAL
int sigpenging(sigset_t *set);
- 功能:获取到内核中的未决信号集
- 参数: set,传出参数,保存的是内核中的未决信号集中的信息。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.2、案例
- 编写一个程序,把常规信号的未决状态打印到屏幕
- 设置某些信号是阻塞的,通过键盘产生这些信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int num = 0;
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set); // 清空信号集
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
while (1)
{
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
for (int i = 1; i < 32; i++)
{
if (sigismember(&pendingset, i) == 1)
{
printf("1");
}
else if (sigismember(&pendingset, i) == 0)
{
printf("0");
}
else
{
perror("sigismember");
}
}
printf("\n");
sleep(1);
if (num == 10)
{
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
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
- 讲的sigismember == 1表示那个信号被阻塞/或者是未决并不合理,应该是说=1是表示在这个信号集里的这一位为1。这个信号集可以是指你自己建的信号集,也可以是指未决信号集,也可以是PCB里的阻塞信号集。它只是查询sigset_t这个数据类型的某一位的值是否为1而已。。所以在循环里面对未决信号集进行某一位查询的时候,只是因为前面对PCB里的阻塞信号集的SIGINT和SIGQIUT进行了设置,这两个信号后来被阻塞了,被阻塞的信号没有被处理,所以会被加入到未决信号集里,所以每次查询未决信号集的时候能查到那一位才为1。
# 2.3、运行截图
- 之所以按下
ctrl + c
和ctrl + \
后进程没有终止,是因为我们将信号2、3设置进了阻塞信号集,所以即使信号产生也不会杀死进程 - 可以看到,信号2、3被阻塞,未决信号集第2、3位置1
# 补充:切换前后台运行
- 执行可执行程序的时候在后面加个
&
,使程序在后台执行,这样我们就可以边执行边继续在前台输入指令
- 注意,因为我们程序是在后台运行时,所以按下
ctrl + c
,程序是没有收到信号的- 输入
fg
切换回后台执行
# 3、内核实现信号捕捉的过程
# 八、SIGCHLD 信号
# 1、SIGCHLD 产生条件
- SIGCHLD信号产生的条件 :
- 子进程终止时
- 子进程接收到 SIGSTOP 信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
- 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
# 2、SIGCHLD 案例
/*
SIGCHLD信号产生的条件:
1.子进程终止时
2.子进程接收到 SIGSTOP 信号停止时
3.子进程处在停止态,接受到SIGCONT后唤醒时
以上情况都会给父进程发送该信号,父进程默认忽略该信号。
使用SIGCHLD信号解决僵尸进程的问题。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num)
{
// 接收到信号后,回收子进程PCB的资源
printf("捕捉到的信号 : %d\n", num);
/*不能直接这样写,是因为我们的未决信号集只能记录信号产生,而不能记录信号产生次数,
如果同时(极短时间内)有几个子进程终止发出了SIGCHLD信号,但是未决信号集对应为仍是1,
然后信号被捕捉到后进入信号处理程序,这就导致了同时发出的信号,只调用了一次处理函数,其余的信号被忽略,发出该信号的子进程的资源没有被回收,产生僵尸进程,
所以要想解决这个问题就要循环调用wait(),去回收资源(其实也会有问题,下面会提到)*/
//① wait(NULL);
// 回收子进程PCB的资源
while (1)
{
// 不使用wait()是因为 wait 不能设置非阻塞,如果在循环里用wait()那回调函数就陷入死循环了,父进程就不能做其他的事情
//② wait(NULL);
//③ 设置为不阻塞.这样就不会说捕获一次就死循环了
int ret = waitpid(-1, NULL, WNOHANG);
if (ret > 0)
{
printf("child die , pid = %d\n", ret);
}
else if (ret == 0)
{
// 说明还有子进程活着
break;
}
else if (ret == -1)
{
// 没有子进程
break;
}
}
}
int main()
{
// ④提前设置好阻塞信号集,阻塞 SIGCHLD,因为有可能子进程很快结束,父进程还没注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
pid_t pid;
for (int i = 0; i < 20; i++)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
// 父进程
// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// ⑤注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while (1)
{
printf("parent process pid : %d\n", getpid());
sleep(2);
}
}
else if (pid == 0)
{
// 子进程
printf("child process pid : %d\n", getpid());
}
return 0;
}
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 3、案例思考:
上面是完整的正确的代码,思考一下上面使用不同代码会出现的问题?然后应该怎么解决!
# 3.1 问题1
① myFun函数里只使用
wait(NULL)
去回收子进程资源,会发生什么,为什么,应该怎么修改?void myFun(int num) { // 接收到信号后,回收子进程PCB的资源 printf("捕捉到的信号 : %d\n", num); wait(NULL); } }
1
2
3
4
5
6
7
# 答案:
- 会发现进程运行过程中有些子进程的资源没有被回收,产生僵尸进程,产生原因如下:
- 未决信号集只能记录信号产生,而不能记录信号产生次数,如果同时(极短时间内)有几个子进程终止发出了SIGCHLD信号,但是未决信号集对应为仍是1,然后信号被捕捉到后进入信号处理程序,这就导致了同时发出的信号,只调用了一次处理函数,其余的信号被忽略,发出信号的子进程的资源没有被回收,产生僵尸进程
# 3.2 问题2
② 为什么不在while 循环里使用 wait(NULL) ,应该如何修改?
void myFun(int num) { while (1) { wait(NULL); } }
1
2
3
4
5
6
7
# 答案:
不使用wait()是因为 wait 不能设置非阻塞,如果在循环里用wait()那回调函数就陷入死循环了,父进程就不能做其他的事情
修改如下:
void myFun(int num) { while (1) { int ret = wait(NULL); if (ret > 0) { printf("child die , pid = %d\n", ret); } else if (ret == -1) { // 所有子进程都结束 break; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面这样写,就会不出现说进程被阻塞的情况,因为所有子进程结束后就会退出循环,但是这样其实也是不提倡的,原因还是 wait 不能设置非阻塞,所以在函数执行期间父进程依旧是被阻塞,不能执行父进程代码的,所以我们不提倡使用
wait()
,所以要使用waitpid()
# 3.3 问题3
③ 修改完后还会出现问题吗?会的话又是为什么?
void myFun(int num) { // 接收到信号后,回收子进程PCB的资源 printf("捕捉到的信号 : %d\n", num); // 回收子进程PCB的资源 while (1) { //设置为不阻塞.这样就不会说捕获一次就死循环了 int ret = waitpid(-1, NULL, WNOHANG); if (ret > 0) { printf("child die , pid = %d\n", ret); } else if (ret == 0) { // 说明还有子进程活着 break; } else if (ret == -1) { // 没有子进程 break; } } }
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
# 答案:
上面这样写其实就可以了,但是有可能会在执行时出现段错误,原因是:
父子进程执行的时间顺序不是固定的,有可能父进程里的信号捕捉还没注册完成,而子进程已经全部执行结束发出了SIGCHLD信号,等信号捕捉注册完成后,我们已经回收不到子进程了(视频里老师的说法,但是感觉并不对)
(评论区大佬的说法)
视频中出现段错误的原因在于在信号处理函数中调用了不可重入的函数:
使用gdb调试跟踪函数调用栈
最下层f 23可以看到是在main函数中,再往上f 22是在父进程中调用了printf
再往上f 10可以看到调用了信号处理函数,这里是我们在main函数中调用printf但是printf还没有调用完成,直接转到了信号处理函数,我这里的信号处理函数为handler,见f 9,再往上f 8调用printf,可以看到f 8 和f 22是一样的
SIGSEGV是因为printf会分配临时空间,在主函数调用printf至malloc时,中断处理函数调用,在其中也调用了printf至malloc时就出现了错误。
# 3.4 问题4
- ④⑤,怎么解决段错误问题?
# 答案:
- 在信号捕捉函数注册前将信号阻塞,等注册完信号捕捉后再解除阻塞(完整代码看上面 2、SIGCHLD案例)
# 4、其余问题
# 问题1:
- 在父进程捕捉SIGCHLD信号的信号处理函数sig_handler中若仅仅只调用一次wait()为什么不能处理掉所有的僵尸子进程?
sig_handler函数开始执行,未决信号集中SIGCHLD信号的标记从1变为0,此时有SIG_CHLD信号发送过来的时候,该位就会变成1,但后续所有的SIG_CHLD都会被忽略,直到当前sig_handler执行完毕,内核发现该位依然是1,就继续执行sig_handler,此时未决信号集中SIGCHLD信号的标记才会变为0。
由于wait的一次调用只能处理一个死亡的子进程,并且是pid最小的那个,所以可以看到虽然父进程没有处理完所有的僵尸子进程,但是输出的进程号是连续的。
假设父进程有30个子进程,当30个子进程都发完了他们各自的SIGCHLD信号之后,父进程可能才刚刚进入第25个子进程发来的SIGCHLD信号触发的sig_handler函数,在当前这个sig_handler函数被执行完之后,父进程再也不会执行sig_handler函数了,因为26号到30号子进程发送的SIG_CHLD信号被丢弃了。那么26号到30号子进程就成为僵尸进程,不会被父进程处理。
# 问题2:
- 为什么加了while可以回收之前被忽略掉SIGCHLD的僵尸进程?
- A子进程产生信号,调用了myfun函数,waitpid(wait函数同理)就只会去回收A进程。waitpid函数是个劳模,它只要见到僵尸进程就忍不住要回收,但能力有限,一次只能回收一次。只要给它机会,它可以把所有的僵尸进程一网打尽。所以只要有while循环,就可以不断执行waitpid函数,直到break。
# 问题3:
- 如果信号阻塞以后不能被捕获,那么是如何做到 “先阻塞SIGCHLD信号,当注册完信号捕捉以后,再解除阻塞,这样就会继续执行回调函数回收资源”?
- 要弄懂这个问题,我们需要理清内核是如何处理信号的。信号的产生是异步的,A子进程产生SIGCHLD信号,不意味着父进程要立刻捕捉然后去做一些反应。当信号产生时,内核中未决信号集第17位会置1,它会等待父进程拥有cpu权限再去执行捕获信号处理函数,在去处理的瞬间17号位就会由1变为0,代表该信号有去处理了。
- 当我们提前设置了堵塞SIGCHLD信号,那未决集中就会一直保持1,不会调用捕获信号处理函数(也可以说信号不能被捕获),等待堵塞解除。所以并不是说,我们把信号堵塞了,然后解除堵塞,这个信号就消失了,它还是在未决集中的,值为1。捕捉函数捕获的其实就是这个1。信号捕捉不是钓鱼,钓鱼的话如果不及时处理,鱼就会跑掉。更像是网鱼,只要信号入网了,就跑不掉了。等我们准备好工具去捕获,会看到网上的鱼还是在的。
- 高老师最后为什么要提前堵塞SIGCHLD信号?加了阻塞之后是什么情况?假设极端情况,20个子进程老早就终止了,内核收到SIGCHLD信号,会将未决信号集中的17号位置为1,就算他们是接连终止,该信号位也不会计数,只有保持1 。但同时该信号被提前阻塞,所以该17号位置保持1(阻塞是保持1,不是变回0),等待处理。当注册完信号捕捉函数以后,再解除阻塞。内核发现此时第17号位居然是1,那就去执行对应的捕获处理函数。在处理函数中,waitpid函数发现:“哎呦,这怎么躺着20具僵尸呀”,然后它就先回收一具僵尸,返回子进程id,循环第二次,继续回收第2具僵尸,直到所以僵尸被回收,此时已经没有子进程了,waitpid函数返回-1,break跳出循环。
- while循环中,返回值0对应的是没有僵尸但有正常的儿子,返回值-1代表压根没有儿子。所以只要子进程中存在僵尸,这个while就不会break,waitpid就可以悠哉悠哉地一次回收一具。
- 《Linux/UNIX系统编程手册》指出为了保障可移植性,应用应在创建任何子进程之前就设置信号捕捉函数。【牛客789400243号】提出了这个观点,应该在fork之前就注册信号捕捉的。其实就是对应了书上这句话。
# 问题4:
- 段错误究竟是怎么发生的?段错误的复现为什么这么难?
- 段错误是个迷,有的人碰到过几次,有的人怎么也碰不到,这是由于神秘莫测的调度算法导致的。【潇潇_暮雨】小伙伴提出了,这是调用了不可重入的函数。《Linux/UNIX系统编程手册》第21.1.2节 对可重入函数进行了详细的解释,有兴趣的可以去翻一下。
- 可重入函数的意思是:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。通俗点讲,就是存在一个函数,A线程执行一半,B线程抢过CPU又来调用该函数,执行到1/4倍A线程抢回执行权。在这样不断来回执行中,不出问题的,就是可重入函数。多线程中每个线程都有自己的堆栈,所以如果函数中只用到局部变量肯定是可重入的,没问题的。但是更新了全局变量或静态数据结构的函数可能是不可重入的。假设某线程正在为一个链表结构添加一个新的链表项,而另外一个线程也视图更新同一链表。由于中间涉及多个指针,一旦另一线程中断这些步骤并修改了相同指针,结果就会产生混乱。但是并不是一定会出现,一定是A线程刚好在修改指针,另外一线程又去修改才会出现。这就是为什么该问题复现难度较高的原因。
- 作者在文中指出,将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是stdio函数库成员(printf()、scanf()等),它们会为缓冲区I/O更新内部数据结构。所以,如果在捕捉信号处理函数中调用了printf(),而主程序又在调用printf()或其他stdio函数期间遭到了捕捉信号处理函数的中断,那么有时就会看到奇怪的输出,设置导致程序崩溃。虽然printf()不是异步信号安全函数,但却频频出现在各种示例中,是因为在展示对捕捉信号处理函数的调用,以及显示函数中相关变量的内容时,printf()都不失为一种简单而又便捷的方式。真正的应用程序应当避免使用该类函数。
- printf函数会使用到一块缓冲区,这块缓冲区是使用malloc或类似函数分配的一块静态内存。所以它是不可重入函数。
# 问题5:
- 请问如果是因为主函数调用printf至malloc时,中断处理函数调用,在其中也调用了printf至malloc所以出现了段错误,那为什么我们先阻塞信号,等信号捕捉函数注册成功后在解除阻塞就不会出现段错误了,注册完再解除阻塞,主进程还是会调用printf函数啊,中断函数里也还是会调用printf函数,为什么这样就不会出现段错误了???
- 还没找到答案~,先插眼课程列表_牛客网 (nowcoder.com) (opens new window)