前言
我们早上去上学时,会遇到信号灯
那么当我们遇到信号灯时,下意识地就会根据信号灯的颜色,决定此时究竟过不过马路
但是
我们始终知道“红灯停,绿灯侠,黄灯亮了要注意”这个规则,不管我们在不在过马路,这个规则时刻在我们的脑海之中
我们将操作系统比作是一个社会
进程相当于社会中的人,形形色色
人收到红绿灯的信号,决定过不过马路,进程也会收到各种信号,决定接下来应该做什么
很显然,进程对于信号有以下3点认识:
- 进程始终知道有哪些信号
- 进程始终知道将来遇到这些信号时应该做什么
- 进程遇到信号时,会根据自己的认识,在接下来合适的完成对应的动作
对于进程来讲是以上3点,对于信号本身来讲,又分为产生、保存、处理
三个阶段
我们逐一来探索
目录
这是信号生命周期的三个阶段:
哦,让我们先对Linux的信号有个最基本的认识
这就是一种信号
1.信号的产生
信号的产生其实有4种方式:
- 终端按键------键盘
- 系统调用函数------kill
- 软件条件
- 硬件异常
1.1 终端按键------键盘
#include <iostream>
#include <unistd.h>
int main() {
while(true) {
std::cout << "I am a proc: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
1.2 系统调用函数------kill
其中 1-31号信号是普通信号,34-64号信号是实时信号
其中9号信号
是权力最大的信号,不会被捕捉,能杀死后台进程
由于ctrl C 本质是给目标进程发送2号信号
,所以我们使用kill -2
也能终止进程:
kill -2 PID
kill -SIGINT PID
另外,对于相当一部分信号而言,进程受到信号的默认动作就是终止当前进程
但也并不全是,譬如:
我们可以通过调用kill接口,自己实现一个mykill
程序
#include <iostream>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void Usage() {
cout << "Usage:\n\t" << "./kill [SIG] [PID]" << endl;
exit(1);
}
int main(int argc, char* argv[]) {
if(argc != 3) {
Usage();
}
else {
kill(atoi(argv[2]), atoi(argv[1]));
}
return 0;
}
1.3 由软件条件产生信号
alarm函数
可以产生对应的SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
我们写个程序看看一秒中之内while循环能打印多少次,由于cpu处理io很慢和云服务器网络传输的原因,打印次数很少:
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
alarm(1); // 一秒之后,会给目标进程发送SIGALRM
int cnt = 0;
while(1) {
cout << cnt << endl;
cnt++;
}
cout <<"cnt: " <<cnt << endl;
return 0;
}
当我们捕捉了14号信号,把IO移除后,我们看看:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo) {
cout << "signo: " << signo << endl;
cout << "cnt: " << cnt << endl;
exit(1);
}
int main() {
signal(14, handler);
alarm(1); // 一秒之后,会给目标进程发送SIGALRM
while(1) {
cnt++;
}
return 0;
}
有io和没io差了2000多倍
1.4 硬件异常产生信号
我们先来谈一下core-dump核心转储,通过core-dump核心转储我们才能知道硬件异常产生了什么信号
查看系统的core-dump的命令:
ulimit -a
因为线上不是调试环境,所以线上不需要核心转储,云服务器默认是0:
改成1024:
ulimit -c 1024
再补充一个概念,在学习进程等待时,我们waitpid的第二个参数status
的第八个比特位叫做core-dump
标志位,这个标志位标记了进程是否收到信号而结束,是的话就会被标记成1,同时会生成一个core文件,可供调试
- 演示除零错误:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main() {
pid_t id = fork();
if(id == 0) {
//child
int cnt = 5;
while(cnt--) {
cout << "I am child " << getpid() <<" count: " << cnt << endl;
sleep(1);
}
int a = 10;
int b = 0;
a = a/b;
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id) {
cout << "Succeed" << endl;
cout << "exit code: " << ((status>>8)&0xff) << endl;
cout << "sig: " << ((status)&0x7f) << endl;
cout << "core dump: " << ((status>>7)&0x1) << endl;
}
return 0;
}
gdb xxx
core-file xxx
除零错误对应的正是8号信号
- 野指针
- 数组越界
- ctrl \
- 等等
大家可以通过设置core-dump核心转储来调试确定进程退出原因
所以说:
程序崩溃准确来说是进程的崩溃,是OS向进程发送信号,本质是OS向目标进程的pcb的位图中指定的比特位由0到1写信号
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
总结思考一下
- 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
- 信号的处理是否是立即处理的?在合适的时候
- 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
- 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
- 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
2.信号的保存
我们终于来到了信号的第二阶段------保存阶段
先来介绍一下关于信号的其他一些知识:
进程的处理有三种形式:
- 默认
- 忽略
- 捕捉(自定义handler函数)
-
block位图:比特位的位置代表信号的编号,比特位的内容(0 1)代表是否阻塞该信号
-
pending数组:比特位的位置代表信号的编号,比特位的内容(0 1)代表是否受到信号。如果没有受到对应的信号,照样可以阻塞特定的信号。阻塞更准确的理解,理解成一种状态
-
handler数组:用信号的编号,作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)
我们稍后时候介绍一下什么是用户态和内核态
介绍一组pending/block位图的系统调用接口:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
信号的系统调用接口:
- 用来改变进程内核block位图的接口
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
:
- 返回内核中的pending位图
int sigpending(sigset_t *set);
实验:
- 阻塞2号信号
- 不断获取pending信号集,并打印
- 发送2号信号给进程
- 过一段时间取消对2号信号的block
- 看到二号信号立刻被递达
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
sigset_t in;
sigset_t out;
sigemptyset(&in);
sigemptyset(&out);
sigaddset(&in, SIGINT);
sigprocmask(SIG_SETMASK, &in, &out);
int cnt = 0;
sigset_t pending;
sigemptyset(&pending);
while(true) {
sigpending(&pending);
for(int i = 1; i < 32; i++) {
if(sigismember(&pending, i)) {
std::cout << 1;
}
else {
std::cout << 0;
}
}
std::cout << std::endl;
sleep(1);
cnt++;
if(cnt == 10) {
sigdelset(&in, 2);
sigprocmask(SIG_SETMASK, &in, &out);
}
}
return 0;
}
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。
3. 信号处理时
什么时候进行信号的递达?
你访问的是用户空间的代码数据,你的状态就是用户态
你访问的是内核空间的代码数据,你的状态就是内核态
那么用户想要通过系统调用访问内核的代码和数据
但是一个普通用户有权力访问操作系统的代码和数据吗?
没有
那么就是说,当用户妄图访问内核空间时,操作系统会对用户进行身份
切换,从user切换到kernel
操作系统怎么知道我们当前的状态是什么呢
CPU中会存在一个状态寄存器,存储权限相关的数据,表示我们所处的状态;看你使用的是那个级别的页表
每个进程都有自己的用户级页表。但是内核只有一份,内核级页表也只需要维护一张
换句话说,内核级页表被所以进程所共享
上图回答了信号在何时被处理
内核如何进行信号的捕捉
4. 信号捕捉函数
1.signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
2.sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
我们来演示一下sigaction函数的用法吧
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
- 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
我们将2号信号捕捉,设置sa_mask使得执行2号信号的handler函数时,同时也将3号信号阻塞
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int flag = 0;
void handler(int signo) {
flag = 1;
int cnt = 5;
while(cnt) {
cout << "cnt: " << cnt << endl;
sleep(1);
cnt--;
}
cout << "flag == 1, proc end by " << signo << endl;
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, 3);
struct sigaction action;
struct sigaction oldaction;
action.sa_handler = handler;
action.sa_mask = set;
action.sa_flags = 0;
sigaction(SIGINT, &action, &oldaction);
while(!flag) {
;
}
return 0;
}
我们发现ctrl C发送信号后,pending位图中2号位置为1
开始执行handler函数,此时2号信号被阻塞同时pending位图中的2号位置0
在执行handler时,我们再发送2号信号,pending中的2号位又被置1;发送三号信号,但是3号信号始终是阻塞的。
5. SIGCHLD信号
子进程退出时会给父进程发SIGCHLD信号(17号信号) 如果子进程比父进程提前退出,那么子进程会僵尸#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
if(fork() == 0) {
// child
int cnt = 5;
while(cnt) {
cout << "child: " << getpid() << " cnt: " << cnt << endl; cnt--;
sleep(1);
}
exit(0);
}
//parent
int cnt = 10;
while(cnt) {
cout << "parent: " << getpid() << " cnt: " << cnt << endl;
cnt--; sleep(1);
}
return 0;
}
而我们将17号信号捕捉,handler方法改为SIG_IGN忽略时,则子进程直接退出,不会等待父进程将它回收:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main() {
signal(SIGCHLD, SIG_IGN);
if(fork() == 0) {
// child
int cnt = 5;
while(cnt) {
cout << "child: " << getpid() << " cnt: " << cnt << endl;
cnt--;
sleep(1);
}
exit(0);
}
//parent
int cnt = 10;
while(cnt) {
cout << "parent: " << getpid() << " cnt: " << cnt << endl;
cnt--;
sleep(1);
}
return 0;
}
值得注意的是,这样操作父进程便得不到子进程返回的PID了
我父进程让你子进程去办事情,你总得告诉我一声你有没有办完事情吧