23.Linux进程信号(四)
当main函数执行头插函数时,执行完头插的第一句之后,因某种原因进入内核,恰好执行对应信号的中断处理方法,二次执行完头插,回到main执行流之后,头插函数执行完第二句。子进程回收可以采用对SIGCHLD信号自定义捕捉的方式来完成,自定义捕捉中采用WNOHANG非阻塞调用+死循环来等待子进程,确保完全回收和不影响父进程。例子:如图,main函数中没有直接调用handler函数的语句,并没有关联。举例
理解用户态和内核态
结论:
以32位举例,进程地址空间分为 [0,3GB]用户区,[3,4GB]内核区。
进程地址空间有两张页表,内核页表 和 用户页表。
内核页表在物理内存中只有一份,用户页表有多份。
无论进程如何调度,我们总能找到操作系统。
OS为了保护自己,不相信任何人,必须采用系统调用的方式进行访问!
操作系统系统调用方法的执行,是在进程地址空间上执行的。
在OS中,OS或用户如知道现在处于内核态还是用户态?
在x86架构中,CPU通过当前特权级(CPL, Current Privilege Level)标识当前状态,存储在CS段寄存器的低2位。
- CPL=0:内核态(Ring 0)
- CPL=3:用户态(Ring 3)
系统调用
fork()
{
mov eax n;
syscall 0x80 //更改CPU特权级别,陷入内核
}
sigaction
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:需要自定义捕捉的信号编号
act:如何处理自定义捕捉方法
结构:
struct sigaction {
void (*sa_handler)(int); //自定义捕捉方法
void (*sa_sigaction)(int, siginfo_t *, void *); //...
sigset_t sa_mask; //需要额外屏蔽的信号集
int sa_flags; //...
void (*sa_restorer)(void); //...};
当某个信号处理函数被调用了,该信号就会被屏蔽,直到处理函数返回,信号屏蔽就会恢复到原来状态。(目的,为了防止在该信号处理函数过程中,又收到了该信号,造成类似递归调用)。
sa_mask就是在信号处理函数过程中需要额外屏蔽的信号集。验证信号屏蔽以及sigaction的使用:#include <iostream> #include <signal.h> #include <unistd.h> void handler(int signum) { std::cout << "信号捕捉到:" << signum << std::endl; // 循环打印pending表,观察2号信号屏蔽情况 while (true) { sigset_t set; sigpending(&set); for (int i = 31; i >= 1; i--) { if (sigismember(&set, i)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; sleep(1); } } int main() { // signal(2, handler); struct sigaction act, oact; // 此时额外的屏蔽信号集为{3,4,5} sigemptyset(&(act.sa_mask)); sigaddset(&act.sa_mask,3); sigaddset(&act.sa_mask,4); sigaddset(&act.sa_mask,5); // 自定义捕捉方法 act.sa_handler = handler; act.sa_flags = 0; sigaction(2, &act, &oact); while (true) { std::cout << "pid: " << getpid() << std::endl; sleep(1); } return 0; }
可重入函数
举例:一个全局链表,main函数中有头插,信号处理方法也有头插。
当main函数执行头插函数时,执行完头插的第一句之后,因某种原因进入内核,恰好执行对应信号的中断处理方法,二次执行完头插,回到main执行流之后,头插函数执行完第二句。此时node2节点就是一个不会被销毁的节点,丢失了,造成内存泄漏。
main执行流和handler执行流 -> insert方法,被两个以上的执行流进入了-> 函数被重入了
不可重入函数:重入有可能造成资源的丢失或行为异常,如果函数使用资源是全局共享且没有被保护,那就是不可重入的。
可重入函数:函数只有自己的临时变量。
volatile
volatile作用:保证资源的可见性。
CPU运算流程:从物理内存搬数据到寄存器->从寄存器取数据运算->或将结果写回内存
例子:如图,main函数中没有直接调用handler函数的语句,并没有关联。编译器优化情况比较高的情况下:flag->register->优化到寄存器中,只进行一次第一步,之后只有第二步,即使发信号修改了flag的内容,CPU依旧用寄存器中的原始数据(寄存器覆盖了进程看到变量的真实情况,内存不可见了)。
SIGCHLD信号
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。子进程回收可以采用对SIGCHLD信号自定义捕捉的方式来完成,自定义捕捉中采用WNOHANG非阻塞调用+死循环来等待子进程,确保完全回收和不影响父进程。若不关心子进程状态码,可以父进程使用signal(SIGCHILD, SIG_IGN); 在子进程退出时会自动回收。#include <iostream> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <unistd.h> void handler(int signum) { std::cout << "信号编号为:" << signum << std::endl; while (true) { pid_t n = waitpid(-1, nullptr, WNOHANG); if (n == 0) break; else if (n < 0) { std::cout << "等待错误" << std::endl; break; } } } int main() { // signal(SIGCHLD, handler); signal(SIGCHLD,SIG_IGN); //等同于信号捕捉方式回收,只不过拿不到状态码 for (int i = 0; i < 10; i++) { pid_t pid = fork(); if (pid == 0) { std::cout << "child exit" << std::endl; if(i<=5) exit(1); while(true) {;} } } while (true) { std::cout << "father running" << std::endl; sleep(1); } return 0; }
更多推荐
所有评论(0)