红绿灯,现在眼前没有红绿灯,但我们知道红灯停,绿灯行。幼儿园老师在我们的头脑里注册了这一个方法。
狼烟,狼烟虽然没有点燃,但一经点燃,官兵知道该怎么做。
闹钟,闹钟没响,但早上响起,我们就要起床。
下课,下课铃没响,但是铃声一响,我们可以出教室。
信号就是事件发生的一种通知机制。注意,进程通信是传输数据,而信号是通知事件给进程,但是可以以看做通信的一种方式。
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("running\n");
8 sleep(1);
9 }
10 return 0;
11 }
当这个前台进程在运行的时候,输入什么命令都没反应
在一个终端,只允许有一个前台进程。当myfile在前台运行的时候,bash作为后台进程接收不到其他命令。
那么将它变成后台进程。./myfile &
,bash此时在前台
在这期间,其他命令可以运行,但是ctrl + c关不了myfile这个进程了
用上节学到的进程间通信是可以解释的,无论是后台进程myfile,还是bash输入的命令。都在显示器显示,显示器是文件,它也是一种临界资源,没有他加锁所以显示到显示器就乱打。
注意:
kill-l查看信号
一共62个信号,前31个叫做普通信号,后31个叫做实时信号。
SIGINT:就是ctrl+c,
SIGQUIT:就是ctrl+\ ,会生成一个core文件
SIGPIPE:读端关闭读,写端还在写就会发送sigpipe信号
生活中,当你接到快递的电话并不是立刻去取快递,而是把手头的事情做完,再去取,而中间这个过程,取快递这个信号被我们保存在脑海里,所以得出结论,信号产生之后并不是被立即处理的。
那么就可以从这三个方面去研究信号。
信号产生的方式。
信号保存的方式。
信号处理的方式。
ctrl+c,ctrl+/
等
kill是系统调用,向进程发送信号
形参是进程的pid,要发几号信号
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<signal.h>
6 int main(int argc,char* argv[])
7 {
8 if(argc==3)
9 {
10 //字符串转换成整形,不是强转,不是强转,强转是不改变底层结构的
11 kill(atoi(argv[1]),atoi(argv[2]));
12 }
13 return 0;
14 }
让sleep 100变成后台进程,然后调用我们写的程序杀掉他。
运行起来会杀掉当前进程。
但是值得注意的是,他不能被捕捉。这个代码利用了signal系统调用,捕捉信号。而面对9号信号,无法捕捉。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<signal.h>
6
7 void handle(int singno)
8 {
9 printf("catch singno:%d\n",singno);
10 }
11 int main()
12 {
13 signal(9,handle);
14 raise(9);
15 return 0;
16 }
依旧是被killed
其实仔细想想也能理解,万一这个进程是病毒呢,假如全部信号被捕捉,运行起来没人管得了他了。
捕捉2,是可以的
他是一个执行6号信号的函数,需要注意的是即使我们捕捉他,他还是会在之后运行成功
这三个是递进关系,kill是任意进程,任意信号。raise是任意信号。abort是6号信号
向管道,假如读端关闭掉文件描述符,写端是可能会被直接杀掉,用的信号就是sigpipe
当某种条件满足,产生的信号
模拟一个野指针,会报出段错误
int *p=NULL;
*p=2;
经查看是11号信号。野指针是怎么被发现呢?
进程里有指针操作,指针里面存放着虚拟地址,解引用访问数据,虚拟地址转换为物理地址,页表内不存在,或者标志位不一致,而mmu硬件转换出现错误,操作系统识别到错误。
野指针(11号信号)
数组越界
除零(8号信号)
溢出等:cpu状态寄存器,溢出会存储溢出状态
硬件产生,操作系统把组合键解释成信号发给进程。
系统调用,操作系统提供的
软件条件,时机成熟,提醒操作系统发出。
硬件条件,操作系统识别。
所有信号都经过操作系统之手完成。因为操作系统是进程的管理者。这四种都是信号产生的触发条件。
当进程异常终止的时候,操作系统将你的一些重要信息转储到硬盘当中生成一个coredump文件,可以通过查看coredump文件来进行查看错误信息。
但是默认不生成
需要我们修改core file size,
通过ulimit -s 修改生成coredump文件的大小
以debug方式生成程序,为了方便调试
通过3号信号来终止这个进程
通过gdb调试可以看出,调试信息给出通过3号信号终止了它
所以实际上,这是一种事后调试,进程崩溃之后才进行定位,那为什么需要默认关闭呢,因为默认生成一个core文件,这个临时文件还是比较大的,例如当一个公司的服务器挂掉,首要的问题不是找出原因为什么挂掉,而是先恢复启动,再找出错误所在。假如不断地挂掉,那么就会不断生成core文件。
在之前进程等待时,waitpid方法的第二个参数,status是一个输出性参数,调用此方法,等待的进程正常退出,错误退出,检测这个整数st16位中的高8位,进程异常退出,通过低7位来检测进程异常退出时,操作系统所发出的信号。而第8位就保存着当前进程是否需要coredump生成core文件。(因为有些信号是不需要要你生成core文件的例如kill -9)。
操作系统发出信号,进程不是立即执行信号所对应操作,那么就需要将这个信号保存起来,而位图正是保存信号的方式。也就是说一个进程结构体里一定有一个位图来存储信号,默认全0。
task_struct
{
unsigned int map=0;
}
对于普通信号,比如当操作系统发2号信号,就对应把这个位图中第2个比特位改成1。所以操作系统发信号,不如把他说成写信号更加合适。操作系统的任务结束,接下来是进程该如何处理。所以操作系统写信号,进程不是立即执行的,由进程自己决定什么时候操作。
看几组概念
前两个表是位图结构,第几个比特位代表第几个信号,比特位为1代表存在,为0代表不存在
Pending表:是否收到信号,收到几号信号
Block表:几号信号,无论是否收到,我就要阻塞他(阻塞可以被取消)
handler表是数组结构,指针数组。
handler表:里面存放着一个个函数指针,DFL默认操作,IGN忽略
所以之前用的signal(信号,函数指针)。下标代表几号信号,执行什么动作就把对应的函数地址写进handler表这个指针数组当中。
注意:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次
或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,相当于我们自己定义的变量,用作输出型参数,与后面的函数相互配合,这个类型可以表示每个信号
的“有效(1)”或“无效(0)”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量。
int sigemptyset(sigset_t *set);
把比特位全部清空
int sigfillset(sigset_t *set);
把比特位设置成全1
int sigaddset (sigset_t *set, int signo);
把对应信号集中信号的比特位由0变1
int sigdelset(sigset_t *set, int signo);
由1变成0
int sigismember(const sigset_t *set, int signo);
查看信号,在信号集中对应的比特位是否为1
先定义一个信号集变量,这几个都是对我们定义的那个信号集进行操作(无论是block或pending),pcb中对应的信号集,是在调用了sigprocmask或sigpending之后才开始改变
通俗点来说那张block位图就叫做信号屏蔽字。
参数解释:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
mask|set 按位与 ,00 | 01 = 0 1,就把第二个信号添加了
mask&~set 希望删除第二个,0111 & ~(0100)= 0011,就删除了
覆盖
int sigpending(sigset_t *set);
sigset_t pending;
sigemptyset(&pending);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
下面来练习一下这几个函数。
block表中阻塞2号信号,打印pending表(没有传入全0),键盘传入2号信号(到pending表),但由于被阻塞,所以就一直在pending表中不会被递达,打印pending表(2号信号比特位为1)。10次之后,解除屏蔽。由于2号信号递达,进程终止
所以看到的结果是,先是全0,前四秒没动,当我发送2号信号,pending表中2号比特位变成1,后6秒打印出来,第10s时解除屏蔽,递达,默认动作是终止进程。
递达完之后,pending表2号比特位肯定又变成0了,但是默认动作终止了进程,我们也看不到。
不过我们可以修改抵达行为,自定义捕捉一下,让他不要终止。
也就是说现象是,与之前大致一样,不过当键盘输入2号信号时,立马被捕捉,由于进程没有终止,循环继续,打印出来是全0.
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void show_pending(sigset_t *pending)
5 {
6 int i=1;
7 //只阻塞了2号信号
8 for(;i<=31;i++)
9 {
10 //i=2时,判断2号信号是否在pending表中
11 if(sigismember(pending,i))
12 {
13 printf("1 ");
14 }
15 else{
16 printf("0 ");
17 }
18 }
19 printf("\n");
20 }
21 void handler(int sig)
22 {
23 printf("%d signal was catched\n",sig);
24 }
25 int main()
26 {
27
28 signal(2,handler);
29 sigset_t block,o_block;
30 //初始化
31 sigemptyset(&block);
32 sigemptyset(&o_block);
33 //向我们的变量添加2号信号被阻塞
34 sigaddset(&block,2);
35 //把我们设置好的传给pcb中,pcb为改变之前给到o_block
36 sigprocmask(SIG_SETMASK,&block,&o_block);
37 int count=0;
38 //不断获取pending表,键盘输入2号信号,过10秒之后解除屏蔽,进程就结束了
39 while(1)
40 {
41 //初始化
42 sigset_t pending;
43 sigemptyset(&pending);
44 //pcb中的pending,传给我们的变量
45 sigpending(&pending);
46 //先输出全0,当在这10s内通过键盘输入2号信号,2号信号比特位输出1,10s到达进程停止
47 show_pending(&pending);
48 sleep(1);
49 count++;
50 if(count==10)
51 {
52 printf("the process will destory\n");
53 //把他以前的经过初始化全0的block表给他,代表10s过后此时没有阻塞
54 sigprocmask(SIG_SETMASK,&o_block,NULL);
55 }
56 }
57 return 0;
58 }
阅读源码可以看到,block表与pending表,handler表(sighand)
由于有普通信号和实时信号,所以他们两个要区分开
action就是一个指针数组,里面存储这函数指针
之间讲过,信号不是被立即处理的,而是在合适的时候,这个合适的时候就是内核态切换到用户态的时候。我们的程序是有可能在用户态在内核态之间互相切换的。
这句话怎么理解呢?假如在程序中定义一个或者修改一个变量,是单纯的用户态。一旦当你printf()这个数据,就必定要调用系统调用接口write,传入文件描述符1,来打印到屏幕,这个write是系统调用,是操作系统帮我们做的,所以这就叫内核态。我们大多数写的代码都是由用户态和内核态组成,所以进程在运行的时候也是内核态,用户态互相切换。
由于大多数进程的代码和数据都不一样,所以每个进程的用户级页表和物理内存映射关系也不一样,但是操作系统的代码在物理内存只有一份。cpu是怎么区分你现在是内核还是用户呢
当你进行系统调用时,每个进程都有自己的内核空间,进程瞬间陷入内核(相当于顶了一个进程壳子的操作系统),寄存器cr会识别到你现在是用户还是内核。
signum代表几号信号。
const struct sigaction *act代表要执行的动作
struct sigaction *oldact代表之前的执行动作
第二个和第三个参数的类型,struct sigaction这个结构体里面包含
第二个和第五个是处理实施信号。
第一个是我们处理的动作(函数指针)
第三个是信号集,当你在进行处理对应信号时,把这个信号阻塞掉,防止处理的时候被再次调用产生干扰
练习一下。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 void handler(int sal)
5 {
6 printf("handling %d\n",sal);
7 }
8 int main()
9 {
10 struct sigaction act,o_cat;
11 act.sa_handler=handler;
12 act.sa_flags=0;
13 sigemptyset(&act.sa_mask);
14
15 sigaction(2,&act,&o_cat);
16 while(1);
17 return 0;
18 }