文章目录
- 信号的基本概念
- kill -l 查看信号列表
- 信号的处理方式
- signal( ) 自定义处理信号
- 信号的产生方式
- 键盘产生
- 进程异常(core dump)
- 系统调用
- 软件条件
- 信号的发送(OS)
- 信号常见相关名词解释
- 进程接收处理信号原理
- 信号集函数的使用
- 打印pending表
- 信号的捕捉
- 可重入函数
- volatile关键字
- SIGCHLD信号
信号的基本概念
生活角度的信号
闹钟、红绿灯、信号强、鸡叫声… -> 都可以代表一种信号,这种信号是给人看的
当我们听到这些场景触发的时候,我们立马能想到什么,我立马就知道了接下来即将发生什么,我们该如何应对
对于信号的处理动作,我们早就知道了,甚至远远早于信号的产生
是不是只有这些场景在我们面前我们才知道该怎么做呢?其实和场景是否被触发,没有直接关联!
本质:我记住了现实生活中各种行为所代表的意义,并且知道看到这些行为我应该使用什么处理方式
计算机角度的信号
信号的产生->信号是给进程发的->进程要在合适的时候,执行相应的动作
进程在没有收到信号的时候,就知道应该识别哪些信号,以及对各种信号该如何处理
本质:曾经编写操作系统的工程师在写进程源代码的时候就已经设计好了所以进程具有识别并处理信号的能力,是远远早于信号产生的
在生活中,我们收到某种信号的时候,并不是一定处理的信号随时都可能产生(异步),但是我当前有更重要的事情进程收到信号的时候,并不是立即处理,而是在合适的时候进行处理既然信号不能被立即处理,已经来的信号,是不是就应该被暂时保存起来信号被保存在哪里呢?PCB (struct task_struct)
信号的本质也是:数据
信号的发送的本质:操作系统往进程task_struct内写入数据
结论:
1.信号是操作系统发送给进程的一种数据,信号发送的本质就是向进程的PCB内写入数据。
2.进程收到信号后并不是立即处理,而是在合适的时候对其进行处理,在此期间信号被暂时保存了起来
3.进程收到信号后会做出反应是因为编写操作系统的工程师在写进程源代码的时候就已经设计好了所以进程具有识别并处理信号的能力,是远远早于信号产生的
kill -l 查看信号列表
指令:kill -l 命令可以用于查看系统定义的信号列表
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到
编号1 ~ 31的信号都是普通信号,剩下的都是实时信号。本章讨论的是普通信号,这些信号各自在什么条件下产生,默认处理动作是什么,在man 7 single 中有详细说明
信号的处理方式
一般而言,进程收到信号的处理方式有三种情况
1.默认动作 – 一部分是杀死自己,或者暂停等待
2.忽略动作 – 也是信号的处理方式之一,不过就是收到信号后什么也不干
3.自定义动作(信号的捕捉) – 使用signal等方法,修改进程收到信号的处理动作(默认 -> 自定义)
signal( ) 自定义处理信号
第一个参数 signum
需处理的信号的编号或者宏
第二个参数 handler
处理程序,用于修改进程收到信号的默认处理动作
小测试1
//前台进程运行时,输入Ctrl + C, OS会向前台进程发送2号信号(SIGINT),默认情况下进程会终止
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
printf("get a signal !! signal number = %d, process pid = %d", signum, getpid());
}
int main()
{
signal(2, handler);
while (1){
sleep(1);
printf("I'm a process, my pid = %d\n", getpid());
}
return 0;
}
实验现象
运行程序,发现当我们输入Ctrl + c时进程并未终止反而打印了一句话,而这句话就是我们的自定义动作。此时我们可以通过
ps axj | grep + 可执行程序文件名来进行进程的查找运行进程的pid,然后使用kill -9 + pid来终止进程
小测试2
使用for循环对所有信号都进行捕获,修改进程收到信号的默认处理动作。我们发现发送2号、3号、20号信号都成功被捕获,但是发送9号信号的时候,进程被杀死了。这说明9号信号是不能被捕获的
基本结论:
1.信号的产生方式之一就是键盘
2.9号信号无法被捕获,所以也不能被自定义
信号的产生方式
1.键盘产生 2.进程异常 3.系统调用 4.软件条件
但是虽然信号产生的方式很多,但是无论信号产生的方式千差万别,最终一定是通过OS向目标进程发送信号的
键盘产生
在处理信号方式的实验中,我们向前台进程发送^C, ^Z, ^\ 以及使用指令kill -9 + pid 都是使用键盘产生信号,然后操作系统将信号发送给进程
进程异常(core dump)
程序运行的时候,若出现类似野指针访问,除以零等异常问题,与计算相关的软件或者硬件就会出现问题,而操作系统是软硬件的管理者,就要对软硬件的健康负责,所以操作系统会发送信号给产生异常的进程,让进程终止
当进程崩溃了,我们最想知道什么??
我们最想知道崩溃的原因 -> 收到了哪一个信号
我还想知道在哪里崩溃的,core dump标志就是来解决这个问题的
在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)
当一个进程异常的时候,进程的退出信号会被设置,表明当前信号退出的原因
如果有必要,OS会设置退出信息中的core dump标志位,并将内存中的数据转储到磁盘中,方便调试
在云服务器中,core dump这项服务是被关掉的
指令:ulimit -a (查看系统资源)
指令:ulimit -c 10240(打开core dump 服务)
小测试
//打开core dump服务后,写一个存在异常的程序,并编译运行
[clx@VM-20-6-centos singal_blog]$ cat signal_test.c
#include <stdio.h>
int main()
{
int a = 10;
printf("%d", a / 0);
return 0;
}
//运行后
[clx@VM-20-6-centos singal_blog]$ ll
total 168
-rw------- 1 clx clx 282624 Oct 26 08:32 core.14988
-rw-rw-r-- 1 clx clx 80 Oct 26 08:05 Makefile
-rwxrwxr-x 1 clx clx 8368 Oct 26 08:32 mytest
-rw-rw-r-- 1 clx clx 84 Oct 26 08:32 signal_test.c
我们发现系统生成了一个文件叫做core.14988,是一个二进制文件,这个文件并不是直接给我们读的,而是给调试器gdb看的
//更改Makefile
[clx@VM-20-6-centos singal_blog]$ cat Makefile
mytest:signal_test.c
gcc -o $@ $^ -std=c99 -g //添加-g选项,提取调试信息
.PHONY:clean
clean:
rm -f mytest
指令:core-file + core文件名(事后调试)
程序出现异常后,我们可以使用core dump服务获取其报错信息,然后使用gdb的core-file + 文件名的指令读取异常信息,就可以知道程序在哪出现异常,又收到了什么信号。这种方法叫做事后调试
关于core dump 标志位小测试
[clx@VM-20-6-centos singal_blog]$ cat signal_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
void test1()
{
if (fork() == 0){
printf("I'm child\n");
int a = 10;
printf("%d", a / 0);
}
int status = 0;
waitpid(-1, &status, 0);
printf("child exit code = %d, child get signal = %d, core dump flag = %d\n", (status >> 8) & 0xff, status & 0x7f, (status >> 7) & 1);
printf("father end\n");
}
int main()
{
test1();
}
可以看到生成了core文件并且子进程收到了八号信号,core dump标志位被设置成1
进程并非收到所有信号都会生成core文件,比如收到SIGINT(2号信号)就不会生成,可以自行测试一下
系统调用
1.kill系统调用接口
小测试
//signal_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
void Usage(const char* proc){ //使用手册
printf("Usage:\n\t %s signo who\n", proc);
}
int main(int argc, char *argv[])
{
if (argc != 3){ //若输入的参数数量不对,则调用使用手册后退出程序
Usage(argv[0]);
return 1;
}
int who = atoi(argv[2]); //使用atoi对输入的字符串转化为int类型
int signal = atoi(argv[1]);
kill(who, signal); //调用kill系统调用接口
printf("signal = %d, who = %d\n", signal, who); //打印信息
}
//sleep.c
[clx@VM-20-6-centos singal_blog]$ cat sleep.c
#include <stdlib.h>
#include <unistd.h>
int main()
{
sleep(1000);
return 0;
}
可以看到我们通过自己写的程序杀死了一个正在运行的进程,kill接口的作用就是向任意进程发送任意一个信号
2.rasiz
向自己所在进程发送一个sig信号
3.about
向自己所在进程发送一个abort(八号)信号
软件条件
通过某种软件(OS),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等场景下,触发信号发送
在进程间通信管道部分的学习中,当读端不光不读,并且关闭了读fd,写端一直在写,这是对操作系统资源的一种浪费,操作系统会向写端发送十三号信号来终止进程,这就是一种典型的软件条件触发信号发送
SIGPIPE信号是一种由软件条件产生的信号,在“管道学习"中已经介绍过了,本节主要介绍alarm函数和SIGAKRM信号
alarm 定时器函数
参数:seconds
等待seconds秒后会向当前进程发送alarm信号(14号)
返回值
设置成功返回0,若alarm函数还未发送信号,在执行下方代码过程中出现alarm(0)取消定时器的指令时,会返回剩余的时间
alarm(0)取消定时器,若距离alarm(5)两秒后收到取消定时器的命令,则函数返回3
小测试1
[clx@VM-20-6-centos singal_blog]$ cat signal_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
void handler(int signum) //检验alarm函数发送几号信号
{
printf("signum = %d\n", signum);
}
int main()
{
for (int i = 1; i <= 31; i++){
signal(i, handler);
}
alarm(3);
while (1){
sleep(1);
printf("I'm a process !! pid = %d\n", getpid());
}
return 0;
}
//测试结果
[clx@VM-20-6-centos singal_blog]$ ./mytest
I'm a process !! pid = 2262
I'm a process !! pid = 2262
signum = 14
I'm a process !! pid = 2262
小测试2
int count = 0;
W>void handler(int signum)
{
printf("hello : %d\n", count);
exit(1);
}
int main()
{
signal(14, handler);
alarm(1);
while (1)
{
count++;
}
return 0;
}
//结果1
[clx@VM-20-6-centos singal_blog]$ ./mytest
hello : 434772462 //可以看到在1秒钟时间里count++执行了4亿多次
//修改我们的代码
int count = 0;
int main()
{
alarm(1);
while (1) {
printf("hello : %d\n", count);
count++;
}
return 0;
}
//执行结果
hello : 15389
hello : 15390
hello : 15391Alarm clock //一秒钟打印了一万五千次
结论:频繁的IO会严重影响效率,cpu有大量时间会等待数据从内存刷新到外设
信号的发送(OS)
进程PCB使用一个三十二位的位图结构(uit32_t )来存储信号信息
OS向进程发送信号的本质:OS向指定进程的PCB中的信号位图中对应比特位设置成1,即完成信号的发送
信号常见相关名词解释
实际信号的处理动作称为信号递达【Delivery】(自定义捕捉, 默认, 忽略)
信号从产生到递达之间的状态,称为信号未决【Pending】(本质是信号被暂存在PCB信号位图中)
进程可以选择阻塞【Block】某个信号
阻塞的本质是OS,允许进程暂时屏蔽指定信号 1.该信号依然是未决的 2.该信号不会被递达,直到解除阻塞,方可递达
进程接收处理信号原理
注:block表也称信后屏蔽字.这些位图结构的类型在Linux系统下叫做sigset_t
信号集函数的使用
sigset_t
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
信号集操作函数汇总
int sigemptyset(sigset_t *set); //将所有位置零
int sigfillset(sigset_t *set); //将所有位置一
int sigaddset (sigset_t *set, int signo); //添加一个信号(将对应信号位置1)
int sigdelset(sigset_t *set, int signo); //删除一个信号(将对应信号位置0)
int sigismember(const sigset_t *set, int signo); //判定一个信号是否在集合中
sigprocmask
第一个参数 how
可以传入上述三个宏
第二个参数 set
输入型参数,OS会根据set来对原有的位图结构进行操作
第三个参数 oldset
输出型参数,OS返回原来的位图结构
以上函数是对block 表,也就是信号屏蔽字进行操作。pending表的类型和block相同,但是pending表的修改是由操作系统完成的,我们只能获取pending表但是不能修改
小测试:定制block表
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main()
{
sigset_t set; //创建两个信号集数据结构
sigset_t oldset;
sigemptyset(&set); //将两个信号集制空
sigemptyset(&oldset);
sigaddset(&set, 2); //将2号信号设置为阻塞
sigprocmask(SIG_SETMASK, &set, &oldset); //替换OS的block表,用set代替,oldset接收OS原block表
while (1){
sleep(1);
printf("I'm a process, my pid = %d\n", getpid());
}
return 0;
}
//现象展示
[clx@VM-20-6-centos sigset_test]$ ll
total 20
-rw-rw-r-- 1 clx clx 91 Oct 26 18:34 Makefile
-rwxrwxr-x 1 clx clx 8632 Oct 26 18:37 mytest
-rw-rw-r-- 1 clx clx 340 Oct 26 18:37 mytest.c
[clx@VM-20-6-centos sigset_test]$ ./mytest //运行程序
I'm a process, my pid = 10311
I'm a process, my pid = 10311
^CI'm a process, my pid = 10311
^C^CI'm a process, my pid = 10311 //使用键盘手动发送2号信号
I'm a process, my pid = 10311 //2号信号并未递达
^\Quit //输入三号信号终止程序
九号信号是管理员信号,不会被阻塞,进程一旦受到9号信号会立马递达,杀死进程
打印pending表
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void print_pending(sigset_t* pending){ //打印思路很简单,若标记位为1则打印1,若为0则打印0
for (int i = 1; i < 31; i++){
if (sigismember(pending, i)){
printf("1");
}
else {
printf("0");
}
}
printf("\n");
}
int main()
{
sigset_t block_set; //创建block表
sigset_t pending; //创建一个信号集结构接收pending表
sigemptyset(&pending);
sigemptyset(&block_set);
sigaddset(&block_set, 2); //将二号信号设置成阻塞状态
sigprocmask(SIG_SETMASK, &block_set, NULL); //用我们的新block表替换系统的旧表
while (1){
sigemptyset(&pending); //将pending表制空
sigpending(&pending); //获取OS中的pending表
print_pending(&pending); //打印pending表
sleep(1);
}
}
实验现象:
想进程发送2号信号,信号被pending表接收,打印出来的pending表2号位变成1,但是进程并未被终止,因为2号信号被阻塞了。最后发送9号信号,终止进程
若进程收到信号,但此信号属于阻塞状态。当我们修改block表接触这个信号的阻塞状态后,这个信号会被递达,接下来修改上面的代码,看一看这个情况
小测试:取消阻塞,信号递达
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void print_pending(sigset_t* pending){
for (int i = 1; i < 31; i++){
if (sigismember(pending, i)){
printf("1");
}
else {
printf("0");
}
}
printf("\n");
}
void handler(int signum) //捕获二号信号,打印一句话
{
printf("%d singal delivered\n", signum);
}
int main()
{
int count = 0;
signal(2, handler); //自定义2号信号递达后进程对其的处理
sigset_t block_set, oldset;
sigset_t pending;
sigemptyset(&pending);
sigemptyset(&block_set);
sigaddset(&block_set, 2);
sigprocmask(SIG_SETMASK, &block_set, &oldset);
while (1){
if (++count == 20){//计数器,20秒时取消对二号信号的阻塞
sigprocmask(SIG_SETMASK, &oldset, NULL);
}
sigemptyset(&pending);
sigpending(&pending);
printf("count = %d ", count);
print_pending(&pending);
sleep(1);
}
}
信号的捕捉
信号的延时处理取决于OS和进程。
信号的产生是异步的,进程会在合适的时候处理信号,因为当前进程可能在做更重要的事。
那么什么是合适的时候?
从内核态切换回用户态的时候,进行信号的检测和处理
用户态:执行用户的代码和数据时,计算机所处的状态叫用户态。用户的代码执行全是在用户态
内核态:执行 OS 的代码和数据时,计算机所处的状态叫内核态。0S的代码执行全是在内核态
用户的身份是以进程为代表的,CPU寄存器保存当前进程的状态
用户态使用的是,用户级页表,只能访问用户数据和代码
内核态使用的是,系统及页表,只能访问系统数据和代码
进程具有地址空间,虽然能够看到用户和内核的所有数据但是并不一定可以访问,会受权限的约束
用户的数据和代码一定要被加载到内存,那么0S的数据和代码呢?也必须要加载到内存中,OS的代码是怎么被执行到的呢?
内核页表被所有进程共享,通过内核页表所有进程都可以使用操作系统的代码和数据
所以进程不管如何切换,我们都能保证管理它们的是同一个操作系统,因为每个进程都使用同一张系统级页表
所谓的系统调用,实际就是将身份转化为内核,然后使用系统级页表找到系统函数进行执行就可以了
在大部分情况下,OS都可以在进程的上下文中直接运行的,因为所有进程公用一张系统级页表,访问的都是同一个操作系统的数据和代码
用户态和内核态本质区别:权限不同
可以将上图简化
sigaction
第一个参数 signum
需要捕获的信号
第二个参数 act
替换操作系统中的struct sigaction 结构体
第三个参数oldact
接收操作系统中的struct sigaction 结构体
对于struct sigaction 我们只需要认识它第一和第三个成员变量,其余设为0
第一个成员变量 sa_handler
singal函数中的handler,一个函数指针,指向我们的自定义方法
第三个成员变量 sa_mask
信号集,我们在处理比如2号信号的同时,会自动阻塞2号信号。在处理2号信号的同时,我们可能还想屏蔽3号、5号信号等,我们可以将3,5等标志位传给信号集,信号集再传给sigaction函数对这些信号进行屏蔽
使用这个函数编译时需要添加 -D_GNU_SOURCE 选项
gcc -D_GNU_SOURCE -o $@ $^ -std=c99
小测试1:测试信号处理时对其他信号的阻塞作用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void handler(int signum) //自定义方法
{
while (1){
printf("get a signal %d, process's pid = %d\n", signum, getpid());
sleep(1);
}
}
int main()
{
struct sigaction act; //创建自己的struct sigaction 变量
memset(&act, sizeof(act), 0); //将变量内部都初始化成0
sigset_t mask; //自己的block表
sigemptyset(&mask); //清空block表
sigaddset(&mask, 3); //将三号信号假如block表中
//act.sa_handler = SIG_IGN;
act.sa_handler = handler; //将自定义方法函数指针传递给act
act.sa_mask = mask; //将block表传递给act
sigaction(2, &act, NULL); //调用sigaction函数
while (1){
sleep(5);
printf("I'm running\n");
}
return 0;
}
测试现象
信号2 的处理过程中,信号3收到阻塞无法递达
小测试2:测试信号处理结束,阻塞结束后的情况
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
int count = 0;
while (++count <= 10){ //增加计时器,信号2 的处理时间为10s
printf("get a signal %d, process's pid = %d\n", signum, getpid());
sleep(1);
}
}
int main()
{
struct sigaction act;
memset(&act, sizeof(act), 0);
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, 3);
//act.sa_handler = SIG_IGN;
act.sa_handler = handler;
act.sa_mask = mask;
sigaction(2, &act, NULL);
while (1){
sleep(5);
printf("I'm running\n");
}
return 0;
}
测试现象
信号2处理过程中发送3号信号,信号3阻塞无法递达。信号2 处理接收,阻塞消失,信号3 递达。
普通信号的标记使用的是位图结构,所以在阻塞过程中,不管发送几个信号在阻塞结束后都只执行一次,后pending位会被置0.
实时信号使用的是链表,每接受一次就增加一个结点。两种信号的处理方式不同是由其数据结构决定的
可重入函数
以上场景包含了两个执行流 1.main执行流 2.信号捕捉执行流。两个执行流的存在让insert函数被重复进入引发了错误。
某个函数一旦重入,有可能出现问题 – 该函数不可被重入 不可重入函数
某个函数一旦重入, 不会出现问题 – 该函数可以被重入 可重入函数
volatile关键字
小测试:编译器优化
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int flag = 0;
void handler(int signum)
{
printf("chang flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(3, handler);
while (!flag);
return 0;
}
1.运行程序,使用键盘发送2号信号程序打印chang flag 0 to 1后运行结束。
2.然后我们不修改代码,去Makefile中编译代码处加上-O3选项,进行一定程度的编译器优化
3.再次运行程序,使用键盘不断发送2号信号,程序不断打印chang flag 0 to 1 但是并未终止
导致这样的原因是,cpu和内存的硬件对数据的处理速度存在较大差异,在逻辑判断语句中flag并未被修改,编译器为优化程序的运行效率,就将flag的数值存在缓存中或者寄存器等更快的(距离CPU更近)硬件中,以提高效率。所以信号处理语句中修改的是内存中的flag,但优化后的程序执行逻辑语句cpu并不从内存读取flag了,就导致了上面的现象
解决方案
我们可以给全局变量flag 添加关键字volatile。
volatile的作用
保持内存可见性,不要对我的这个变量做任何优化,读取必须贯穿式读取内存,不要读取中间缓存区,寄存器内部数据
SIGCHLD信号
子进程退出会向父进程发送SIGCHLD信号,该信号的默认处理时忽略,所以之前实验进行waitpid接收子进程好像并未收到信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
if (fork() == 0){
int count = 5;
while (count--){
printf("I'm child, mypid = %d\n", getpid());
sleep(1);
}
exit(0);
}
//显式设置忽略17号信号,当进程退出后,自动释放僵尸进程 (只在linux下有效)
signal(SIGCHLD, SIG_IGN);
while (1);
printf("father end\n");
return 0;
}
显式设置忽略17号信号,当进程退出后,自动释放僵尸进程 (只在linux下有效)
void handler(int signum){
pid_t id = 0;
while ((id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success\n");
}
}
还可以自定义捕获信号,并在其中进行非阻塞式等待。使用while循环可以接收同时结束的进程发送来的数据。使用非阻塞等待可以在没有进程需要读取的时候父进程自己做自己的事情