[Linux] 一篇文章, 掌握Linux进程信号: 信号的产生、深入理解信号的处理与捕捉、信号在内核中的表示、进程的内核态与用户态转换分析、volatile关键字...
进程信号
可以通过
kill
命令, 给进程发送信号, 终止进程
介绍进程信号
- 进程需要有识别进程信号的能力, 可以接收, 也可以分辨出是什么信号
- 进程需要有处理信号的能力, 即 需要执行信号表示的含义或功能
- 进程默认拥有1、2的能力, 无论是否接收到信号
- 不管外卖(一般不会吧)
- 马上放下手头的事情, 去门口取外卖
- 告诉小哥等一会, 先忙完手头的一点事情再去拿外卖
- 告诉小哥把外卖放下就行, 然后再决定什么时候去拿
- 默认情况, 默认处理. 即, 按照信号的含义进行处理
- 忽略信号, 不做处理. 即, 不管信号
- 接收到信号, 自定义处理. 即, 由用户接收信号, 并自定义处理动作
kill
命令来给指定进程传递信号kill -9
终止了一个进程, 9
其实就是一个进程信号bash(shell环境)
下, 可以使用kill -l
, 查看进程信号列表:如果是
zsh(shell环境)
, 可能只会输出一小部分:
kill -l
显示出的Linux进程信号的列表signum.h
**相关头文件中, 一般都在/usr/include/bits/.
路径下:man 7 signal
来查看man手册中记录的有关信号的内容:man
手册中, 不仅记录了信号的宏定义, 还记录了 信号的类型 和 含义, 即 默认处理方法进程信号的处理
位图
, 当使用指令或其他方式 向进程发送信号时, 操作系统就会将进程信号位图的指定位置 写入1- 默认情况, 默认处理. 即, 按照信号的含义进行处理
- 忽略信号, 不做处理. 即, 不管信号
- 接收到信号, 自定义处理. 即, 由用户接收信号, 并自定义处理动作
signal()
捕捉信号
signal()
是一个系统调用接口, 用于捕捉进程信号, 并由用户处理:sighandler_t signal(int signum, sighandler_t handler);
sighandler_t
只是一个 返回值为空 参数为一个int类型 的函数指针: void (*)(int)
signal()
函数的返回值是一个函数指针, 其第二个参数也需要传入一个函数指针signal()
的参数:-
int signum
, 为signal()
的第一个参数, 需要传入指定的信号编号, 表示 捕捉此信号 自定义处理 -
sighandler_t handler
, 为signal()
的第二个参数, 需要传入一个 返回值为空 参数为一个int
类型 的函数指针, 其实可直接传入一个函数名
指定信号的自定义处理函数
.一般在函数的参数中传入一个函数指针, 此函数指针一般可能用于回调
此函数可被称为回调函数、回调方法
signal()
究竟是怎么使用的呢?有什么效果呢?#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int cnt = 1;
void handler(int signo) {
cout << "我是进程, 我捕捉到一个信号: " << signo << ", 这是第 " << cnt << " 次" << endl;
cnt++;
}
int main() {
signal(2, handler);
sleep(1);
cout << "进程已经设置完了" << endl;
while (true)
{
cout << "我是一个正在运行中的进程: " << getpid() << endl;
sleep(1);
}
return 0;
}
Ctrl+C
快捷键会给当前会话的所有前台进程发送2信号(SIGINT)
, 此信号的默认处理方式是: 从键盘中断进程signal()
将2信号的处理方式设置为一个自定义的回调函数Ctrl+C
会发生什么呢?Ctrl+C
快捷键 却不能中断进程了, 而是不断回调我们传入的函数 以自定义处理信号Ctrl+C
发送的SIGINT(2)
信号,signal()
的作用, 捕捉指定信号, 并自定义处理按理论来说,
signal()
是可以针对所有的普通信号进行捕捉的, 但实际上存在例外:
signal()
无法捕捉9信号(SIGKILL)
也就是说, 即使使用了
signal(9, handler);
, 给进程发送9信号, 进程依旧会默认处理:#include <iostream> #include <unistd.h> #include <signal.h> using std::cout; using std::endl; int cnt = 1; void handler(int signo) { cout << "我是进程, 我捕捉到一个信号: " << signo << ", 这是第 " << cnt << " 次" << endl; cnt++; } int main() { signal(9, handler); sleep(1); cout << "进程已经设置完了" << endl; while (true) { cout << "我是一个正在运行中的进程: " << getpid() << endl; sleep(1); } return 0; }
用户层产生进程信号的方式
键盘产生进程信号
进程信号可以由键盘产生
Ctrl+C
产生 2)SIGINT
信号系统调用产生进程信号
kill()
kill
除了是一个命令之外, 还存在一个同名系统调用kill()
:kill()
系统调用的用法其实 与 kill
命令相同kill
命令行命令是kill sig pid
, 而kill()
系统调用则是 kill(pid, sig)
kill()
系统调用的两个参数应该传入:pid_t pid
, 此参数传入需要发送进程的pidint sig
, 此参数传入需要发送的进程信号, 可以使用 信号编号 也可以使用 信号宏名
man
手册中提到, kill()
执行成功则返回0, 否则返回-1kill()
系统调用, 可以模仿实现一下 命令行的kill
命令:mykill.cc:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
void usage(const string& proc) {
cout << "Usage:\n\t" << proc << " sig pid" << endl;
}
// 模拟实现 kill命令
// ./mykill sig pid
int main(int argc, char* argv[]) {
if (argc != 3) {
usage(argv[0]); // argv[0] 即为命令行的第一个字符串
exit(1);
}
if (kill( (pid_t)atoi(argv[2]), atoi(argv[1]) ) == -1) {
cerr << "kill error, " << strerror(errno) << endl;
exit(2);
}
return 0;
}
./mykill
时, 会提示使用方式: ./mykill sig pid
raise()
raise()
也是一个系统调用接口:kill()
的功能是给指定的进程发送信号, 那么raise()
就是 给调用者发送信号, 即 给自己发送信号raise()
的调用结果是什么呢?#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int cnt = 1;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << ", 这是第 " << cnt << " 次" << endl;
cnt++;
}
int main(int argc, char* argv[]) {
signal(2, handler);
while (true) { // 循环给自己发送 信号2
raise(2);
sleep(1);
}
return 0;
}
signal()
来捕捉2信号, 然后再循环使用 raise(2) 测试raise()发送信号的结果.raise()
的功能是 向自己发送信号abort()
abort()
是一个使用和作用更加简单的系统调用:
man
手册中关于abort()
系统调用的描述的大致意思是:首先, 解除对
SIGABRT
信号的阻塞, 再向调用的进程发送SIGARBT
信号这会导致进程异常终止, 除非
SIGABRT
信号被捕捉, 且自定义的处理信号的函数返回并且,
abort()
函数导致进程终止, 会 关闭并刷新 进程打开的所有流如果,
SIGABRT
信号被忽略 或 被捕捉且处理信号的函数会返回, 则abort()
函数仍然会将进程终止是如何实现的呢?
操作系统, 会恢复进程对
SIGABRT
信号的默认配置, 并通过二次发送信号, 达到终止进程的目的
abort()
一般情况下一定会使进程异常退出, 无论SIGABRT
信号是被忽略还是被捕捉abort()
会向自己发送 SIGABRT
信号, SIGABRT
信号的编号是什么呢?abort()
函数:#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int cnt = 1;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << ", 这是第 " << cnt << " 次" << endl;
cnt++;
}
int main(int argc, char* argv[]) {
signal(2, handler);
while (true) { // 循环给自己发送 信号2
raise(2);
sleep(1);
if (cnt > 5)
abort();
}
return 0;
}
abort()
使进程异常退出abort()
之前, 捕捉了 SIGABRT
信号, 会如何呢?#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int cnt = 1;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << ", 这是第 " << cnt << " 次" << endl;
cnt++;
}
int main(int argc, char* argv[]) {
signal(2, handler);
signal(SIGABRT, handler); // 捕捉 SIGABRT 信号
while (true) { // 循环给自己发送 信号2
raise(2);
sleep(1);
if (cnt > 5)
abort();
}
return 0;
}
abort()
终止了SIGABRT
信号时不同的是, 可以明显看出来 abort()
实际上是调用了两次才成功终止了进程abort()
, 给进程发送的 SIGABRT
信号被捕捉了软件条件产生进程信号
SIGALRM
是一个软件条件产生的信号. 我们可以在程序内 调用 alarm() 系统调用来设置闹钟
.终止进程
#include <iostream>
#include <cerrno>
#include <unistd.h>
using std::cout;
using std::endl;
int cnt = 0;
int main(int argc, char* argv[]) {
alarm(1); // 设置 1s 的闹钟
while (true) {
cout << "count: " << cnt++ << endl;
}
return 0;
}
1s 内的 I/O 次数, 不等于1s内硬件计算的次数, 如果将代码改为下面这样:
#include <iostream> #include <unistd.h> #include <signal.h> using std::cout; using std::endl; int cnt = 0; void handler(int signo) { cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << endl; cout << "count: " << cnt << endl; exit(1); } int main(int argc, char* argv[]) { signal(SIGALRM, handler); alarm(1); // 设置 1s 的闹钟 while (true) { cnt++; } return 0; }
可以看到, 最终的执行结果就是 亿级的, 而不是万级
这才是, 计算的速度.
所以说,
I/O速度 相比较于硬件的运行速度 是非常的慢的
不是这个系统调用本身产生的
.只是设置了一个条件
.硬件异常产生进程信号
除0
、解引用空指针
或 越界访问
操作:#include <iostream>
int main() {
// 越界访问
int arr[10];
arr[100000] = 0;
// 解引用空指针
//int* pi = nullptr;
//*pi = 10;
// 除0
//int i = 10;
//i /= 0;
return 0;
}
浮点异常
的错误段错误
段错误
进程崩溃的本质是什么呢?
进程收到了异常信号
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << endl;
exit(1);
}
int main() {
// 捕捉进程, 自定义处理
for(int sig = 1; sig <= 31; sig++) {
signal(sig, handler);
}
// 越界访问
int arr[10];
arr[100000] = 0;
// 解引用空指针
//int* pi = nullptr;
//*pi = 10;
// 除0
//int i = 10;
//i /= 0;
return 0;
}
-
越界访问:
-
解引用空指针:
-
除0:
本质上是 异常错误产生了相应的信号 并发送给了进程, 进而才导致了进程的退出
越界访问和解引用空指针
会产生信号11, 而 除0
会产生信号8. 这两个信号在Linux系统中, 可以看到:除0 和 越界访问、野指针 如何产生相应信号
除0
进程的所有非图形计算操作实际上都是由CPU执行的
.将状态寄存器设置为有错误
的状态: 浮点数异常.- 操作系统需要识别是谁报的错, 即 操作系统需要知道是哪个进程报的错
- 操作系统需要识别是什么错误, 即 操作系统需要知道进程因为什么出现了错误
越界访问、野指针
通过虚拟地址转化为物理地址, 再找到物理内存, 在读取访问对应的数据或代码
.- 操作系统需要识别是谁报的错, 即 操作系统需要知道是哪个进程报的错
- 操作系统需要识别是什么错误, 即 操作系统需要知道进程因为什么出现了错误
都会在硬件上体现出来, 然后产生相应的信号
, 再通过操作系统发送给进程.进程崩溃一定会使进程退出吗?
进程崩溃的本质是进程收到了信号, 不能再正常运行了
. 而信号的处理 处理默认情况, 还是有其他情况的. 比如, 当我们把指定的信号捕捉并自定义处理方法时, 进程就不会退出了#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int cnt = 0;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << ", count: " << cnt++ << endl;
}
int main() {
// 捕捉进程, 自定义处理
signal(8, handler);
// 除0
int i = 10;
i /= 0;
return 0;
}
signal(8, handler);
捕捉并自定义处理了 8信号, 并且处理方法不会使进程退出. 在此基础上, 操作系统因为检测到了浮点异常错误, 就会不停的向进程发送 8信号, 进程也会不停的接收并处理此信号. 但是由于处理方法并不涉及进程的退出, 所以进程不会退出.core dump
低16位
就可以了, 此低16位中的高8位 用来表示退出码, 低8位 用来表示退出信号
暂时只需要关注低7位, 其中最高位是一个单独的 core_dump 标志, 暂时忽略
博主有关进程控制的文章的相关链接:
可以是一个动作 叫做 内存快照
.可以是 stutas 整型中的一个标记位
*, 此标记位 表示进程是否执行了 core dump 操作, 如果 执行了 core dump标记位就会被置为1
, 否则会被置为0
.man 7 signal
命令, 来查看man手册中记载的有关进程信号的部分详细信息. 其中记录着各信号以及其编号.Action一栏
. 此栏中 记录着 Core 的信号被进程接收到之后, 进程就有可能会发生 core dump操作
, 如果执行了, 对应的 进程退出信息中的 core dump 标志位就会被设置为1.#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
int main() {
pid_t id = fork();
if(id == 0) {
// 子进程 除0错误, 应该接收到 8信号
int i = 10;
i /= 0;
exit(1);
}
// 父进程等待子进程
int status = 0;
waitpid(id, &status, 0);
printf("exitcode: %d, signo: %d, core dump flag: %d\n", (status>>8) & 0xFF, status & 0x7F, (status>>7)&0x1);
return 0;
}
ulimit -a
可以查看系统的一部分相关设置:使用的是虚拟机的话, 这个设置应该是有数值的, 也不会出现进程不执行 core cump的情况
ulimit -c 20
将 core file size 设置为 20 之后, 再执行上面的代码程序:core file size
设置就是 设置系统可生成的 core文件的数量, 服务器默认会设置为0core file size
时, 即使接收到了某些会执行 core dump操作的信号, 也不会执行 core dumpcore dump 操作其实就是将进程的内存信息和当时的部分运行状态 "快照" 下来, 存储到 core 文件中
core.进程pid
core 文件有什么用?
-
首先, 我们将上面的代码重新 以调试模式 编译链接一下:
g++ -g mykill.cc -o mykillg
-
然后再执行
./mykillg
, 会生成一个新的 core文件 -
然后我们使用 gdb 调试进程:
gdb mykillg
-
在 gdb 调试界面, 直接输入
core-file core.2127
可以发现, 我们通过gdb调试进程时使用core文件, 可以
直接定位出 进程上次运行的错误位置、信息
进程信号在内核中的表示
进程信号相关概念
- 进程 执行对进程信号的实际处理 动作, 称为
信号递达
, 传递的递 - 进程信号 从产生到递达之间的状态, 称为
信号未决
. 即 信号接收到了, 但是没处理的状态 - 进程 可以选择
阻塞某种进程信号
. 即 接收到了, 但是阻塞信号递达, 就是不做处理 - 被阻塞信号产生时, 将保持未决状态, 直到进程解除对此信号的阻塞, 信号才会递达
- 信号阻塞 和 忽略不同,
忽略是处理的行为, 即信号已经递达
. 而阻塞是递达之前的状态
在内核中表示
pending 位图
:表示进程收到的信号, 对应位置即为对应编号的信号
. 当pending位图中的某位为1, 即表示此位的信号在进程中处于未决状态
. 即 接收了但是还未处理handler 指针数组
:存储进程信号处理方法的数组, 每位对应一个处理方法
. 上图中, 即表示 SIGHUP(1) 信号的处理方法是 SIG_DFL, SIGINT(2) 的处理方法是 SIG_IGN, SIGQUIT(3) 的处理方法是 用户处理方法(sighandler)……一次类推block 位图
:指定位置为1时, 即表示此位置信号会被阻塞
. 上图中 则表示 SIGINT(2) 和 SIGQUIT(3) 被阻塞.pending 位图表示的是进程接收信号的情况, block 位图表示的是进程阻塞信号的情况, 而 handler 数组表示的是指定信号的对应处理方法
- SIGHUP(1) 信号, 进程对此信号的处理方法是 SIG_DFL, 进程并没有收到此信号(pending为0), 也没有阻塞此信号递达(block为0).
- SIGINT(2) 信号, 进程对此信号的处理方法是 SIG_IGN, 进程收到了此信号(pending为1), 但是进程会阻塞此信号递达(block为1), 即 进程收到的信号会一直处于未决状态(pending一直为1). 除非阻塞解除
- SIGQUIT(3) 信号, 此进程对此信号的处理方法是自定义的 sighandler(), 进程没有收到此信号(pending为0), 但是进程会阻塞此信号递达(block为1), 也就是说 即使进程收到了此信号, 此信号也会一直处于未决状态, 除非阻塞解除
sigset_t
进程阻塞了此信号
, 且此信号已经处于未决状态了, 即使 再多次的向进程发送此信号
, 当阻塞解除时 进程最终也只会处理一次此信号
(当然 如果不存在阻塞, 且一直向进程发送信号, 那么进程就会一直处理)sigset_t
sigset_t
是一个 typedef 出来的类型, 实际上是一个结构体__sigset_t
, 不过这个结构体内部只有一个 unsigned long int
类型的数组数组
表现出来的sigset_t
形式表现的 pending位图, 被称为 未决信号集
; 同样以 sigset_t
形式表现的 block位图, 被称为 阻塞信号集
, 也叫 信号屏蔽字
信号集操作
为什么 sigset_t 结构体中的数组大小不固定?
这是 此结构体的实际内容.
其中 __val数组的大小是
_SIGSET_NWORDS
, 这是一个宏定义. 而 宏的内容是(1024 / (8 * sizeof (unsigned long int)))
而不同配置平台的 unsigned long int 类型的大小可能是不同的, 所以 数组的大小也可能不同.
不能直接访问此数组来对信号集进行操作
. 所以 操作系统为我们提供了一些系统调用int sigpending(sigset_t *set);
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);
-
int sigpending()
:使用此接口, 可以获取进程的未决信号集内容, 传入的 sigset_t 指针是一个输出性参数, 获取的未决信号集内容会存储在传入的变量中
但是, 并不能通过 修改获取到的未决信号集内容 想要一次来修改进程当前的未决信号集.
成功返回0, 错误返回-1
-
int sigemptyset()
:调用此函数, 会将传入的信号集初始化为空, 即所有信号、阻塞会被消除, 信号集的所有位设置为0
成功返回0, 错误返回-1
-
int sigfillset()
:调用此函数, 会将传入的信号集所有位设置为1.
成功返回0, 错误返回-1
-
int sigaddset()
和int sigdelset()
:sigaddset()
的作用是, 给指定信号集中添加指定信号, 即 将指定信号集中的指定位置设置为1sigdelset()
的作用是, 删除指定信号集中的指定信号, 即 将指定信号集中的指定位置设置为0着两个函数, 都是成功返回0, 失败返回-1.
-
int sigismember()
:调用此函数, 可以判断 信号集中是否有某信号. 即 判断信号集的某位是否为1
如果 信号在信号集中 返回1, 如果不在 返回0, 如果出现错误 则返回-1
sigprocmask()
sigprocmask()
的使用稍微复杂一些:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
, 从参数来看就比上面的接口要复杂的多.获取 和 修改 信号屏蔽字
. 这个接口的使用相对来说比较复杂假设,
当前进程的 信号屏蔽字(阻塞信号集) 为 mask
-
首先介绍
const sigset_t *set
也就是第二个参数第二个参数需要传入一个信号集, 此信号集是
修改进程的信号屏蔽字(mask)用的
.此参数
需要根据 how(第一个参数) 的不同, 来传入不同意义的信号集
-
然后是
sigset_t *oldset
第三个参数第三个参数也是需要传入一个信号集, 不过一般传入被全部置0的信号集.
此参数是一个输出型参数, 用于获取没做修改的 mask, 即函数执行结束后,
此参数会获取没有执行此函数时的mask
. -
最后介绍
int how
第一个参数how 是一个整型参数, 需要传入系统提供的宏. 不同宏的选择此函数会有不同的功能, 就需要传入不同意义的 set(第二个参数)
how set的意义 函数功能 SIG_BLOCK set的内容为 需要添加阻塞的信号的位置为1 在mask中 为set指定的信号 添加阻塞. 以位图的角度可以看作 mask |= set SIG_UNBLOCK set的内容为 需要解除阻塞的信号的位置为1 在mask中 为set指定的信号 解除阻塞. 以位图的角度可以看作 mask &= ~set SIG_SETMASK set的内容为 需要指定设置的mask 将set设置为mask. 以位图的角度可以看作 mask = set
-
如果需要为指定位置添加阻塞:
其实就是 将传入的 set 与进程原来的信号屏蔽字 做
按位或操作
, 最终结果 作为进程最新的信号屏蔽字 -
如果需要为指定信号解除阻塞:
其实就是 将传入的
set先按位取反
, 再与进程原来的信号屏蔽字 做按位与操作
. 最终结果 作为进程的新的信号屏蔽字 -
如果需要直接设置信号屏蔽字:
其实就是, 直接将传入的
set 覆盖进程原来的信号屏蔽字
, 即将传入的set 作为进程新的信号屏蔽字
信号集操作相关代码演示
对信号屏蔽字做修改, 并向进程发送信号 将进程的未决信号集打印出来查看
:#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::cerr;
using std::endl;
int cnt = 0;
void handler(int signo) {
cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号: " << signo << ", count: " << cnt++ << endl;
}
// 打印信号集的函数
void showSignals(sigset_t *signals) {
// 使用 sigismember() 接口判断 31个普通信号是否在信号集中存在
// 存在的信号输出1, 否则输出0
for(int sig = 1; sig <= 31; sig++) {
if(sigismember(signals, sig)) {
cout << "1";
}
else {
cout << "0";
}
}
cout << endl;
}
int main() {
// 先输出进程的 pid
cout << "pid: " << getpid() << endl;
// 定义sigsetmask()需要使用的 set 和 oldset, 并初始化
sigset_t sigset, osigset;
sigemptyset(&sigset);
sigemptyset(&osigset);
// 将进程的 所有普通信号屏蔽
for(int sig = 1; sig <= 31; sig++) {
sigaddset(&sigset, sig);
signal(sig, handler);
}
sigprocmask(SIG_BLOCK, &sigset, &osigset);
// 获取并打印进程的未决信号集
sigset_t pendings;
while(true) {
sigemptyset(&pendings);
sigpending(&pendings);
showSignals(&pendings);
sleep(1);
}
return 0;
}
先将进程的所有普通信号阻塞
, 然后 再循环获取进程当前的未决信号集并打印出来
解除信号阻塞之后, 对应信号递达了
深入理解进程处理信号 **
进程会在合适的时候将信号递达
什么时候才是合适的时候?
进程究竟会在什么时候处理信号?
当 进程从内核态, 转换为用户态的时候, 进程会进行信号的检测与处理
什么是内核态? 什么又是用户态? 什么是内核态和用户态的转换?
进程的内核态 和 用户态
用户空间部分 与 物理内存
之间的相互页表映射还存在一张页表 用于 内核空间 与 物理内存 之间相互映射, 被称为内核级页表
与用户级的页表不同, 进程地址空间的内核空间 与 物理内存之间的映射页表, 整个操作系统只有一张
, 也就是说操作系统中 所有进程共用一张 内核级页表
. 即:物理内存中 只加载着一份有关进程内核空间内容的数据和代码
如果
每个进程都可以随便访问内核空间, 那其实就是说 每个进程都可以随便修改 物理内存中只有着一份的、所有进程共享的数据和代码.进程会分为两种状态
: 内核态
和 用户态
需要访问、调用、执行 内核数据 或 代码(中断、陷阱、系统调用等)时
, 就会 陷入内核, 转化为内核态
, 因为只有 进程处于内核态时, 才有权限访问内核级页表, 即有权限访问内核数据与代码
不需要访问、调用、执行 内核数据 或 代码, 或进程时间片结束时
, 就会 返回用户, 转化为用户态
, 此时 进程将不具备访问内核级页表的权限, 只能访问用户级页表
保护 内核级数据和代码
. 也就是 进程在发生从内核态转换为用户态的过程时, 会检测进程的信号并处理
那么,
操作系统如何判断进程当前的状态呢?
CPU内部存在一个 状态寄存器CR3, 此寄存器内有比特标识位表示当前进程的状态:
- 若标识位 表示0, 则表明进程此时处于内核态
- 若标识位 表示3, 则表明进程此时处于用户态
深入理解信号处理 *
进程会有无数次的状态转换
. 为什么呢?没有资格直接访问系统级的软硬件资源
的 本质上都会直接或间接地去调用系统接口(printf、scanf……), 然后通过操作系统 直接或间接地访问一些系统级的软硬件资源
. 操作系统作为所有硬件和软件的管理者肯定是要提供这样的功能的.无数次地陷入内核(切换状态, 切换页表)
, 再访问内核代码数据, 然后完成访问, 再将结果返回给用户(切换状态, 切换页表)
, 最终用户得到结果.如果编写的程序不调用任何函数呢?
只在 main函数内部使用一个 while(1);
使进程死循环的运行. 那么, 此进程还会无数次的发生状态转换吗?
会的
只要时间片用完了, 那么就需要将此进程从CPU上剥离下来
, 而剥离操作一定是操作系统做的, 那么也就是说将 进程从CPU上剥离下来也是需要陷入内核执行内核代码的
. 将进程从CPU上剥离下来的时候, 需要维护一下进程的上下文, 以便下次接着执行进程的代码.进程被剥离下来, 进程会进入内核态维护起来. 等待下次运行时, 又会回到用户态执行代码.
即使一个进程什么实际作用都没有, 这个进程的运行过程中, 也会发生无数次的内核态与用户态的转换
- 进程代码运行到open()需要
陷入内核
执行open()代码. - 陷入内核并执行完open()代码后, 需要将open()结果返回给用户, 需要转换回用户态
- 在转换回用户态之前, 需要先在进程PCB中检测进程的未决信号集
- 在未决信号集中, 检测到1和2信号未决, 并且均为被屏蔽(阻塞). 就需要在handler数组中寻找指定的处理方法
- 1信号默认处理, 需要执行内核中的默认处理方法(一般为进程终止); 2信号忽略处理, 直接将未决信号集中2信号改为0
- 处理完信号, 再将open()结果返回给用户, 这个过程需要
转换为用户态
如果进程的未决信号中存在着用户自定义的处理方法, 又该是怎样的处理方式呢?
先去执行用户代码将信号处理了
, 然后再将open()的结果返回给用户.进程执行用户自定义的信号处理函数时, 进程应该以内核态执行还是以用户态执行呢?
进程 需不需要切换回用户态
**之后, 再执行用户自定义函数呢?肯定需要
的.假如用户代码存在一些 损坏系统的恶意代码
. 这些恶意代码在用户态是没有权限执行
的, 而内核态肯定有权限
.以内核态的身份执行用户代码
, 也就意味着进程有权限执行恶意代码, 那么系统就会被损坏
.当进程需要执行用户方法去处理进程信号时, 进程还会先转换回用户态去执行.
执行完用户处理方法之后, 进程还需不需要再陷入内核, 然后再返回用户?
需要再陷入内核
此时是无法返回到进程原本代码的执行位置的.
因为 进程执行内核代码之后的返回信息 还在内核中
, 以用户态的身份是无法访问并返回给用户的. 所以, 进程在以用户态的身份执行过信号的用户处理方法之后, 还需要再次陷入内核, 然后根据内核中的返回信息使用特定的返回调用 返回到用户.- 进程代码运行到open()需要
陷入内核
执行open()代码. - 陷入内核并执行完open()代码后, 需要将open()结果返回给用户, 需要转换回用户态
- 在转换回用户态之前, 需要先在进程PCB中检测进程的未决信号集
- 在未决信号集中, 检测到3信号未决, 并且均为被屏蔽(阻塞). 就需要在handler数组中寻找指定的处理方法
- handler数组中, 3信号的处理方法是用户自定义的, 所以需要
换回用户态
去执行用户级代码 - 以用户态执行完自定义信号处理方法之后, 不能直接返回到用户, 需要
再次陷入到内核
- 再次陷入内核之后, 获取返回信息 再调用特定的返回调用, 返回到用户. 这个过程需要
转换回用户态
需要执行用户自定义的处理方法
时, 那么 从调用内核代码到返回用户 的整个过程一共需要 经历4次状态转换
不需要执行用户自定义处理方法
时, 那么 从调用内核代码到返回用户 的整个过程 就只需要 经历2次状态转换
无穷∞画法
这里, 进程执行完用户自定义信号处理方法 返回内核之后, 之后的执行流程与PCB信号集有一个交点.
此交点表示, 此时
还会进行 信号集的检测
.如果此时又有信号未决, 并且时间片充足, 那么就会再次处理.
在进程处理信号时, 如果操作系统还向进程发送相同的信号, 进程时不会处理的. 因为pending信号集中 只能表示信号是否存在, 而不能记录信号被发送过来的次数. 也就是说, 信号未决时, 依旧有相同的信号发送过来, 进程不会处理后续的信号.
深入理解信号捕捉
signal()
用来捕捉信号, 并自定义处理.signal()
之外, Linux操作系统还为我们提供了另一个系统调用, 来对信号进行捕捉.sigaction()
更复杂
一些, 但最终要实现的功能与signal()
是一样的:sigaction():
signal()
要复杂的多:-
第一个参数
int signum
, 很明显 这个参数就需要传入指定的进程信号, 表示要捕捉的信号
-
第二个参数
const struct sigaction *act
, 这个参数很奇怪, 它与此函数同名, 并且是一个结构体指针这个结构体的内容是什么?
在man手册中, 可以看到
struct sigaction
的内容一共有5个:void (*sa_handler)(int);
, 此成员的含义其实就是自定义处理信号的函数指针
;void (*sa_sigcation)(int, siginfo_t *, void *);
, 此成员也是一个函数指针. 但是这个函数的意义是用来 处理实时信号的, 不考虑分析. (siginfo_t 是表示实时信号的结构体)sigset_t sa_mask;
, 此成员是一个信号集, 这个信号集有什么用呢?我们在使用时解释int sa_flags;
, 此成员包含着系统提供的一些选项, 本篇文章使用中都设置为0void (*sa_restorer)(void);
, 很明显 此成员也是一个函数指针. 但我们暂时不考虑他的意义.
也就是说, 我们暂时可以知道,
sigaction()
的第二个参数是一个结构体指针, 并且指向的结构体里有一个成员是用来自定义处理信号
的此参数的作用就是, 将指定信号的处理动作改为传入的
struct sigaction
的内容 -
struct sigaction *oldact
, 第三个参数看起来似曾相识, 好像我们在介绍sigprocmask()
接口时的第三个参数其实这两个函数的第三个参数的作用是相似的, 都是一个输出型参数.
在
sigaction()
这个函数中, 第三个参数的作用是获取 此次修改信号struct sigaction
之前的原本的struct sigaction
如果传入为空指针, 则不获取
#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
void handler(int signo) {
cout << "获取到一个信号,信号的编号是: " << signo << endl;
sigset_t pending;
// 增加handler信号的时间,永远都会正在处理2号信号!
while (true) {
sigpending(&pending);
for (int i = 1; i <= 31; i++) {
if (sigismember(&pending, i))
cout << '1';
else
cout << '0';
}
cout << endl;
sleep(1);
}
}
int main() {
// 先定义两个 struct sigaction 用于传参
struct sigaction act, oact;
// 初始化 act
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
// sigaction 捕捉 2信号
sigaction(2, &act, &oact);
while (true) {
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
signal()
捕捉信号相同, 但是使用要麻烦一些:sigaction()
捕捉2信号成功3信号(Ctrl+\)
时, 进程依旧会去处理3信号.sa_mask
了:#include <iostream>
#include <signal.h>
#include <unistd.h>
using std::cout;
using std::endl;
void handler(int signo) {
cout << "获取到一个信号,信号的编号是: " << signo << endl;
sigset_t pending;
// 增加handler信号的时间,永远都会正在处理2号信号!
while (true) {
sigpending(&pending);
for (int i = 1; i <= 31; i++) {
if (sigismember(&pending, i))
cout << '1';
else
cout << '0';
}
cout << endl;
sleep(1);
}
}
int main() {
// 先定义两个 struct sigaction 用于传参
struct sigaction act, oact;
// 初始化 act
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); // 在 act.sa_mask 中设置 3信号
// sigaction 捕捉 2信号
sigaction(2, &act, &oact);
while (true) {
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
sigaction()
的第二个参数中的sa_mask
信号集中, 添加 3信号.进程捕捉到指定信号并自定义处理的同时, 阻拦3信号的递达
struct sigaction
结构体的sa_mask
成员的意义是, 添加进程在处理捕捉到的信号时对其他信号的阻塞
. 如果需要添加对其他信号的阻塞, 那么就可以继续在 sa_mask
中添加其他信号.防止用户自定义处理信号时, 嵌套式的发送其他信号并捕捉处理
****.sa_mask
来进行对其他信号的拦截阻塞.信号捕捉技巧
signal()
或 sigaction()
传入不同的信号以及对应的不同的处理方法进行对信号的捕捉.signal()
或sigaction()
捕捉信号时, 只传入相同的函数指针就可以实现 对不同信号不同处理:handlerAll(int signo)
函数, 并使用 switch 语句, 将不同的 signo 分别处理.如果需要捕捉的信号过多, 也可以使用 一定的数据结构 将所有的信号自定义处理函数存到数组结构中, 然后再通过指定方法进行对信号的分别处理.
signal()
或者 sigaction()
捕捉信号时, 就只需要统一传入 handlerAll
的函数指针就可以了.可重入函数
全局的单链表
结构. 并且此时需要执行一个节点的头插操作:node1->next = head;
head = node1;
刚执行完第一步
之后, 进程因为硬件中断或者其他原因 陷入内核了
.node2
的头插操作, 执行完毕的结果就是:node2 成为了链表的第一个节点 head
node1->next = head;
head = node1;
, 结果就成了这样:node2
无法被找到了.内存泄漏
, 这是一个很严重的问题重入
可能因为重入而造成数据错乱
, 这样的函数 被称为 不可重入函数
, 即此函数不能重入, 重入可能会发生错误一个函数即使重入
, 也不会发生任何错误
(一般之访问函数自己的局部变量、参数), 这样的函数就可以被称为 可重入函数
. 因为每个函数自己的局部变量是独立于此次函数调用的, 再多少次调用相同的函数, 也不会对之前调用函数的局部变量有什么影响.如果一个函数符合以下条件之一则是
不可重入
的:
- 调用了
malloc
或free
, 因为malloc
也是用全局链表来管理堆的- 调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构
volatile 关键字
volatile
也是其中之一. 下面分析一下这个关键字的作用myproc.c(注意是C语言文件):
#include <stdio.h>
#include <signal.h>
int flags = 0;
void handler(int signo) {
printf("获取到一个信号,信号的编号是: %d\n", signo);
flags = 1;
printf("我修改了flags: 0->1\n");
}
int main() {
signal(2, handler);
while (!flags)
;
// 未接收到信号时, flags 为 0, !flags 恒为真, 所以会死循环
printf("此进程正常退出\n");
return 0;
}
正常编译
运行的结果是:可能对 flags 做出优化
.while(!flags);
判断时, CPU会从内存中拿出数据进行判断. 当flags从0变为1时, 是内存中的数据发生了变化, CPU也会从内存中拿到新的数据进行判断
while(!flags);
判断时, CPU读取到flags为0 并存放在寄存器中之后, 为了节省资源 在之后的判断中 CPU 不会再从内存中读取数据, 而是直接根据寄存器中存储的数据进行判断. **while(!flags);
判断时, CPU依旧会只根据寄存器中存储的0 来进程判断, 这就会造成 进程不会正常退出gcc
编译时, 使用 -O2
选项 让编译器做出这样的优化:volatile int flags = 0; // 全局变量
volatile
关键词的作用, 即 保持内存的可见性
. 告知编译器,被该关键字修饰的变量, 不允许被优化, 对该变量的任何操作, 都必须在真实的内存中进行操作
SIGCHLD 信号
让父进程主动去询问子进程是否退出是否需要接收退出信息
子进程退出时, 会向父进程发送一个信号, 即 SIGCHLD 信号
**SIGCHLD信号
就可以了:#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <unistd.h>
using std::cout;
using std::endl;
int flags = 0;
void handler(int signo) {
cout << "子进程退出, 我收到了信号: " << signo << "我是: " << getpid() << endl;
}
int main() {
signal(SIGCHLD, handler);
pid_t id = fork();
if (id == 0) {
// 子进程
while (true) {
cout << "我是子进程, pid: " << getpid() << endl;
sleep(1);
}
exit(0);
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
}
return 0;
}
ignore
忽略暂停信号是 19, 继续信号是 18
waitpid()
会等待子进程退出, 而等待的动作是主动去询问子进程是否退出.#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
void freeChild(int signo) {
assert(signo == SIGCHLD);
pid_t id = waitpid(-1, nullptr, 0);
if(id > 0) {
cout << "父进程等待子进程成功, child pid: " << id << endl;
}
}
int main() {
signal(SIGCHLD, freeChild);
pid_t id = fork();
if (id == 0) {
// 子进程
while (true) {
cout << "我是子进程, pid: " << getpid() << endl;
sleep(1);
}
exit(0);
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
waitpid()
回收子进程.
waitpid()
, 第一个参数应该传入回收子进程的pid, 不过-1 表示回收任意子进程
第三个参数传入 0 表示
阻塞等待
.
同时创建多个子进程, 并将这些子进程同时退出
, 会出现什么状况:#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
void freeChild(int signo) {
assert(signo == SIGCHLD);
pid_t id = waitpid(-1, nullptr, 0);
if (id > 0) {
cout << "父进程等待子进程成功, child pid: " << id << endl;
}
}
int main() {
signal(SIGCHLD, freeChild);
for (int i = 0; i < 10; i++) {
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 10;
while (cnt) {
cout << "我是子进程, pid: " << getpid() << ", cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出, 进入僵尸状态" << endl;
exit(0);
}
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
子进程从创建到退出的整个过程
.有几个子进程以僵尸状态 残留下来
, 并没有被父进程回收掉.太多子进程在同一时间退出了, 即太多相同的信号在同一时间被发送给父进程了
, 而进程处理信号是需要时间的, 当前信号没有被处理完毕时, 是不会记录后面有多少信号发送过来的.有一部分子进程发送信号的时候 父进程还在处理其他子进程的信号, 父进程并没有接收到这一部分子进程的信号
. 所以没有回收这一部分子进程.void freeChild(int signo) {
assert(signo == SIGCHLD);
while(true) {
pid_t id = waitpid(-1, nullptr, 0);
if (id > 0) {
cout << "父进程等待子进程成功, child pid: " << id << endl;
}
else {
cout << "等待结束" << endl;
break;
}
}
}
死循环回收
, 没有子进程需要回收的时候跳出循环, 应该就可以把所有子进程都回收掉:捕捉到信号之后, 如果有子进程一直不退出, 父进程代码不会再运行了. 因为调用的函数 会一直在死循环内, 回不到main函数中了
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
void freeChild(int signo) {
assert(signo == SIGCHLD);
while (true) {
pid_t id = waitpid(-1, nullptr, 0);
if (id > 0) {
cout << "父进程等待子进程成功, child pid: " << id << endl;
}
else {
cout << "等待结束" << endl;
break;
}
}
}
int main() {
signal(SIGCHLD, freeChild);
for (int i = 0; i < 10; i++) {
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 0;
if(i < 6)
cnt = 5; // 前6个子进程 5s就退出
else
cnt = 30; // 后4个子进程 30s 退出
while (cnt) {
cout << "我是子进程, pid: " << getpid() << ", cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出, 进入僵尸状态" << endl;
exit(0);
}
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
只要子进程一直在运行, 父进程就无法正常工作
进程就不会从信号处理函数中跳出来.waitpid()的第三个参数传入
WNOHANG
, 表示非阻塞等待
非阻塞等待, 在子进程没有退出时, 会返回0
. 这样就可以退出死循环, 结束信号处理函数的运行. 从而回到main函数中.#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
void freeChild(int signo) {
assert(signo == SIGCHLD);
while (true) {
pid_t id = waitpid(-1, nullptr, WNOHANG);
if (id > 0) {
cout << "父进程等待子进程成功, child pid: " << id << endl;
}
else if(id == 0){
cout << "还有子进程在运行, 但是没有子进程退出, 父进程要去做自己的事了 " << endl;
break;
}
else {
cout << "父进程等待所有子进程结束" << endl;
break;
}
}
}
int main() {
signal(SIGCHLD, freeChild);
// 为方便演示, 我们只创建5个子进程
for (int i = 0; i < 5; i++) {
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 0;
if(i < 2)
cnt = 5; // 前2个子进程 5s 就退出
else
cnt = 30; // 后3个子进程 30s 退出
while (cnt) {
cout << "我是子进程, pid: " << getpid() << ", cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出, 进入僵尸状态" << endl;
exit(0);
}
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
回收子进程的其他方式
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;
int main() {
signal(SIGCHLD, SIG_IGN);
// 为方便演示, 我们只创建5个子进程
for (int i = 0; i < 5; i++) {
pid_t id = fork();
if (id == 0) {
// 子进程
int cnt = 0;
if(i < 2)
cnt = 5; // 前2个子进程 5s 就退出
else
cnt = 30; // 后3个子进程 30s 退出
while (cnt) {
cout << "我是子进程, pid: " << getpid() << ", cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出, 进入僵尸状态" << endl;
exit(0);
}
}
// 父进程
while (true) {
cout << "我是父进程, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
signal()
手动对 SIGCHLD 信号设置了 SIG_IGN 忽略处理, 但是最终子进程却自动被回收了.只是为了更加方便的回收子进程, 可以直接捕捉并设置忽略.
作者: 哈米d1ch 发表日期:2023 年 4 月 8 日