1、什么是信号?
2、查看信号列表
3、信号捕捉
4、信号产生的5种方式
5、介绍CoreDump
6、信号处理的方式
7、如何理解信号产生到处理的过程
8、sigpending、sigprocmask、sigaction函数的使用
9、信号处理的时机
10、SIGCHLD信号
11、可重入函数
生活中的信号:红绿灯、下课铃声、闹钟铃声等等。
当信号出现的时候,我们之前就知道信号该如何处理,并且不被要求立即处理该信号,因为有时我们正在处理重要的事情。(类比闹钟铃声)
技术应用角度的信号:当进程执行除0代码的时候是如何被操作系统终止的?本质是因为进程收到了信号,在合适的时机处理该信号的时候被操作系统终止。
如何查看Linux中信号列表?
使用命令:kill -l
需要知道的是没有32、33号信号,总共就62个信号。其中1-31是普通信号,34-64是实时信号。
1、什么是信号捕捉?
简单来说就是改变信号的默认处理方式
介绍signal函数
功能:对特定信号进行捕捉
使用方式:signal(要捕捉的信号编号,要执行的处理方式)
1、代码异常(如:除零错误、对野指针解引用)
2、命令行产生(如:kill -信号编号 pid、killall -信号编号 进程名)
3、键盘组合键产生(如:ctrl + c、ctrl + z、ctrl + \ ,分别对应2,20,3号信号)
4、系统调用
5、软件条件
代码演示
1、异常
#include #include #include using namespace std; int main() { int cnt = 5; while(true) { cout<<"I am a process"< if(cnt == 0) { int num = 3/0;//除零错误 } cnt--; sleep(1); } return 0; }错误信息和信号列表对比大概可以看出除零错误属于8号信号
用signal函数对8号信号进行捕捉,执行我们的自定义的处理方式
#include #include #include using namespace std; void handler(int signo) { cout<<"收到了8号信号"< while(1) { cout<<"I am a process"< sleep(1); } } int main() { signal(8,handler); int cnt = 5; while(true) { cout<<"I am a process"< if(cnt == 0) { int num = 3/0; } cnt--; sleep(1); } return 0; }
2、命令行产生
①kill -信号编号 pid
#include #include #include using namespace std; int main() { int cnt = 5; while(true) { cout<<"I am a process"< sleep(1); } return 0; }②killall -信号编号 进程名
#include #include #include using namespace std; int main() { int cnt = 5; while(true) { cout<<"I am a process"< sleep(1); } return 0; }3、键盘组合键产生
①ctrl + c
②ctrl + z(强制当前进程转为后台,并使之停止)
jobs可以显示后台进程,fg + 后台编号可以将进程从后台放置到前台运行。
./test是前台运行该进程,./test &代表的是后台运行该进程
在后台运行进程时,解释器还能够解释命令,但由于都向同一个显示器打印,会造成互相干扰的情况,键盘组合键只能对前台进程有效,后台进程无效。
要想杀掉后台进程有两种方式:1、先fg 1 再 ctrl +c 2、kill -2 pid
③ctrl + \
如何知道ctrl + c、ctrl + z、ctrl + \分别给进程发的是什么信号?
答:对所有信号捕捉,然后再发送信号,信号捕捉函数就可以打印发送的信号。
#include #include #include #include using namespace std; void handler(int signo) { cout<<"进程收到了"<"号信号"< sleep(1); } int main() { for(int i = 1; i <= 31; i++) { signal(i,handler); } while(true) { cout<<"I am a process pid:"<<getpid()< sleep(1); } return 0; }ctrl + c:向进程发送2号信号
ctrl + z:向进程发送20号信号
ctrl + \:向进程发送3号信号(注:有的朋友可能ctrl + \没有反映,具体原因可以百度解决)
上述代码,还证明了并不上所有信号都能够被捕捉,比如9号信号就不能被捕捉,因为所有信号都能够被捕捉,那么这个进程真就“刀枪不入”了,连操作系统也拿它没办法了。
4、系统调用产生
①kill
功能:向任意进程发送任意信号
写一个kill命令
#include #include #include using namespace std; int main(int argc,char** argv) { if(argc!=3) { cout<<"Usage: kill -信号编号 pid"< return 1; } int signo = argv[1][1] - '0'; //cout< int pid = atoi(argv[2]); //cout< kill(pid,signo); return 0; }②raise
功能:向当前进程发送任意信号
③abort
功能:向当前进程发送abort信号
这个函数有点特殊,因为你就算捕捉了该信号还是会终止进程。
5、软件条件产生
1、管道的读端关闭,写端会收到SIGPIPE信号,处理该信号时被操作系统杀掉。
common.h
#include #include #include #include #include #include #include #include #include #define PATH "./.fifo" using namespace std;server.cc
#include "common.h" int main() { umask(0); if(mkfifo(PATH,0666)!=0) { cerr<<"mkfifo error"< return 1; } int fd = open(PATH,O_RDONLY); if(fd < 0) { cerr<<"open error"< return 2; } #define SIZE 1024 char buf[1024] = {0}; while(true) { ssize_t s = read(fd,buf,sizeof(buf)-1); if(s==0) { cout<<"写端关闭"< unlink(".fifo"); close(fd); exit(0); } else if(s>0) { buf[s] = '\0'; cout<<"client->server: "< } else { cerr<<"read error"< close(fd); exit(3); } } return 0; }client.cc
#include "common.h" void handler(int signo) { cout<<"写端收到了"<"号信号"< exit(1); } int main() { signal(SIGPIPE,handler); int fd = open(PATH,O_WRONLY | O_APPEND); if(fd<0) { cerr<<"open error"< } while(true) { char msg[1024] = {0}; cout<<"请输入信息# "; fflush(stdout); if(fgets(msg,sizeof(char)*1024,stdin)!=nullptr) { //msg为 xxxx\n\0 //下面的write我只传了xxxx write(fd,msg,strlen(msg)-1); } else { exit(0); } } return 0; }2、alarm函数
功能:自定义多少秒之后向进程发送一个alarm信号,该信号的默认处理方式是终止进程。
测试1秒钟服务器能够对cnt++多少次
#include #include #include #include using namespace std; int cnt = 0; void handler(int signo) { cout< exit(1); } int main() { alarm(1); signal(SIGALRM,handler); while(true) { cnt++; } return 0; }05 介绍CoreDump
1、什么是CoreDump?(核心转储)
coredump是指当程序出错而异常中断时,OS会把程序工作的当前状态存储成一个coredump文件。然后我们可以通过该文件来定位异常的地方。
2、一般线上生产环境都不会自动打开CoreDump,需要我们手动打开。
ulimit -c 文件大小(一般1024的整数倍)
3、试验一下CoreDump
#include #include #include using namespace std; int main() { cout<<"my pid is "<<getpid()< int* p = nullptr; *p = 3; return 0; }发生段错误,生成了core.25647文件,这个25647是什么意思呢?
本质其实就是异常进程的pid
生成了core.25647,我们可以通过gdb来分析CoreDump文件,定位异常。
使用core-file core.26293可以导入coredump文件,然后gdb就可对该文件进行分析。
分析结果:进程终止是因为收到了11号信号,异常的地方是在test.cc文件的第10行。
06 信号处理的方式
1、默认处理方式
一般是终止该进程,可通过man 7 signal查看各个信号的默认处理方式
2、忽略
什么都不处理,只是将该信号从收到设置为未收到(ignore的缩写)
3、自定义
可通过signal函数或者sigaction函数自定义处理方式。
07 信号屏蔽字&&信号未决表&&信号处理函数
task_struct中存在三张表,分别是block表、pending表、handler表。
block是一个32位的位图结构,从上到下0代表该信号没被阻塞,1代表该信号被阻塞。
pedding也是一个32位的位图结构,从上到下0代表该信号没被收到,1代表该信号被收到。
handler是一个函数指针数组,从上到下依次为每个信号对应的处理方式,SIG_DFL代表默认处理方式,SIG_IGN代表忽略。
pedding表说白了就是记录进程收到了哪些信号。
block表为了防止信号被处理而设计的,当某个信号被阻塞后,该信号就是递达了它都不会被处理,直到它被解除阻塞才能够被处理。
解释一下,给进程test使用组合键ctrl + c产生信号,操作系统做了什么?
首先,ctrl + c产生的是2号信号,因此操作系统会修改test进程控制块中的pending位图,将2号信号由0置为1,然后在合适时机处理该信号,当合适时机到来时,进程test执行2号信号的处理函数,由于该信号的处理函数是默认处理方式,那么操作系统会将test进程改为z状态并释放部分资源,最后被父进程回收test进程。
08 sigpending、sigprocmask、sigaction函数的使用
1、sigprocmask
功能:可以读取或更改进程的信号屏蔽字
成功返回0,出错返回-1
其中how选项为SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK
set为输入型参数,设置新的阻塞信号集。
oldset为输出型参数,获取老的阻塞信号集。
在此之前需要介绍一下sigset_t 类型
sigset_t是一个32位的位图结构
对它进行操作的常用函数有:
①sigemptyset(sigset_t* set)
②sigfillset(sigset_t* set)
③sigaddset(sigset_t* set , signo)
④sigdelset(sigset_t* set , signo)
⑤sigismember(const sigset_t* set , signo)
现在我们使用sigprocmask函数对所有信号进行屏蔽
#include #include #include using namespace std; int main() { sigset_t new_block; sigset_t old_block; sigfillset(&new_block); sigemptyset(&old_block); sigprocmask(SIG_SETMASK,&new_block,&old_block); while(true) { cout<<"I am process"< sleep(1); } return 0; }可以发现,绝大多数信号可以被屏蔽,但9号信号是无法被屏蔽的
使用一下sigprocmask中老的信号集
#include #include #include using namespace std; int main() { sigset_t new_block; sigset_t old_block; sigfillset(&new_block); sigemptyset(&old_block); sigprocmask(SIG_SETMASK,&new_block,&old_block); int cnt = 0; while(true) { cout<<"I am process"< sleep(1); if(cnt == 5) { cout<<"信号屏蔽字已经恢复到原来"< sigprocmask(SIG_SETMASK,&old_block,nullptr); } cnt++; } return 0; }从上述现象可以看出,当有多个信号同时到来的时候,先处理信号编号小的。
2、sigpending
成功返回0,失败返回-1。
功能:读取当前进程的未决信号集
代码测试:
#include #include #include using namespace std; void ShowPending(const sigset_t& pending) { for(int i = 1; i<=31; i++) { if(sigismember(&pending,i)) { cout<<"1"; } else { cout<<"0"; } } cout< } int main() { sigset_t block; sigfillset(&block); sigprocmask(SIG_SETMASK,&block,nullptr);//阻塞所有信号 sigset_t pending; sigemptyset(&pending); while(true) { sigpending(&pending); ShowPending(pending); sleep(1); } return 0; }3、sigaction
成功返回0,失败返回-1
功能:可以读取和修改与指定信号相关联的处理动作
sa_handler:设置普通信号的处理方式
sa_sigaction:实时信号的处理方式,这里我们不管。
sa_mask:在信号处理时,设置额外需要屏蔽的信号
sa_flags:用来处理普通信号,设置为0就行。
sa_restirer:不管。
代码测试
#include #include #include using namespace std; void handler(int signo) { cout<<"收到"<"号信号"< } int main() { //signal(2,handler); struct sigaction act; act.sa_handler = handler; act.sa_flags = 0; sigaction(2,&act,nullptr);//相当于signal(2,handler); while(true) { cout<<"I am a process"< sleep(1); } return 0; }09 信号处理的时机
信号处理的时机:从内核态切换回用户态时会做信号检测,检测到有信号就会处理。
当检测到有信号要处理时,就要处理信号。
对默认处理方式是终止进程,修改pending位图,然后返回用户态。
对忽略处理方式是修改pending位图,返回返回用户态。
对自定义的处理方式则需返回到用户态模式下处理该自定义函数,然后在返回用户态,在进行信号检测,如果没有信号,则返回用户态继续进行向下执行。
1、为什么要返回用户态默认执行自定义函数?
因为自定义函数是用户写的,必须要防止用户冒用操作系统的身份执行非法行为。
2、处理完自定义函数,为什么不能直接返回用户态继续执行剩下的代码?
因为你需要返回到内核态完成返回工作,比如你需要恢复上下文或者系统调用的返回值,这些都需要更高的权限来做,必须返回到内核态再回到用户态。
10 SIGCHLD信号
子进程退出时会给父进程发送SIGCHLD信号,对该信号的默认处理方式是忽略。
#include #include #include #include #include using namespace std; void handler(int signo) { cout<<getpid()<<"收到了"<"号信号"< } int main() { signal(SIGCHLD,handler); if(fork() == 0) { int cnt = 0; while(true) { cout<<"I am a child pid: "<<getpid()< if(cnt == 5) { exit(1); } cnt++; sleep(1); } } else { while(true) { cout<<"I am a father pid: "<<getpid()< sleep(1); } } return 0; }但这种对SIGCHLD信号的处理方式在多个子进程同时退出则不能很好的处理。因为对于普通信号而言只有一个比特位来记录信号是否被收到(实时信号会被链表链接起来)。
多个子进程同时退出的现象
#include #include #include #include #include #include using namespace std; void handler(int signo) { cout<<getpid()<<"收到了"<"号信号"< waitpid(-1,nullptr,0); } int main() { signal(SIGCHLD,handler); for(int i = 0;i <= 5; i++) { if(fork() == 0) { int cnt = 0; while(true) { cout<<"I am a child pid: "<<getpid()< if(cnt == 5) { exit(1); } cnt++; sleep(1); } } } while(true) { cout<<"I am a father pid: "<<getpid()< sleep(1); } return 0; }这个代码的弊端:
①无法处理多个子进程同时退出的情况
②当子进程永远不退出时,则会一直阻塞住,因为waitpid使用的是0,代表阻塞等待。
改进后
#include #include #include #include #include #include using namespace std; void handler(int signo) { while(true) { int ret = waitpid(-1,nullptr,WNOHANG); if(ret > 0) { cout<<"等待成功->"< } else if(ret == 0) { cout<<"有的进程还未退出"< break; } else if(ret < 0) { cout<<"进程已全部退出"< break; } } } int main() { signal(SIGCHLD,handler); for(int i = 0;i <= 5; i++) { if(fork() == 0) { int cnt = 0; while(true) { cout<<"I am a child pid: "<<getpid()< if(cnt == 5) { exit(1); } cnt++; sleep(1); } } } while(true) { cout<<"I am a father pid: "<<getpid()< sleep(1); } return 0; }最常用的做法是忽略SIGCHLD信号,当它退出时自动被操作系统回收。
#include #include #include #include #include #include using namespace std; int main() { signal(SIGCHLD,SIG_IGN); for(int i = 0;i <= 5; i++) { if(fork() == 0) { int cnt = 0; while(true) { cout<<"I am a child pid: "<<getpid()< if(cnt == 5) { exit(1); } cnt++; sleep(1); } } } while(true) { cout<<"I am a father pid: "<<getpid()< sleep(1); } return 0; }有人或许有疑惑,SIGCHLD信号的默认处理方式不是忽略吗?你使用SIG_IGN处理方式不还是忽略吗?为啥手动写就能让子进程自动回收呢?
可以把这里当成一种特殊的情况,暂且只能记住了,或许之后会找到更好的答案。
11 可重入函数
要理解重入函数,先得理解重入的概念。
重入:一个执行流还未执行完毕,另一个执行就开始执行。
可重入函数:当执行流重入函数时,该函数不会出现不同的现象或者任何问题,则称这个函数是可重入的,否则是不可重入的。
大多数函数都是不可重入的,比如链表的头插函数。
所有的STL容器都是不可重入的,要想变得可重入得加锁保护,但加锁会损害STL容器的效率,这对以效率著称的STL是破坏性的,因此STL容器没有加锁。
- 相关阅读:
oracle 迁移PG 博客
【Azure 应用服务】应用代码需要客户端证书进行验证,部署到App Service后,如何配置让客户端携带证书呢?
神经网络 深度神经网络,深度神经网络应用实例
Python(2)数据类型
ES6 新增功能复盘梳理
微服务知识03
Red Hat Enterprise Linux RHEL 8.6 下载安装
《时代》专访ChatGPT之父:人工智能影响经济还需要很多年
反向迭代器------封装的力量
OpenStack与CloudStack
- 原文地址:https://blog.csdn.net/m0_62171658/article/details/128044492