1. 进程信号
1.1 什么是Linux信号
- 本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,但不是需要立即处理,你可以在后续进行处理
结合进程->信号结论
- 进程要处理信号,必须具备信号"识别"的能力(看到 + 处理动作)
- 为什么进程能够"识别"信号呢?程序员做的
- 信号产生是随机的,进程可能正在忙自己的事情,所以信号的后续处理,可能不是立即处理的!
- 信号会临时的记录下对应的信号,方便后续进行处理
- 在什么时候处理呢?合适的时候
- 一般而言,信号的产生相对于进程而言是异步的(两者同时发生)
1.2 信号如何产生
- ctrl+c:本质就是通过键盘组合键向目标进程发送2号信号 -> 进程退出了
- OS解释组合键->查找进程列表->前台运行的进程->OS写入对应的信号到进程的内部的位图结构中
信号是如何记录的?
实际上,当一个进程接收到某种信号后,该信号是被记录在该进程的进程控制块当中的。我们都知道进程控制块本质上就是一个结构体变量,而对于信号来说我们主要就是记录某种信号是否产生,因此,我们可以用一个32位的位图来记录信号是否产生。
- 就是进程的PCB内部保存了信号的位图字段
如何理解信号发送的本质
- 信号位图是在task_struct->task_struct内核数据结构->OS
- OS向目标进程写信号,OS直接修改PCB中的指定的位图结构,完成"发送"信号的过程
1.3 信号处理的常见方式:
- 默认(进程2自带的,程序员写好的逻辑)
- 忽略(也是信号处理的一种方式)
- 自定义动作(捕捉信号)
1.4 常见信号->kill -l
- kill -l查看进程,[1,31]都是普通信号,[34,64]都是实时信号
1.5 案例:模拟实现信号
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
using namespace std;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum <<"Pid: " << getpid()<<endl;
}
int main()
{
// signal(2, fun);
// 键盘
signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
while(true)
{
cout << "我是一个进程,我正在运行..., Pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
1.6 对系统调用接口的理解
- 用户调用系统接口->执行OS对应的系统调用代码->OS提取参数,或者设置特定的数值
- ->OS向目标进程写信号->修改对应进程的信号标记位->进程后续会处理信号->执行对应的处理动作!
1.7 由软件条件产生信号
- 如果管道的读端不光不读而且还关闭了,写端一直在写,
- 就会导致写没有意义,OS会自动终止对应的写端进程,通过发送信号的方式,SIGPIPE 13
- 理解: a. OS先识别到某种软件条件触发或者不满足 b.OS 构建信号,发送给指定的进程
1.8 硬件异常产生信号
理解除0错误
- 进行计算的是CPU,这个硬件
- CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位,溢出标记位,OS会自动进行计算完毕之后的检测,
如果溢出标记位是1,OS里面识别到有溢出问题,立即找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候进行处理
一旦出现硬件异常,进程不一定会退出(虽然一般默认是退出的)
一个程序中发生了死循环,主要是因为寄存器中的异常一直没有被解决
1.9 理解野指针或者越界问题
- 都必须通过地址,找到目标位置
- 我们语言上面的地址,全部都是虚拟地址
- 将虚拟地址转换成物理地址
- 页表+MMU(Memory Manager Unit,硬件!!)
- 野指针越界 -> 非法地址 -> MMU转化的时候,一定会报错
对信号的总结: 所有的信号,都有他的来源,但最终全部都是被OS识别,解释,并发送的!
1.10 理解信号处理的合适时候
- 合适的时候:从内核态返回用户态的时候
用户级页表 && 内核级页表
- 进程地址空间映射到物理内存中的页表是有2种的
- 一种是用户级页表,一种是内核级页表
- 通过是处于内核态,还是用户态,我们可以执行OS的代码,比如执行进程切换的时候,
- CPU的寄存器有2套,一套可见,一套CPU不可见,自用
-
CR3 -> 表示当前CPU的执行权限 1内核,3用户态
1.11 两种不能被忽略 && 杀死的信号
-
SIGKILL和SIGSTOP
2. 阻塞信号
2.1 信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意: 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.2 在内核中的表示
2.3 理解sigset_t类型
- sigset_t是一个系统类型,就像pid_t一样
- sigset_t---> 不允许用户自己进行位操作,OS给我们提供了对应的操纵位图的方法
- sigset_t---> user是可以直接使用该类型,和用内置类型 && 自定义类型没有任何差别
- sigset_t---> 一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象
2.4 案例
2.4.1 问题一: 对所有信号都进行了自定义捕捉,那我们是不是就写了一个不会被异常或者用户杀掉的进程??
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
void catchSig(int signum)
{
std::cout << "获取一个信号: " << signum << std::endl;
}
int main()
{
// 9号进程无法被屏蔽或者阻塞
for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig);
while(true) sleep(1);
return 0;
}
- 9号进程无法被屏蔽或者阻塞,所以不存在一个无法被用户杀死的进程
2.4.2 问题二:将2号信号block,并且不断的获取并打印当前进程的pending信号集,然后再突然发送一个2号信号,会发生什么??
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void handler(int signum)
{
std::cout << "捕捉 信号: " << signum << std::endl;
// 不要终止进程,exit
}
static void showPending(sigset_t& pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
// 0. 方便测试,捕捉2号信号,不要退出
signal(2, handler);
// 1. 定义信号集对象
sigset_t bset, obset;
sigset_t pending;
// 2. 初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&bset, 2 /*SIGINT*/);
// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);
(void)n;
std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl;
// 5. 重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中的没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 20)
{
// 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
// 但是2号信号的默认处理动作是终止进程!
// 所以需要对2号信号进行捕捉
std::cout << "解除对于2号信号的block" << std::endl;
int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
assert(n == 0);
(void)n;
}
}
return 0;
}
2.4.3 问题三:对所有信号都进程block,我们是不是就写了一个不会被异常或者用户杀掉的进程??
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t& pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
static void blockSig(int sig)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset, sig);
int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
assert(n == 0);
(void)n;
}
int main()
{
for (int sig = 1; sig <= 31; sig++)
{
blockSig(sig);
}
sigset_t pending;
while (true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
#!/bin/bash
i=1
id=$(pidof signal)
while [ $i -le 31 ]
do
if [ $i -eq 9 ];then
let i++
continue
fi
if [ $i -eq 19 ];then
let i++
continue
fi
kill -$i $id
echo "kill -$i $id"
let i++
sleep 1
done
- 9号进程无法被屏蔽或者阻塞
- 19号进程(SIGSTOP)也无法被屏蔽
3. 捕捉信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;
void showPending(sigset_t *pending)
{
for(int sig = 1; sig <= 31; sig++)
{
if(sigismember(pending, sig)) cout << "1";
else cout << "0";
}
cout << endl;
}
void handler(int signum)
{
cout << "获取了一个信号: " << signum << endl;
sigset_t pending;
int c = 20;
while(true)
{
sigpending(&pending);
showPending(&pending);
c--;
if(!c) break;
sleep(1);
}
}
int main()
{
// signal(2, SIG_IGN);
cout << "getpid: " << getpid() << endl;
// 内核数据类型,用户栈定义的
struct sigaction act, oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
sigaddset(&act.sa_mask, 6);
sigaddset(&act.sa_mask, 7);
// 设置进当前调用进程的pcb中
sigaction(2, &act, &oact);
cout << "default action : " << (oact.sa_handler) << endl;
while(true) sleep(1);
return 0;
}
- 处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS将会block
4. 核心转储 && 验证进程等待中的core dump标记位
4.1 获取子进程status
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
sleep(1);
int a = 100;
a /= 0;
exit(0);
}
int status = 0;
waitpid(id, &status, 0);
cout << "父进程:" << getpid() << " 子进程:" << id << \
" exit sig: " << (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;
return 0;
}
- 子进程的status中的core dump标志,是否发生了核心转储
- 当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中!
- core主要引用在调式的时候,还会形成一个新文件(保存核心数据),这里我不好演示
- 一般而言,云服务器(生产环境)的核心转储功能是被关闭的
5. 使用alarm信号闹钟验证1s之内会进行多少次count++
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
typedef function<void ()> func;
vector<func> callbacks;
uint64_t count = 0;
void showCount()
{
// cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
cout << "final count : " << count << endl;
}
void showLog()
{
cout << "这个是日志功能" << endl;
}
void logUser()
{
if(fork() == 0)
{
execl("/usr/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
// 定时器功能
// sig:
void catchSig(int signum)
{
for(auto &f : callbacks)
{
f();
}
//alarm(1);
}
static void Usage(string proc)
{
cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}
void handler(int signum)
{
sleep(1);
cout << "获得了一个信号: " << signum << endl;
exit(1);
}
int main(int argc, char* argv[])
{
signal(SIGALRM, catchSig);
alarm(1); // 设定了一个闹钟,这个闹钟一旦触发,就自动移除了
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(logUser);
while(true) count++;
return 0;
}
- 如果只计算到1w多,那么主要是因为cout + 网络发送 = IO
6. 可重入函数&不可重入函数
可重入函数:
- 在多线程的程序当中,如果一个函数可能同一时刻被多个线程访问,没有导致程序的结果产生二义性,则该函数被称之为可重入函数
不可重入函数
- 在多线程的程序当中,如果一个函数可能同一时刻被多个线程访问,从而导致程序的结果产生二义性,则该函数被称之为不可重入函数
6.1 理解volatile类型
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;
int flag = 0;
void changeFlag(int signum)
{
(void)signum;
cout <<"change flag: "<< flag;
flag = 1;
cout << "->" << flag << endl;
}
int main()
{
signal(2, changeFlag);
while(!flag);
cout << "进程正常退出后:" << flag << endl;
}
- 没加优化之前,CPU都是在内存中拿数据
- 加了优化之后,内存中的数据会放在edx中,之后CPU只向edx里面拿数据
就导致每次拿的都是1无法正常退出,
volatile int flag = 0;
-
保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
7. SIGCHLD信号(了解)
7.1 证明子进程退出会向父进程发送信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;
void handler(int signum)
{
cout << "子进程退出: " << signum << " fater: " << getpid() << endl;
}
// 证明 子进程退出,会想父进程发送信号
int main()
{
signal(SIGCHLD, handler);
if (fork() == 0)
{
cout << "child pid: " << getpid() << endl;
sleep(1);
exit(0);
}
while (true) sleep(1);
}
7.2 在子进程退出后,不等待子进程,让它自动释放僵尸进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
using namespace std;
// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{
// OS 默认就是忽略的
signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略
if(fork() == 0)
{
cout << "child: " << getpid() << endl;
sleep(5);
exit(0);
}
while(true)
{
cout << "parent: " << getpid() << " 执行我自己的任务!" << endl;
sleep(1);
}
}
-
signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略