目录
3.1 fork创建子进程,但是没有exec加载新程序时,信号的继承情况
4、kill、raise、alarm、pause、abort函数
6.1.3 我们可不可以自己修改信号屏蔽字,实现某个信号的打开和屏蔽呢?
1. 信号
1.1 什么是信号
信号是一种向进程发送通知,告诉其某件事情发生了的一种简单通信机制。
1.2 信号的命名
Linux下边定义了很多的信号,所有的信号都是一个整数编号,不过为了好辨识,Linux系统给这些整数编号都定义了对应的宏名, 宏名都是以SIG开头
1.3 谁会向进程发送信号
总结起来,会有三个“人”会向进程发送信号,分别是“另一进程”、“OS内核”、“硬件”。
1.4 进程收到信号后,进程会如何处理
三种处理方式,分别是忽略、捕获、默认。
1.5 都有哪些信号
1.5.1 信号列表
(1)35~64:这些信号是Linux后期增设的信号,这些个信号不需要关心
(2)1~34:也不是所有的信号都要掌握,我们只关心其中常用的信号
1.5.2 常用信号
(1)为什么当进程收到某些信号时,会被终止呢?
因为你发送的这些信号的处理方式是终止,所以进程会被终止掉。
(2)kill命令
1)kill的作用
2)pkill
(3)信号的发送与接收
1)发送
一般来说,大多数发送信号的原因,都是因为内核、硬件发生了某些事件时,才会向某个进程发送
我们自己发送信号的原因无非如下几种情况:
2)接收
对于我们自己写的进程来说,最常见信号操作的还是接收信号,不过在一般情况下,我们进程并不会去重新设置信号的处理方式,而是使用信号的默认处理方式来处理信号
(4)core文件
1)什么是core文件
用于保存程序(进程)在当前结束的这一刻,进程在内存中的代码和数据,core文件可以用于分析进程在结束时的状况,不过由于进程代码和数据都是二进制的,一般需要特殊软件翻译后才能看懂。
2)并不是所有的信号在终止进程时都会产生core文件
3)如果你不想丢弃core文件怎么办
对相关的系统文件进行设置就可以了,core文件一般默认保存在当前路径下。
2. signal函数
函数原型
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
调用捕获函数的过程
- 当信号没有发生时,进程正常运行,当信号发生时,进程的正常运行会被中断,然后去处理信号
- 一看信号的处理方式是捕获,就会从“信号处理方式登记表”中将捕获函数的地址取出并执行捕获函数,
- 捕获函数执行完毕后,恢复进程的正常运行。
不过当信号来时,如果当前有一条指令正在运行,会先等这条指令运行执行完毕后再去调用信号处理函数。
不过如果捕获函数有调用exit或者_exit的话,进程会被终止,不过是正常终止。
值得强调的地方
(1)信号被设置为SIG_DFL时,表示将处理方式设置为默认
其实在不做任何处理方式设置的情况下,信号的处理方式就是系统设置的默认处理方式。
(2)信号被设置为SIG_IGN(忽略)时
进程将不会再接收到这个信号,这信号对进城没有任何影响。
(3)设置为捕获时,需要将handler设置为捕获函数的地址,类型为void (*)(int)
为了确保和捕获函数的类型统一,SIG_DFL、SIG_IGN和SIG_ERR宏的类型也必须是void (*)(int)。
(4)除了SIGKILL这两个信号外,其它所有的信号都可被忽略和捕获。
3. 子进程对父进程信号的继承情况
父进程fork出子进程时,子进程会继承父进程很多的属性,其中就包括信号
3.1 fork创建子进程,但是没有exec加载新程序时,信号的继承情况
在fork子进程之前,如果父进程调用signal设置了某个信号的处理方式的话,那么fork出的子进程会继承父进程对该信号设置的处理方式
父进程将信号的处理方式设置为捕获时,捕获函数对子进程也是有效的。
再次强调,只有在fork之前,父进程所设置的信号处理方式,才会被子进程继承。
(1)为什么捕获函数在子进程里面依然有效。
因为子进程复制了父进程的代码和数据,子进程自然也会包含信号处理函数的代码,所在子进程中依然有效。
(2)子进程可以自己调用signal函数,修改掉所继承的处理方式。
(3)那如果父进程是在if(ret > 0){}里面设置得呢?
这就是父进程自己的设置,跟子进程没有关系。
3.2 当有调用exec加载新程序时
3.2.1 fork之前,父进程设置的处理方式是忽略 或 默认时
exec加载新程序后,忽略和默认设置依然有效。
3.2.2 fork之前,父进程设置处理方式是捕获时
新程序的代码会覆盖子进程中原有的父进程的代码,信号捕获函数的代码也会被覆盖,
既然捕获函数已经不存在了,捕获处理方式自然也就没有意义了,所以信号的处理方式会被还原为默认处理方式。
终之,如果子进程所继承的信号处理方式是捕获的话,exec加载新程序后,捕获处理方式会被还原为默认处理方式。
3.3 总结
4、kill、raise、alarm、pause、abort函数
4.1 kill、raise
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); //kill命令就是调用这个函数来实现。 #include <signal.h> int raise(int sig);
4.2 alarm、pause
#include <unistd.h> unsigned int alarm(unsigned int seconds); int pause(void);
4.3 abort函数
也被称为叫自杀函数,之所以称为自杀函数,是因为调用该函数时,会向当前进程发一个SIGABRT信号
这个信号的默认处理方式是终止,因此如果不忽略和捕获的话,会将当前进程终止掉。
5. 使用信号唤醒休眠函数
5.1 会导致休眠的函数
我们调用sleep、pause等函数时,这些函数会使进程进入休眠状态,如果你不想继续休眠时怎么办?
可以使用信号将其唤醒。
5.2 唤醒的方法
给信号登记一个空捕获函数即可,当然你也可以在捕获函数写你要的代码,不过如果仅仅只是用于唤醒的话,捕获函数的内容一般都是空的。
5.3 唤醒的过程
当信号发送给进程后,会中断当前休眠的函数,然后去执行捕获函数,捕获函数执行完毕返回后,不再调用休眠函数,而是执行休眠函数之后的代码,这样函数就被唤醒了。
5.4 我想继续休眠怎么办
自己手动重新启动休眠函数(重新调用休眠函数)。
根据被信号捕获后 pause的返回值 调用goto
5.5 休眠函数自动重启
比如使用read从键盘读取数据,当键盘没有输入数据时,read会休眠,不过函数被信号唤醒后,会自动重启read的调用。
当read函数休眠时,如果被信号唤醒了,当捕获函数返回后,read会自动重启。
对于绝大多数休眠函数来说,被信号中断后,如果你想继续休眠的话,需要自己去手动重启
6. 信号的发送、接收和处理的过程
6.1 信号屏蔽字
6.1.1 信号屏蔽字的作用,以及它被放在了哪里
6.1.2 屏蔽字长啥样子
为了方便理解,我们简单地认为屏蔽字就是一个64位的unsigned int数,每一位对应着一个信号
0表示信号可以 被立即处理 1表示该信号被屏蔽了,暂不处理。
6.1.3 我们可不可以自己修改信号屏蔽字,实现某个信号的打开和屏蔽呢?
可以
只不过在默认情况下,信号屏蔽字中所有的位都为0,也就说默认将所有的信号都打开了。
6.2 未处理信号集
6.2.1 作用
跟屏蔽字一样,也一个64位的无符号整形数,专门用于记录未处理的信号。
“未处理信号集”同样也是被放在了进程的进程表中(task_struct)。
6.2.2 什么时候会记录
6.2.3 什么时候处理记录的“未处理信号”
当屏蔽字中该信号的位变成0时(被打开了),此时就回去检查“未处理信号”,看该信号有没有未决情况,有的话就处它。
6.3 信号处理的完整过程
7. 修改信号屏蔽字的API
7.1 修改的原理
(1)定义一个64位的与屏蔽字类似的变量
(2)将该变量设置为要的值 ----------将某信号对应的位设置为0或者为1。
(3)使用这个变量中的值来修改屏蔽字
7.2 设置变量的API
#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);
set就是我们前面说的变量,至于变量名也可以定义为其它的名字,不一定非要叫set。
7.3 使用变量修改屏蔽字的API
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
8.sigaction函数
sigaction函数相当于是signal函数的复杂版