Linux操作系统学习之---进程信号的产生和保存
一个辨析 :
[!信号和信号量 , 一字之差 , 有啥联系吗???]
- 就如同老婆和老婆饼 , 没有半毛钱关系!!!
- 信号量是在进程间通信中实现
互斥
和同步
机制的关键之一.- 信号则是操作系统用来终止程序的一个手段.
一.初步理解信号量:
信号的处理:
1.默认行为:
系统内定义有许多信号 , 其中大多数信号的默认行为都是终止进程
2.自定义行为:
sighandler_t signal(int signum, sighandler_t handler)
- 传入自定义的函数指针 , 来修改信号signum的默认行为
- 这种操作也叫做``信号捕捉
自定义捕捉2号信号:
下面对信号2
SIGINT
进行捕捉 , 让他原有的停止进程行为变成打印信息
#include<iostream>#include<unistd.h>#include<signal.h>using namespace std;typedef void (*handler)(int signal);void func(int signal){cout << "收到信号:" << signal << endl;}int main(){sighandler_t s = signal(2,func);while(true){cout << "程序运行中" << endl;sleep(1);}return 0;}
运行起来会发现 , 按下Ctrl+c无法终止进程了,而是执行了我们自己func函数里的逻辑:
[!无法被捕捉的信号]-
- 如果让能让进程终止的信号全部被自定义捕捉 , 岂不是恶意软件可以放飞自我啦?
- 但道高一尺魔高一丈 , 操作系统在设计时就规定了
9号 SIGKILL
和19号 SIGSTOP
无法被捕获
#Linux/进程信号/两个东方不败的信号
无法捕捉的信号:
//自定义函数void handler(int single){cout << "信号: " << single << "被自定义捕捉啦!!!" << endl;}//验证逻辑for (int i = 1; i <= 31; i++){sighandler_t ret = signal(i, handler);if (ret == SIG_ERR) //如果捕获失败,判断成立cout << "信号 : " << i << "捕捉失败!!" << endl;}while (true){cout << "哈哈哈哈" << endl;sleep(1);}
3.忽略
- 这个就比较简单 , 一般是进程通过函数
sigignore
将处理方式设置为SIG_IGN
二.信号产生:
[!信号的产生方式]
- 信号产生方式有五中 : 终端按键 / 系统命令 / 函数调用 / 软件条件 / 硬件异常
1. 终端按键(键盘):
- 按下键盘上的Ctrl + c , 会向前台进程发送2号信号SIGINT, 终止前台进程.
- 按下键盘上的Ctrl + z , 会向前台进程发送20号信号SIGTSTP , 暂停前台进程.
- 按下键盘上的Ctrl + \ , 会向前台进程发送3号进程SIGQUIT , 退出当前进程.
[!前台进程的本质]-
键盘发送的信号只能由前台进程接受 , 所以要明晰什么是前台进程!!!
- 前台进程的本质是能够接受键盘输入!!!同一时间只有一个前台进程 . 登陆终端时,shell是前台进程 , 当运行自己的可执行程序 , shell退居二线 .
- 后台进程是不能接受键盘输入的!!! 同一时间可以有很多个后台进程 .
键盘是独占资源 , 一次只能有一个进程来接收他的信号 . 系统规定,这个接受者只能是前台进程.
2.系统命令:
常用的就是kill命令 , 使用kill -l可以看到所有信号对应的编号.
kill -9 [pid]
: 发送9信号SIGKILL
杀死进程kill -2 [pid]
: 发送2信号SIGINT
进程 , 等价于键盘输入Ctrl+c.
3.函数调用:
kill()
- int kill(pid_t pid, int sig)
- 调用时向任意进程发送指定信号sig
- 命令行里的kill指令的底层实现就涉及这个函数.
raise()
- int raise(int sig)
- 相比kill函数适用范围更窄 , 只能向当前进程发送信号
abort()
- [[noreturn]] void abort(void)
- 执行时直接退出当前进程,等价于raise(3),发送信号3
SIGQUIT
- 由于执行后直接退出进程 , 所以不存在返回值.
4.软件条件
匿名管道通信
使用匿名管道实现进程池时 , 如果想让多个读端关闭(子进程) , 只需关闭一个父进程的写端
因为操作系统从来不做浪费资源的事 ! 当父进程关闭写端 , 子进程作为读端再也不会受到消息 , 因此留着他们也是浪费资源 , 会被操作系统叫停.
进程属于软件 , 匿名管道属于软件(由文件系统实现,不和磁盘交互) , 操作系统也是软件 , 因此这是软件条件—管道读端关闭导致的信号!!!
alarm函数
- 函数声明
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值为剩下的时间
//参数表示倒计时
- 函数原理
- 计算机从开始运行就会存在一个不断增加的时间戳 , 可以理解为一个一直++的计数器 , 通过这个计数器可以算出当前的时间信息.
- 假设当前时间戳为2000s , 那调用一次alarm(10) , 则会在时间戳增加到2010s时向当前进程发送信号.
- 实践一 : 使用alarm函数
alarm函数发送的信号是14号SIGALRM , 默认行为是终止当前进程 , 这里将其自定义捕获
void sigcb(int single)
{cout << "信号 : " << single << "被捕捉啦\n"; alarm(1); //调用之后重置一秒的闹钟
}
int main()
{signal(SIGALRM,sigcb);alarm(1); //一秒后发送信号while(true){pause(); //让程序暂停,等待信号}
}
运行程序发现一秒钟后就开始不停的接受信号了
- 实践二 : 结合alarm和pause,感受IO效率之慢:
int main()
{//感受IO效率之慢alarm(1); //alarm1秒后发送信号,默认行为终止程序int count = 0;while(true){cout << "count = " << count++ << endl;//看看一秒内可以执行多少词cout}
}
[!测试思路]
- 全局变量count用来计数
- 定义一个一秒钟的闹钟alarm(1)
- while循环内的count在一秒内使劲++
- 当一秒结束 , alarm发送信号 , 执行自定义捕捉函数,打印count最终结果
int count = 0; //全局变量,用作计数
void sigcb(int single)
{cout << "信号 : " << single << "被捕捉啦\n"; cout << count << endl;exit(1);
}
int main()
{//感受没有IO的飞快signal(SIGALRM,sigcb);alarm(1);while(true){count++;}
}
运行后发现一秒钟count++执行了五亿多次 , 相比于刚才一秒里七万多次cout打印简直是降维打击!!!
5.硬件异常
下面是两种最常见的硬件异常 , 一个涉及寄存器,另一个涉及MMU.
除0异常 :
当程序中出现 x/0 这样的表达式时 , 运行起来会出现异常 , 其实和硬件
寄存器
有关
- 算术运算由CPU执行 , 当CPU执行 /0 操作时 , 会将错误信息以将EFLAGSS寄存器里的某个比特位置为 1 , 触发
硬件异常
- 随后由内核接管 , 检查到EFLAGS寄存器里的内容后向进程发送8号信号SIGFPE
- 最后让进程按情况处理信号.
非法指针操作异常:
当程序试图修改cosnt char* str 字符串时(写入只读也行为.rodata) , 也会出现异常 , 此时和硬件
MMU
(负责查询页表,完成虚拟到物理地址的转换)有关
- 程序通过指针进行写入操作(如*str = ‘1’) , CPU将虚拟地址传输给MMU.
- MMU执行虚拟地址到对应物理地址的转换(页表遍历), 发现目标地址映射到制度数据段
- MMU检测到非法写入 , 出发异常 .
- 随后内核接管 , 向进程发送11号进程SIGSEGV , 即段错误.
- 最后进程按情况处理信号
core dump:
想象一下 , 一个已经上线运行的项目出现了错误 , 能够立即关停项目让程序员修bug吗?
答案是不能 , 所以就需要将这些错误以文件的形式存储下来 . 方便事后调试.
core dump
就是用来标识是否有错误日志文件产生的.
**值得注意的是 , 一般在云服务器上 , 这个功能默认是关闭的 . **
- 原因: 如果在云服务器运行时有一个带有bug的程序不停的出错/重启/崩溃 , 产生的错误文件会很容易将磁盘占满,影响使用.
- 查看方式 : 使用
ulimit -a
可以查看是否开启. - 临时开启 : 使用
ulimit -c <size>
可以设置错误文件最大的size.
an@mycloud:...$ ulimit -a #查看的命令
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0 #最开始是0
#........
an@mycloud:~/code-in-linux/c++_linux$ ulimit -c 4096 #设置错误文件大小
an@mycloud:~/code-in-linux/c++_linux$ ulimit -a #再次查看
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 4096
#.........
当核心转储功能开启后 , 就可以测试了:
[!二进制与状态解析速记]-
一、十六进制 ↔ 二进制
- 1位十六进制 = 4位二进制
0xF
→1111
,0xA
→1010
,0x7
→0111
- 字母转换技巧:
B=11
→8+2+1
→1011
C=12
→8+4
→1100
二、status位运算解析
// 结构:高8位exit_code | 1位core_dump | 低7位signal exit_code = (status >> 8) & 0xFF; // 右移后取1字节 core_dump = (status >> 7) & 0x1; // 挪第7位到末尾 signal_num = status & 0x7F; // 屏蔽高9位
宏等价:
WEXITSTATUS(status)
→ 高8位WCOREDUMP(status)
→ bit7WTERMSIG(status)
→ 低7位三、核心心法**
- &操作:用
1
保留目标位,0
屏蔽干扰(如&0x7F
取低7位)- 人生映射:专注目标时,杂念自然被"屏蔽"
#linux/进程信号/status全解析(进制转换/位运算)
三.信号保存
[!信号保存的关键在于三张表]
- 信号掩码表(Signal Mask)
- 未决信号集(Pending Signal Table)
- 信号处理函数表(Single Handler Table)
- 而信号发送后又有四种情况
- 信号递达 : 进程接收到信号 , 进行
默认行为
or自定义行
为or忽略
- 信号未决 : 描述的是信号还没有递达的
状态
.- 信号阻塞 : 进程的信号掩码表里屏蔽了对应的信号,无法抵达
- 信号被忽略 : 进程收到信号,但没有任何行为执行
- 可以这样粗略的理解三张表
- 信号掩码表就类似于文件权限里的umask , 一个位图 , 用于屏蔽指定的信号
- 未决信号集也类似于个位图 , 接收信号后,递达信号前就会将对应比特位置1
- 信号处理函数表可以理解为一个函数指针数组 , 对应每个信号的处理方式.
[!信号阻塞和信号忽略]-
- 假如信号递达是人拿到了快递
- 信号阻塞就是快递到了快递站 , 人因为各种原因没去拿.
- 信号忽略就是快递到了快递站 , 但人有条件去拿却不拿,让快递在快递站吃灰
阻塞是递达之前,忽略是递达的一种情况
1.未决信号集:
- 暂时不考虑实时信号 , 就说那31个普通信号.
可以认为未决信号集类似一个有32个比特位的整形值 , 除了第一个比特位外(普通信号只有31个) , 每一个比特位都对应一个信号 . 比特位为0表示没有对应信号产生,为1则有.
//举例子...
00000000000000000000000000000000 //初始全零00000000000000000000000000000010 //一号信号到来 , 将第二个比特位置100000000000000000000000000000100 //二号信号到来 , 将第三个比特位置1
2.信号处理函数表:
- 一个函数指针数组 , 下标对应信号编号.
- 当信号递达时 , 直接访问这个数组 , 调用提前设定好的函数
进程在信号递达之前就知道自己该干嘛 , 这就是信号处理函数表的意义
3.位信号掩码表:
- 和未决信号集是一样的结构 , 只是比特位表达的含义不同 . 为0表示不对应信号递达 , 为1则相反
//举例子...
00000000000000000000000000000100 //(未决信号集)操作系统收到信号2
00000000000000000000000000000000 //(位信号掩码表)对应比特位为0,信号2可以递达00000000000000000000000000000100 //(未决信号集)操作系统收到信号2
00000000000000000000000000000100 //(位信号掩码表)对应比特位为1,直到这个比特位重新 置为0后信号2才能递达.
4. 三张表的联合工作:
- 未决信号集决定
信号是否产生
.- 位信号掩码表决定这个
信号能否递达
.- 信号处理函数表决定
递达方式
.
5.一个小实验:从阻塞到递达
- 通过
sigaddset()
将sigset_t类型的位图第二位设置为1 (让二号信号阻塞). - 使用
sigprocmask()
将位图从用户态更新到内核. - 使用
alarm
函数设置一个闹钟,时间到后执行自定义捕捉来接触二号信号的阻塞. - 运行时不停的通过
sigpending()
获取未决表并打印
预期运行效果 : 起初二号信号被阻塞,因此按下键盘上的Ctrl+c发送2号信号会阻塞,被记录在未决表里并显示 . 五秒后alarm结束,接触二号信号的阻塞 , 执行二号信号,终止进程.
void handler_alrm(int signal)
{cout << "捕捉信号SIGALRM , 接触SIGINT信号的阻塞" << endl;sigset_t set;sigdelset(&set,SIGINT);sigprocmask(SIG_SETMASK,&set,nullptr);
}void print_signal(sigset_t sig)
{for(int i = 31 ; i>= 1 ; i--){if(sigismember(&sig,i)){cout << 1;}elsecout << 0;}cout << endl;
}
int main()
{sigset_t set,old_set;sigaddset(&set,SIGINT);sigprocmask(SIG_SETMASK,&set,&old_set);alarm(5);signal(SIGALRM,handler_alrm);while(true){sigpending(&set);print_signal(set);sleep(1);}return 0;
}
6.小实验的运行结果:
- 起初2号信号在位信号掩码表里 , 因此发送的信号会出现在未决信号集里(可以看到从右往左第二位比特位变为1) .
- 当五秒结束,alarm函数触发信号 , 执行我们自己写的捕捉函数将2号信号解除屏蔽.
- 然后2号成功抵达 , 执行默认动作 , 终止进程.
- 虽然后面没有输出pending表 , 但是操作系统向进程递达信号
之前
会将pending表对应的比特位置0.
四.Q&A
Q: 如果重复收到同一个信号 , 操作系系统会怎么做?
A :
- 对于普通信号 , 操作系统在一个信号抵达前 , 会忽略随之而来的相同信号.
- 对于实时信号 , 操作系统会将数量记录下来.
Q: 信号的默认行为里 , 有的是Term类型 , 有的是 Core .区别在哪?
A:
- term类型就是正常递达信号的类型.
- Core类型就是会在抵达信号的同时 , 产生核心转储文件的类型.