一、信号相关概念
1、信号的结论
1)进程必须能够识别并处理信号,即使信号没有产生,也要具备处理信号的能力,这种能力属于进程内置功能的一部分。
2)即使进程没有收到信号,也能知道信号的处理方法。
3)当进程收到一个具体的信号时,进程可能不会立即处理这个信号,等到合适的时间才会处理这个信号,信号的处理方式有三种,分别是默认动作,忽略,自定义动作(信号的捕捉)。
4)从信号产生,到信号开始被处理,一定会有时间窗口,进程具有临时保存信号已经发生了的能力。
注意:并不是所有的信号都能被捕获。
eg:SIGKILL(9)和SIGSTOP(19)
2、ctrl + c 为什么能够杀掉我们的前台进程呢?
Linux中,一次登录中,一个终端一般会配上一个bash,每一个登录,只允许一个进程是前台进程(前台进程用来获取键盘输入,键盘输入首先是被前台进程收到的),可以允许多个进程是后台进程。
当一个进程执行时,该进程就变成了前台进程,而bash变成了后台进程。

1~31号信号为普通信号,34~64号信号为实时信号。
ctrl + c 本质是被进程解释成为收到了信号(2号信号)。

代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
//int: 收到了哪一个信号
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(SIGINT, myhandler);//只需设置一次,一直有效!
//信号的产生和我们自己的代码运行时是异步的
while(true)
{
cout << "I am a crazy process : " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:

ctrl + c 的默认动作是终止进程,现在我们设置了自定义动作就会按照我们的要求来执行,但是此时进程就不会终止了,可以通过 kill -9 45836杀掉该进程。
如何将进程变成后台进程呢?

ctrl + c 只能杀掉前台进程,无法杀掉后台进程。
3、键盘数据是如何输入给内核的,ctrl + c 又是如何变成信号的?
键盘摁下ctrl + c,产生硬件中断,OS捕获中断,将其转换为SIGINT信号,并发送给当前前台进程。
二、信号的产生
1、键盘组合键
ctrl + c : 2号信号

ctrl + \ : 3号信号

2、kill 命令
kill -signo pid

3、系统调用
1)kill

可以给任意进程发信号。
代码(自主实现kill命令):
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
// ./mykill signum pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if(n == -1)
{
perror("kill");
exit(2);
}
return 0;
}
proc.c:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process, pid: %d\n", getpid());
sleep(1);
}
return 0;
}
运行结果:
终端1:

终端2:

2)raise

给当前进程发信号。
代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
int cnt = 0;
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
cnt++;
if(cnt % 2 == 0)
{
raise(2);
}
}
return 0;
}
运行结果:

3)abort

向进程自身发送SIGABRT(6号)信号,让当前进程异常终止。
代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
int cnt = 0;
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
cnt++;
if(cnt % 2 == 0)
{
abort();
}
}
return 0;
}
运行结果:

怎么知道abort发送的是6号信号呢?
代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(SIGABRT, myhandler);//执行只是注册信号处理函数,当收到信号时才调用函数
int cnt = 0;
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
cnt++;
if(cnt % 2 == 0)
{
//kill(getpid(), 2);
//raise(2);
//abort();
}
}
return 0;
}
运行结果:
终端1:

终端2:

4、异常
当发生除零或野指针时,会让进程崩溃,操作系统会给进程发送信号。
操作系统是硬件的管理者,CPU也是硬件,CPU中有状态寄存器,当发生除零时,状态寄存器中的溢出标志位会由0变成1,虽然我们修改的是CPU内部的状态寄存器,但是这只影响当前进程,操作系统会通过调度机制,让其他进程继续执行。当访问野指针时,因为CPU访问的是虚拟地址,MMU(内存管理单元)通过页表将虚拟地址转换为物理地址,但访问野指针会导致地址转换失败。
代码:
#include <iostream>
using namespace std;
int main()
{
// int a = 10;
// int b = 0;
// a /= b;
// cout << "a = " << a << endl;
int *p = nullptr;
*p = 100;
return 0;
}
运行结果:


5、软件条件
异常不只会由硬件产生,还可以由软件产生。
1)闹钟:SIGALRM(14号信号)

返回值:如果之前没设置过闹钟,或之前的闹钟已经超时,则返回0;
如果之前还有未超时的闹钟,则返回距离上次闹钟剩余的秒数。
代码:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
alarm(3);//3秒后发送信号
while(true)
{
cout << "process is running" << endl;
sleep(1);
}
return 0;
}
运行结果:

2)当一个进程向一个没有读端的管道写入数据时,操作系统会向该进程发送SIGPIPE信号。
代码:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
char buffer[1024];
int n = 1024;
n = read(4, buffer, n);
printf("n = %d\n", n);
perror("read");
return 0;
}
运行结果:

6、core dump 标志


默认云服务器上面的core功能是被关闭的。

代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 500;
while(cnt)
{
cout << "i am a child process, pid: " << getpid() << "cnt: " << cnt << endl;
sleep(1);
cnt--;
}
exit(0);
}
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
cout << "child quit info, rid: " << rid << " exit code: " <<
((status>>8)&0xFF) << " exit signal: " << (status&0x7F) <<
" core dump: " << ((status>>7)&1) << endl;
}
return 0;
}
运行结果:


打开系统的core dump功能(临时生效),一旦进程出异常,OS会将进程在内存中的运行信息(定位到出错行),给dump(转储)到进程的当前目录,形成core.pid文件(磁盘):核心转储(core dump)



先运行,再core-file:事后调试

7、什么是信号的发送?
对于普通信号而言,信号是给进程的PCB发的。
task_struct
{
int signal;// 0000 0000 0000 0000 0000 0000 0000 0010 普通信号,位图管理信号
...
}
//1.比特位的内容是0还是1,表明是否收到信号
//2.比特位的位置(第几个,不算0),表示信号的编号
//3.所谓的"发信号",本质就是OS去修改task_struct的信号位图对应的比特位
//4.OS是进程的管理者,只有它有资格去修改task_struct内部的属性
三、信号的保存
1、信号其他相关概念
1)实际执行信号的处理动作称为信号递达(Delivery)。
2)信号从产生到递达之间的状态,称为信号未决(Pending)。
3)进程可以选择阻塞(Block)某个信号。
4)被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
5)阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2、在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。
常规信号在递达之前产生多次信号只计一次,而实时信号在递达之前产生多次信号可以依次放在一个队列里。
3、sigset_t
sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞(1表示阻塞),而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。
4、信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都清零
int sigfillset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都置1
int sigaddset (sigset_t *set, int signo);
//将指定信号signo添加到信号集set中,即把该信号对应的位置1
int sigdelset(sigset_t *set, int signo);
//从信号集set中移除指定信号signo,即把该信号对应的位清零
int sigismember(const sigset_t *set, int signo);
//检查信号signo是否在信号集set中,即判断该信号对应的位是否为1
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//可以读取或更改进程的信号屏蔽字
how:

set:
为NULL时,不修改屏蔽字,仅读取旧值到oldset。
oldset:
输出型参数:保存修改前的信号屏蔽字。
为NULL时,不保存旧值。
#include <signal.h>
int sigpending(sigset_t *set);
//用于获取已经发给进程,但被阻塞住还没处理的信号
代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void PrintPending(sigset_t &pending)
{
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << "\n\n";
}
void handler(int signo)
{
cout << "catch a signo: " << signo << endl;
}
int main()
{
// 0. 对2号信号进行自定义捕捉
signal(2, handler);
// 1. 对2号信号进行屏蔽 --- 数据预备
sigset_t bset, oset;//在栈上开辟的空间,属于用户区
sigemptyset(&bset);
sigemptyset(&oset);
sigaddset(&bset, 2);
// 1.1 调用系统调用,将数据设置进内核
sigprocmask(SIG_SETMASK, &bset, &oset);//屏蔽2号信号
// 2. 重复打印当前进程的pending
sigset_t pending;
int cnt = 0;
while(true)
{
// 2.1 获取
int n = sigpending(&pending);
if(n < 0) continue;
// 2.2 打印
PrintPending(pending);
sleep(1);
cnt++;
// 2.3 解除阻塞
if(cnt == 4)
{
cout << "unblock 2 signo" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
// 3. 发送2号信号
return 0;
}
运行结果:

1~31个信号中,只有9和19号信号不能被屏蔽。
四、信号的捕捉
1、信号什么时候被处理的?
当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理。
内核态:允许我们访问操作系统的代码和数据。
用户态:只能访问用户自己的代码和数据。
2、谈谈地址空间

有几个进程,就有几份用户级页表 --- 进程具有独立性。
内核级页表只有一份。
每一个进程看到的3~4GB的东西都是一样的。整个系统中,不管进程如何切换,3~4GB空间的内容是不变的。
进程视角:我们调用系统中的方法,就是在我们自己的地址空间中进行执行的。
操作系统视角:任何一个时刻,都有进程在执行。我们想执行操作系统的代码,可以随时执行。
操作系统的本质:基于时钟中断的一个死循环。
计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断。
3、信号的捕捉流程


4、sigaction(注册信号处理函数)

act是输入型参数,oldact是输出型参数。

sa_handler:处理方法,sa_mask:用于屏蔽更多信号。
代码示例:
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
1. pending位图,在执行信号捕捉方法之前从1->0,先清零,再调用
2. 信号被处理时,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用
void PrintPending()
{
sigset_t set;
sigpending(&set);
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&set, signo))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
void handler(int signo)
{
PrintPending();
cout << "catch a signal, signal number: " << signo << endl;
// while(true)
// {
// PrintPending();
// sleep(1);
// }
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
// sigemptyset(&act.sa_mask);
// sigaddset(&act.sa_mask, 1);
// sigaddset(&act.sa_mask, 3);
// sigaddset(&act.sa_mask, 4);
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
5、可重入函数

insert函数被main和handler执行流重复进入,最后导致node2节点丢失,造成内存泄漏。
当一个函数被重复进入时,出错或可能出错,叫做不可重入函数,否则,叫做可重入函数。
大部分函数都是不可重入的。
6、volatile
关键字 volatile:防止编译器过度优化,保持内存的可见性。
代码示例:
volatile int flag = 0;
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
//在优化条件下,flag变量可能直接被优化到CPU内的寄存器中
cout << "process quit normal" << endl;
return 0;
}
mysignal:mysignal.cc
g++ -o $@ $^ -O3 -g -std=c++11// -O3:最高级别的优化选项
.PHONY:clean
clean:
rm -f mysignal
7、SIGCHLD信号
子进程退出的时候,会主动向父进程发送SIGCHLD(17)信号。
代码示例:
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo)
{
sleep(5);
pid_t rid;
while((rid = waitpid(-1, nullptr, WNOHANG)) > 0) // 非阻塞等待,若子进程不退出,返回0
{
cout << "I am process: " << getpid() << "catch a signo: " << signo <<
"child process quit: " << rid << endl;
}
}
int main()
{
signal(17, SIG_IGN);//SIG_DFL -> action -> IGN
// srand(time(nullptr));
// signal(17, handler);
for(int i = 0; i < 10; i++)
{
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << "I am process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
cout << "child quit!" << endl;
exit(0);
}
//sleep(rand()%5 + 3);
sleep(1);
}
//father
while(true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
对于17号信号,SIG_IGN和SIG_DFL的区别:
1)signal(SIGCHLD, SIG_IGN)
父进程会忽略子进程结束的通知,内核会自动回收子进程的资源(避免产生僵尸进程)。
父进程不需要调用wait()或waitpid()来回收子进程。
2)signal(SIGCHLD, SIG_DFL)
系统默认行为是忽略信号,但内核不会自动回收子进程资源,子进程会变成僵尸进程。
父进程需要手动调用wait()或waitpid()来回收子进程的退出状态。