目录
前提知道信号与信号量没有任何关系就比如老婆和老婆饼一样;
生活例子--提炼基本结论
1. 比如说平时定闹钟,我今晚十一点睡觉,睡觉之前定个八点的闹钟,那么在闹钟响了在客观上就是闹钟给我发了个信号;
2. 平时我们过红绿灯,红灯亮了,红黄灯就给我发出一个信号那么此时我就应该等一等 ,绿灯亮了我就可以走了;
3. 在古代我是一个大头兵,远远望见狼烟升起,那么就知道有人要冲出来了就知道要打仗了;
4. 在家里呢,考试没考好,回家的时候发现爸妈往沙发上一座并且脸色阴沉给你传递了一个不好的信号,你此时就知道完蛋了;
1. 信号在生活中随时可以产生
你也不知道哪一天被分手了等叫做 信号的产生和我是:异步的
2. 你能认识这个信号(红绿灯,狼烟点起,脸色等)
3. 我们知道信号产生了,也知道信号触发了该怎么处理(红绿灯亮了我们知道该怎么办)
我们把2和3叫做 你能识别并处理这个信号
4. 我们可能正在做着更重要的事情,只能把到来的信号暂不处理
(就比如我点了个外卖,点好后我在打游戏,突然外卖小哥电话打来了,我就意识到了外卖到了。说明了我能认识外卖小哥电话这个信号,外卖小哥电话产生了我也知道该怎么处理下去拿外卖,但我此时正在做其他的事,那我可能说放楼下或者不管电话)
注意将上述的我换成进程即可;进程识别,进程处理,进程暂不处理等
信号概念的基本储备
信号:linux系统提供的一种向指定进程发送特定事件的方式;
比如说我要向进程发送9号信号表明要终止进程,比如说要发送19号要暂停一个信号等;
进程收到对应的信号后要对信号做识别和处理;
信号产生是异步的:信号的到达不依赖于进程的执行状态,可能在进程执行的任何时刻被接收。
信号处理
信号的默认动作:
进行自定义处理,不想执行默认动作,想让进程收到信号后执行我设定的方法:
#include <iostream> #include <signal.h> #include <unistd.h> #include <sys/types.h> void hander(int sig) { // 这个sig就是传递过来的信号编号 std::cout << "get a sig: " << sig << std::endl; } int main() { // 对信号的自定义捕捉,我们只要捕捉一次,后续一直有效 signal(2, hander); // 对2号信号自定义,名字hander随便起//将来收到2号信号后,未来2号信号将以参数形式传递给hander方法 //如果一直不产生2号信号,那么hander就永远不会被调用 //我们也可以对更多的信号进行捕捉 signal(3, hander); signal(4, hander); signal(5, hander); while (true) { std::cout << "hello nihao,pid: " <<getpid()<< std::endl; sleep(1); } return 0; }
注意:2 SIGINT代表是终止进程,我们平常用的ctrl+c-----给目标进程发送2号信号,所以使用ctrl+c在命令行中使用直接结束进程
注意:3 SIGQUIT代表是退出进程,我们平常用的ctrl+\-----给目标进程发送3号信号,所以使用ctrl+\在命令行中使用直接退出进程
信号产生
1. 通过kill命令,向指定的进程发送特定的信号
2. 键盘可以产生信号!比如ctrl+c或者ctrl+\
用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 . 前台进程因为收到信号,进而引起进程退出;
Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步
3. 系统调用
int kill(pid_t pid, int sig);
kill(getpid(),xxx)==int raise(int sig);给自己发信号,但raise不如kill重要
void abort(void);表示用于立即异常终止当前程序的执行。。abort() 函数会导致程序异常终止。它通常用于在遇到无法恢复的错误时强制停止程序,并产生一个 SIGABRT 信号。
无论用户使用kill命令还是使用键盘打命令还是用户程序调用,但最终发送信号的还是os。
因为发送信号的本质就是修改pcb的位图,只有os才有资格修改;
如果我把所有信号都捕捉了呢?
4. 软件条件
就比如管道的时候,读端关闭而写端一直写---就会发送13 SIGPIPE。在软件层面上因为某些条件不满足,向你的进程发送信号;
这时候介绍alarm函数:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main(){
int cnt=1;
alarm(1);//设定一秒的闹钟 一秒后收到SIGALRM信号
while(true){
std::cout<<"cnt: "<<cnt<<std::endl;
cnt++;
}
return 0;
}
验证IO很慢:
只最后进行IO,不用循环里每次都IO,减少IO次数;
理解闹钟:
就比如电脑关机了两天,断电断网,但是两天后把电脑打开后右下角的时间还是准确的。
这是因为我们的电脑,在主板上还有一个东西叫纽扣电池,很小,但他一直在维持电脑的时间的,虽然我们关机了,但是电脑中依旧有个电池有电。
闹钟的返回值:
闹钟设置一次就默认触发一次;
5. 异常
除0崩溃:
#include <iostream> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> void hander(int sig) { std::cout << "get a sig: " << sig << std::endl; } int main() { signal(8,hander); while (true) { std::cout << "pid: " << getpid() << std::endl; sleep(2); int a = 10; a /= 0; // int *p=nullptr; // *p=100; } }
发现循环打印
一直在打印,捕捉到了,说明就是发送了信号。通过man 7 signal查到8号默认终止
野指针崩溃:
#include <iostream> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> void hander(int sig) { std::cout << "get a sig: " << sig << std::endl; } int main() { signal(11, hander); int *p = nullptr; *p = 100; while (true) { std::cout << "pid: " << getpid() << std::endl; sleep(2); // int a = 10; // a /= 0; } }
发现也是循环打印,但是没在循环里面也循环打印
通过man 7 signal查到11号默认终止
崩溃了为什么会退出?因为发送的信号默认是终止进程;
可以不退出么?可以,捕捉了异常,但不建议,推荐终止进程;
程序上出现异常最终会体现在硬件上,硬件上的问题又会被os识别到;
Core Dump:
首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024
core是协助debug的文件!
信号保存
信号的其他概念
我们把实际执行信号的处理动作称为信号递达(Delivery):(一个信号被处理就叫做信号的递达)默认,忽略,自定义三种行为;
信号从产生到递达之间的状态,称为信号未决(Pending)。(我们都知道一个信号在产生之后到递达之前一定要临时在pcb里保存,所以信号的状态产生了但没处理他,临时在pcb里保存,叫信号未决)
进程可以选择阻塞 (Block )某个信号。 (如果一个信号处于未决状态还没有被递达,那么在后面合适的时候当前信号就会被递达,那有时候我们也可以选择阻塞某个信号那么此时这个信号永不递达一直未决除非主动解除阻塞)
但是一个信号阻塞和他有没有未决是没有关系的!
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
注意:上述操作都是os内部的。
sigset_t
sigset_t :用户层面的操作
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
sigprocmask:调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。
sigpending:读取当前进程的未决信号集,通过set参数传出。
阻塞效果:#include <iostream> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> void printfpending(sigset_t &pending) { std::cout<<"current process pending:"; for (int signo = 1; signo <= 31; signo++) { // 不管怎么样你的信号一直都是1-31 if (sigismember(&pending, signo)) // 看signo号信号是否存在; signo是要检查的信号的编号,通常是一个整型值; { std::cout<<1; } else{ std::cout<<0; } } std::cout<<"\n"; } int main() { // 1.屏蔽二号信号 sigset_t block_set, old_set; // 两个信号集 sigemptyset(&block_set); sigemptyset(&old_set); // 将两个信号集清空 sigaddset(&block_set, SIGINT);//添加二号信号,我们并没有修改内核的block表,因为我们只是在用户栈上进行定义和清空的两个set // 1.1设置进入进程的block表中 sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽! while (true) { // 2.获取当前进程的pending信号集 sigset_t pending; // 自己在用户栈上定义的类型 sigpending(&pending); // 获取,用户已经拿到这个位图了 // 3.打印pending信号集 printfpending(pending); sleep(1); } return 0; }
解除屏蔽:
#include <iostream> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> void printfpending(sigset_t &pending) { std::cout<<"current process pending:"; for (int signo = 1; signo <= 31; signo++) { // 不管怎么样你的信号一直都是1-31 if (sigismember(&pending, signo)) // 看signo号信号是否存在; signo是要检查的信号的编号,通常是一个整型值; { std::cout<<1; } else{ std::cout<<0; } } std::cout<<"\n"; } void handler(int signo){ std::cout<<signo<<" 号信号被递达!"<<std::endl; } int main() { //0.捕捉2号信号 signal(2,handler); // 1.屏蔽二号信号 sigset_t block_set, old_set; // 两个信号集 sigemptyset(&block_set); sigemptyset(&old_set); // 将两个信号集清空 sigaddset(&block_set, SIGINT);//添加二号信号,我们并没有修改内核的block表,因为我们只是在用户栈上进行定义和清空的两个set // 1.1设置进入进程的block表中 sigprocmask(SIG_SETMASK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽! int cnt=10; while (true) { // 2.获取当前进程的pending信号集 sigset_t pending; // 自己在用户栈上定义的类型 sigpending(&pending); // 获取,用户已经拿到这个位图了 cnt--; // 3.打印pending信号集 printfpending(pending); if(cnt==0){ std::cout<<"解除对2号信号的屏蔽"<<std::endl; sigprocmask(SIG_SETMASK, &old_set, &block_set); } sleep(1); } return 0; }
解除屏蔽,一般会立即处理当前被解除的信号(如果被pending了);
解除屏蔽后pending位图对应的信号也要被清零,在递达之前就被清零了;
信号处理
处理信号就是递达信号!
捕捉信号

内核态vs用户态
再谈地址空间
谈谈键盘输入数据的过程
谈谈如何理解os如何正常的运行
其他捕捉方法
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
//当前如果正在对2号信号进程处理,默认2号信号会被自动屏蔽
//对2号信号处理完成的时候,会自动解除2号信号的屏蔽
void printfpending(sigset_t &pending)
{
std::cout<<"current process pending:";
for (int signo = 1; signo <= 31; signo++)
{ // 不管怎么样你的信号一直都是1-31
if (sigismember(&pending, signo)) // 看signo号信号是否存在; signo是要检查的信号的编号,通常是一个整型值;
{
std::cout<<1;
}
else{
std::cout<<0;
}
}
std::cout<<"\n";
}
void handler(int signum){
std::cout<<"get a sig: "<<signum<<std::endl;
while(true){
sigset_t pending;
sigpending(&pending);
printfpending(pending);
sleep(1);
}
exit(1);
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);//如果你还想处理2号信号(os对2号自动屏蔽),同时对其他信号也进行屏蔽,那么可以添加到sa_mask中
sigaddset(&act.sa_mask,3);//同时对3号也进行屏蔽,那么ctrl+\就不管用了
act.sa_flags=0;//暂时置为0.其他俩字段不管
sigaction(2,&act,&oact);
while(true){
std::cout<<"i am a proce ,pid: "<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
可重入函数 

volatile
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
volatile int flag=0;
void handler(int signo){
std::cout<<"get a signo: "<<signo<<",change flag:0->1 "<<std::endl;
flag=1;
}
int main(){//main函数里没有任何代码对flag进行修改
signal(2,handler);
while(!flag);//while不要其他代码
std::cout<<"process quit normal "<<std::endl;
}
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作
SIGCHLD
子进程退出时,不是悄悄退出的,而是会给父进程发送信号--SIGCHLD
那么如果十个子进程都同时退呢?
同时退说明都会向父进程发送SIGCHLD,我们的信号是普通信号,是用pending位图来记录收到的信号的,如果有10个子进程同时退出了那么你就会收到10个SIGCHLD,但你的pending只会记录一次,所以如果这样写的话,最后只会回收一个子进程;
所以用循环等待解决
#include <iostream> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> void handler(int signo) { std::cout << "get a signo:" << signo << " pid: " << getpid() << std::endl; while (true) { // 循环一次回收一个 pid_t rid = waitpid(-1, nullptr, WNOHANG); if (rid > 0) { std::cout << "wait succes,rid:" << rid << std::endl; } else if (rid < 0) { std::cout << "wait done" << std::endl; break; } else { std::cout << "wait done" << std::endl; break; } } } void othersing() { std::cout << "other sing" << std::endl; } int main() { signal(SIGCHLD, handler); pid_t s = fork(); if (s == 0) { std::cout << "i am child aprocess,pid :" << getpid() << " ,ppid: " << getppid() << std::endl; sleep(3); exit(1); } // father while (true) { othersing(); sleep(1); } return 0; }
如果一共有10个子进程,5个推出,5个永远不退呢?
那么就会导致阻塞,导致无法返回main函数,那么此时就需要非阻塞等待
事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
#include <iostream> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { signal(SIGCHLD, SIG_IGN); // 父进程收到后,进行忽略 pid_t s = fork(); if (s == 0) { int cnt = 5; while (cnt--) { std::cout << "i am child aprocess,pid :" << getpid() << " ,ppid: " << getppid() << std::endl; sleep(1); } exit(1); } // father while (true) { std::cout << "i am father aprocess,pid :" << getpid() << " ,ppid: " << getppid() << std::endl; sleep(1); } return 0; }
等待有俩目的:获取子进程的退出信息 回收子进程
如果不关心子进程的死活那么可以用替换成忽略,父进程就可以安安心心做自己的事情。