目录
一、进程与信号
1.进程与信号的关系
- 进程必须识别并且能够处理信号;即使信号没有产生,也要具备处理信号的能力;信号的处理能力,属于进程内置功能的一部分
- 进程即便没有收到信号,也能知道信号怎么处理
- 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,要等到合适的时候
- 一个进程当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存那些信号已经发生了的能力
ctrl+c为什么会杀掉我们的前台进程呢?
linux中,每一次登录,一个终端一般会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程。
前台进程和后台进程的一个区别是谁能拿到键盘输入
在运行一个进程时,我们只需要在后面加一个&符号,它就会变成后台进程,例如:
./myprocess &如果要杀掉该进程,我们就需要查看该进程的pid,让后使用kill -9 pid,进程pid查看方法:
ps axj | grep myprocesssctrl+c本质是被进程解释成为收到了信号,2号信号
2.信号的种类
3.信号的处理方式
- 默认动作
- 忽略
- 自定义动作
验证:进程myprocess是收到了2号信号才终止的
1.signal函数,系统调用函数
只需要设置一次,往后都有效
int signum:传哪个信号
sighandler_t handler:自定义的动作
signal是修改进程对特定信号处理的动作
#include <iostream> #include <cstdlib> #include <unistd.h> #include <signal.h> using namespace std; void myheader(int signo) { cout << "myprocess get a signal: " << signo << endl; } int main() { signal(2, myheader);//只需要设置一次,往后都有效 while(true) { cout << "i am a process!" << endl; sleep(1); } return 0; }
信号相对于进程的控制流程是异步的
键盘是如何向内核输入数据的呢?ctrl+c是如何变成信号的呢? --- 硬件
答:在电脑开机启动时,电脑会自动将操作系统加载到内存中,键盘输入数据,会向CPU发送一个硬件中断,键盘有自己独有的中断号,操作系统读取到CPU发送的中断号,在自己的中断向量表中查找该中断号对应的中断方法,对应的即是读取键盘数据的方法,随即操作系统通过该方法读取键盘数据,并存入对应缓冲区中,此过程重复至键盘不再发送硬件中断,拷贝完成后,操作系统会给CPU发送中断
如果键盘输入ctrl+c组合键,操作系统会判断数据,控制(ctrl+c转化为2号信号并发给前台进程)
4.信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。信号模拟了中断
二、 信号的产生
1.键盘组合键
如
| ctrl+c |
发送2号信号 | 终止前台进程 |
| ctrl+\ | 发送3号信号 | 终止前台进程 |
| ctrl+z | 发送19号信号 | 暂停前台进程 |
不是所有的信号都是可以被signal捕捉的,比如19号信号,还有9号进程。
2.kill命令
3.系统调用
1.kill
int main(char argc, char* argv[]) { if(argc != 3) { printf("Usage:\n\t, %s signnum pid\n", argv[0]); } int signnum = stoi(argv[1]); pid_t pid = argv[2]; kill(pid, signnum); return 0; }
2.raise
发送一个信号给调用者
int main() { int cnt = 5; while (true) { cout << "i am a process, pid: " << getpid() << endl; sleep(1); cnt--; if(cnt == 0) raise(2); } return 0; }
这个raise相当于
kill(getpid(), 2);
3.abort
引起一个正常的进程终止,发送自己一个6号信号
这个函数,即使被signal捕捉,执行完被捕捉的函数,最后也会终止进程。
而kill -6 pid命令,在6号信号被捕捉后,并不会终止进程。
kill(getpid(), 6);在6号信号被捕捉后,kill也不会终止进程
原因在于abort函数内部经过了特殊处理
无论信号的产生方式有多少种,最终都是操作系统统一发送给进程的,OS是进程的管理者
4.异常
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
我们的/0和野指针怎么怎么让操作系统知道的呢?
5.软件条件
异常只会由硬件产生吗?
alarm会发出14号信号。
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; void work() { cout << "Print log..." << endl; } void headler(int sigo) { cout << "...get a sig, number: %d" << sigo << endl; int n = alarm(5); cout << "剩余时间: " << n << endl; } int main() { signal(SIGALRM, headler); int n = alarm(50); while (true) { cout << "Proc is run..., pid: " << getpid() << endl; sleep(1); } return 0; }
进程等待补充
status进程退出标识符
8-15位是表示退出码,0-7位是退出信号
而core dump(核心转储)标志位是一位数据0和1,表示是Term还是Core
int main() { int id = fork(); if(id == 0) { //child int cnt = 500; while(cnt--) { cout << "i am child process! pid: " << getpid() << " cnt: " << cnt << endl; sleep(1); } } //father int status = 0; int rid = waitpid(id, &status, 0); // 0代表阻塞等待 if(rid == id) { cout << "child quit info, rid: " << rid << " exit code: " << ((status >> 8) & 0xFF) << " exit signal: " << (status & 0x7F) << " core dump: " << ((status >> 7) & 0x1) << endl; } return 0; }通过ulimit这个命令将core dump(核心转储)打开
1.使用Action为Term的信号
2.使用Action为core信号
在我们发出Action为core的信号
一旦打开系统core dump功能,一旦进程出现异常,OS会将进程在内存中的运行信息,给我们dump(转储)到进程的当前目录(磁盘),形成core.pid文件: 核心转储(core dump)
出现这个core.pid文件后,我们调试时,就可以定位到出错的是哪一行
gdb core-file core.pid先运行,再core-file: 事后调试
而在现实实际中,尤其是大型服务器群,我们不要使用core dump功能。假设有人写出一个一启动就挂的服务,而在公司中,一般服务挂了之后需要自动再启动,就会导致一个问题。就是core.pid文件会把磁盘打满,将操作系统给挂掉,这是损失非常大的。所以再线上服务,就需要禁止core dump功能。
三、信号的保存
什么是信号的发送呢?
信号是操作系统给进程的PCB发送,是结构体中一个int signal;32位的一个位图
- 比特位的内容是0 1,表示是否收到该信号
- 比特位的位置,表示信号的编号
- 所谓的"发信号"就是"写信号",本质就是操作系统修改进程PCB task_struct里位图signal对应的比特位
因为操作系统是进程的管理者,只有它能修改进程task_struct内部的属性!
为什么要信号保存呢?
进程收到信号之后,可能不会立即处理这个信号,信号不会被处理,就要有一个时间窗口。
1.阻塞信号
1.信号的相关概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.在linux内核中的表示
每个信号都有两个标志位block(阻塞)、pending(未决),还有一个函数指针提供方法
block位图位为0表示阻塞,为1表示不阻塞
pending位图判定进程是否收到信号和收到了那些信号
函数指针类型
宏:
SIG_DFL:默认
SIG_IGN:忽略信号
SIG_ERR:
自定义函数
3.sigset_t
可以用来表示block或pending的位图数据结构
4.信号集操作函数
1.sigset_t使用函数
#include <signal.h>
int sigemptyset(sigset_t *set); //全置0
int sigfillset(sigset_t *set); //全置1
int sigaddset (sigset_t *set, int signo); //插入
int sigdelset(sigset_t *set, int signo); //删除
bool sigismember(const sigset_t *set, int signo); //查找
2.sigprocmask(操纵block位图)
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,更改进程的信息屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset中,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字为mask
| SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号 相当于mask = mask | set |
| SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字解除阻塞的信号 相当于mask = mask & (~set) |
| SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值 相当于mask = se |
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
注:9号和19号信号,不可被屏蔽
3.sigpending(查看pending位图)

sigset_t *set是一个输出型参数,可以将pending位图通过set带出来。
void PrintPending(sigset_t& pending) { for (size_t i = 31; i >= 1; --i) { if(sigismember(&pending, i)) { cout << "1"; } else { cout << "0"; } } cout << endl << endl; } void headler(int signo) { cout << "get a siganl: " << signo << endl; sleep(1); } int main() { //0.对2号信号进行自定义动作 signal(2, headler); //1.对2号信号进行屏蔽 -- 数据预备 sigset_t bset, oset; //这个位图bset是在用户栈上开辟空间创立的,属于用户 sigemptyset(&oset); sigemptyset(&bset); sigaddset(&bset, 2); //我们将2号信号屏蔽了吗?没有,因为它还没有被设置到进程的task_struct里 //1.2调用系统调用,将数据设置进内核 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 == 20) { cout << "unblock 2 signal" << endl; sigprocmask(SIG_SETMASK, &oset, nullptr); } } return 0; }
四、信号的捕捉处理
1. 信号是如何被处理的?是什么时候处理的?
当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理!
内核态:允许你访问操作系统的代码和数据
用户态:只能访问用户自己的代码和数据
调用系统调用! -- 操作系统是自动会做"身份"切换的,用户身份变成内核身份或者反着来
int 80 -- 汇编指令,从用户态陷入内核态
过程:
- 先执行代码,出现了系统调用 --- 陷入内核态
- 执行操作系统代码。执行完毕,返回用户态前处理当前进程可以递送的代码 --- 返回用户态
- 自定义捕捉,执行我们的捕捉方法 --- 重新进入内核
- 最后在直接返回用户态,从上次中断的位置继续向下执行
4次状态切换
2.重谈进程地址空间
用户级页表有几份? 进程有几个,就有几份用户级页表。因为进程具有独立性
内核级页表有几份? 只有一份
每一个进程看到的3-4GB的内容都是一样的,整个系统中,无论进程再怎么切换,3-4GB内容都是不变的!
进程视角:我们调用系统调用的方法,就是在我们自己的地址空间中进行执行的
操作系统视角:任何一个时刻,都会由进程执行。我们想执行操作系统的代码,就可以随时执行!
操作系统的本质:基于时钟中断的一个死循环!
如何检测进程能否访问内核空间,检测CPU内部寄存器ecs,低两位为0能访问,为3不能访问
3.sigaction函数
函数原型
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); //函数指针 void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; //位图,额外要屏蔽信号 int sa_flags; void (*sa_restorer)(void); };示例:
void PrintPending() { sigset_t pending; sigpending(&pending); for (size_t i = 31; i >= 1; --i) { if(sigismember(&pending, i)) { cout << "1"; } else { cout << "0"; } } cout << endl; } void headler(int signo) { cout << " catch a signal: " << signo << endl; while(true) { PrintPending(); sleep(1); } } int main() { struct sigaction act, oact; memset(&act, 0, sizeof(act)); memset(&oact, 0, sizeof(oact)); act.sa_handler = headler; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, 1); sigaddset(&act.sa_mask, 3); sigaddset(&act.sa_mask, 4); sigaddset(&act.sa_mask, 5); sigaction(2, &act, &oact); while (true) { cout << " i am process" << endl; sleep(1); } return 0; }
4.可重入函数
如果一个函数,被重复进入的情况下,出错了,或者可能出错,不可重入函数!否则,叫做可重入函数。
目前我们学习到的大部分函数都是不可重入的
5.volatile关键字
保持内存的可见性,防止编译器过度优化。
volatile int flag = 0; void headler(int signo) { cout << "catch a signal: " << signo << endl; flag = 1; } int main() { signal(2, headler); //在编译器优化下,flag有可能直接被优化进CPU内部的寄存器中。所以要在该变量前加关键字,volatile while(!flag); cout << "process quit normal" << endl; return 0; }
五、SIGCHLD信号
子进程退出时,会主动的给父进程发送SIGCHLD(17号)信号。
由此我们可以写出另一种进程等待方式,基于信号来进程等待
如果我们一次退出10个子进程呢?退出一半的子进程呢?
可以以非阻塞方式加while循环进行等待进程退出
void headler(int signo) { sleep(1); pid_t rid; while(rid = waitpid(-1, nullptr, WNOHANG) > 0) //以非阻塞等待方式退出 cout << "i am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl; } int main() { signal(17, headler); for (size_t i = 0; i < 10; ++i) { pid_t id = fork(); if(id == 0) { //child while(true) { cout << "i am a child process, ppid: " << getppid() << " pid: " << getpid() << endl; sleep(1); break; } cout << "child quit " << endl; exit(0); } } //father while(true) { cout << "i am a father process! pid: " << getpid() << endl; sleep(1); } return 0; }
由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可用。
当我们不需要知道子进程退出结果,就可以直接采用signal(17, SIG_IGN);
我们默认对17号信号默认动作 -- SIG_DFL -- action -- IGN
int signum:传哪个信号




















