当前位置: 首页 > news >正文

【Linux】进程 信号保存 信号处理 OS用户态/内核态

🌻个人主页:路飞雪吖~

       🌠专栏:Linux


目录

一、信号保存

✨进程如何完成对信号的保存?

 ✨在内核中的表示

✨sigset_t

✨信号操作函数

🪄sigprocmask --- 获取或设置当前进程的 block表

🪄sigpending --- 获取当前的pending信号集

二、信号捕捉

✨信号捕捉的流程

✨sigaction​编辑

✨操作系统是怎么运行的

🍔硬件中断

🍔时钟中断

🍔死循环

🪄时间片

🍔软中断

🍔缺页中断?内存碎片处理?除零野指针错误?

三、🌟如何理解内核态和用户态

四、可重入函数

五、volatile --- 易变关键字(保持内存可见性)

六、SIGCHLD信号


一、信号保存

🌠信号相关常见概念

• 实际 执行信号 的处理动作称为 信号到达;

• 信号从 产生到递达 之间的状态,称为信号未决;

• 进程可以选择 阻塞某个信号【阻塞特定信号[叫屏蔽信号,与IO阻塞没有任何联系],信号产生了,一定把信号进行pending(保存),永远不递达,除非我们解除 阻塞】;

• 被阻塞的信号产生时,将保持在 未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;

• 阻塞 和 忽略 是不同的,只要信号被阻塞 就不会递达,而忽略是在递达之后可选的一种处理动作。

✨进程如何完成对信号的保存?

pending [信号]位图 :当前进程收到的信号列表;

handler_t XXX[N] :函数指针数组,指向信号的处理方法

   信号编号 -1 :就是函数指针数组的下标!

block [屏蔽]位图:是否屏蔽信号;

• pending 例子: 当【kill -2】发送2号信号,从右往左的第二个 比特位 由 0 --> 1,此时就向该进程发了一个 2号 信号,从右往左的 第几个 比特位 信号就是几.

• block 例子:SIGINT(2) 信号2,当前的block位 为 1, 表示的是把2号信号屏蔽,即便pending 收到了 2号 信号,这个 2号 信号 也无法执行对应的 handler,即禁止2号信号进行递达,除非把 2号 信号 的 block 位 由 1 --> 0,pending 的2号信号才会执行对应的方法 。

 屏蔽一个信号[block] 和 当前是否收到这个信号[pending] 两者是没有关系的,因为它们是两个位图,修改时没有联系。

进程能识别信号本质是:每一个进程的每一个信号都能横着看这三张表来识别信号,是程序员内置的特性【这些代码的数据结构内核都是程序员写的】。

 ✨在内核中的表示

// 内核结构 2.6.18 
struct task_struct {.../* signal handlers */struct sighand_struct *sighand;sigset_t blockedstruct sigpending pending;...
}struct sighand_struct {atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_t siglock;
};struct __new_sigaction {__sighandler_t sa_handler;unsigned long sa_flags;void (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t sa_mask;
};struct k_sigaction {struct __new_sigaction sa;void __user *ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending {struct list_head list;sigset_t signal;
};

✨sigset_t

从上图来看,每个信号只有⼀个bit的未决标志,非0即1, 不记录该信号产生了多少次,阻塞标志也是这样 表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t 称为信号集 ,这个类型 可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号 是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask), 这⾥的“屏蔽” 应该理解为阻塞而不是忽略。

✨信号操作函数

不建议直接使用 位操作 来对位图直接进行设置、查找、检测。Linux直接提供了一组接口,可以用来对该信号集直接进行比特位的操作:

对位图增删查改:

#include <signal.h>

int sigemptyset(sigset_t *set); // 把对应的信号集 做 清空

int sigfillset(sigset_t *set); // 信号集 全部 置 1

int sigaddset(sigset_t *set, int signo); // 向指定的信号集当中,添加信号

int sigdelset(sigset_t *set, int signo); // 从指定的集合当中,移出某个信号

int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在集合里

• 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

• 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号 包括系统支持的所有信号。

• 注意,在使用sigset_t类型的变量之前,⼀定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于 确定的状态。初始化sigset_t变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删 除某种有效信号。 

🪄sigprocmask --- 获取或设置当前进程的 block表

读取或更改进程的信号屏蔽字【阻塞信号集】。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

🪄sigpending --- 获取当前的pending信号集

#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1

🌠 为什么 pending表 不提供 修改的方法函数,只提供检查的方法呢?

OS不需要提供操作pending的方法,因为 信号产生的5种方式【键盘、指令、系统调用、软件条件、异常】全部都在修改pending表,所以不需要提供修改。

🌠handler 表 由谁来修改?

signal() 函数,一直都在修改这个表。

sigset_t 是OS提供的数据类型,这个数据类型定义的变量 是在哪里开辟的空间 --> 用户栈上开辟的空间。

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <signal.h>
#include <functional>
#include <vector>
#include <sys/wait.h>void PrintPending(const sigset_t &pending)
{std::cout << "curr pending list [" << getpid() << "] :";for (int signo = 31; signo > 0; signo--){if (sigismember(&pending, signo)){std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void non_handler(int signo)
{std::cout << "处理" << signo << std::endl;
}int main()
{// 不让 2号 信号 执行退出::signal(2, SIG_IGN);//::signal(2, non_handler);// 1. 对2号信号进行屏蔽// sigset_t OS提供的数据类型// 栈上开辟的空间sigset_t block, oblock;// 对空间进行清0sigemptyset(&block);sigemptyset(&oblock);// 1.1 添加2号信号// 我们有没有把对2号信号的屏蔽,设置进入内核中?// 只是在用户栈上设置了block的位图结构,并没有设置进入内核中sigaddset(&block, 2);// 1.2 设置进内核中sigprocmask(SIG_SETMASK, &block, &oblock); // 把当前的信号集统一进行替换int cnt = 0;// 2. 获取并打印while (true){// 2.1 如何获取pending 表?sigset_t pending;sigpending(&pending);// 2.2 打印PrintPending(pending);sleep(1);cnt++;if(cnt == 10){std::cout << "解除对 2号 信号的屏蔽" << std::endl;sigprocmask(SIG_SETMASK, &oblock, nullptr);}}return 0;
}

二、信号捕捉

✨信号捕捉的流程

 🪄操作系统运行状态:

1. 用户态 --- CPU开始调度执行我自己写的代码

2. 内核态 --- 执行操作系统的代码

• 处理信号?立即处理吗? 我在做我的事情,优先级很高,信号处理,可能并不是立即处理,是在合适的时候【信号到来,没有立即处理,进程记录下来对应的信号】,即 进程从 内核态 切换回 用户态 的时候,检测当前进程的 pending && block,决定是否处理 再结合 handler表 来处理信号。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

• 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。

• 当前正在执行 main 函数,这时发生中断或异常切换到内核态。

• 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。

• 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个 独立的控制流程。

• sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。

• 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

 从用户态 开始调用我们的代码当需要系统调用,进入内核 执行内核处理动作 进行检测 发现信号要自定义捕捉,进入到用户态 处理完用户态的方法,再返回到内核 执行剩下的动作,紧接着返回到 用户态 的历史代码处 继续向后运行。

 🪄 执行 do_signal() 方法 为什么还要 从 内核态 切转到 用户态【void sighandler(int)】?直接内核执行完不行吗?

信号捕捉的方法是用户自定义的,即怎么处理这个方法是由用户自己写的,若让内核的权限直接执行这个方法,这个方法 若有删除用户 、删除root的配置文件、给用户赋权... 让内核执行用户的方法时 用户的代码 若有非法操作,用户就越权了,所以要切换 ---- 有安全风险

🪄 为什么要从 用户态[void sighandler(int)] 切换到 内核态[sys_sigreturn()] ? 调用完直接切换到 int main() 的下一条指令不行吗?

从一个函数 调用 另一个函数 知道函数名就可以,但是想 从 一个函数 执行完毕 返回到 另一个函数 这两个函数 必须 曾经要有调用关系,即 信号处理完,只能从内核返回。 

🪄 OS 怎么知道把信号处理完了,应该返回到用户空间的下一行代码呢?

CPU 有个寄存器【pc,正在执行指令的下一条地址】,信号处理完成之后,把PC指针恢复。

✨sigaction

#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{std::cout << "get a sig: " << signo <<std::endl;exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler = handler;::sigaction(2, &act, &oact);while(true){pause();}}

 

发信号是直接在 pending表 中直接修改比特位,保存信号用的是位图,

当在信号没有被递达之前,同时来了多个同样的信号,此时当前进程只能记录其中的一个信号【最新的】,

假设 我们执行handler方法非常久,我们处在 处理2号信号之间,若这时又来了一个 2号信号,此时会发生什么?重复执行 handler,会导致 handler方法不断递归,栈就会一直被叠加,造成栈溢出,

#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << "cnt: " << cnt << std::endl;sleep(1);}exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler = handler;::sigaction(2, &act, &oact);while (true){pause();}
}

为了规避这种现象 OS 不允许信号处理的方法进行嵌套 --- 当我们某一个信号正在被处理时,假设信号准备被递达了,内核态 返回 用户态,检查测到 2号 信号要被处理/捕捉,OS 会自动的把对应信号的block位设置为1【屏蔽2号 信号】,当信号处理完 返回内核时 会自动的把2号信号的 block 解除。

在 OS 处理进程的信号捕捉方法时,对同一种信号 OS 只允许对每一个信号的方法进行串行处理,而不支持进行嵌套处理 一次。

#include <iostream>
#include <signal.h>
#include <unistd.h>//printBlockList
void PrintBlock()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);// 读取或更改进程的信号屏蔽字sigprocmask(SIG_BLOCK, &set, &oset);std::cout << "block: ";for(int signo = 31; signo > 0; signo--){if(sigismember(&oset, signo))// 判断一个信号是否在集合里{std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;PrintBlock();sleep(1);}exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler = handler;::sigaction(2, &act, &oact);while (true){PrintBlock();pause();}
}

当我们正在处理某一个信号时,会把当前正在处理的信号给屏蔽掉。

• sigset_t sa_mask: 

如果我想自定义屏蔽信号的list,就可以把屏蔽的信号加入到  sigset_t sa_mask 里。

#include <iostream>
#include <signal.h>
#include <unistd.h>//printBlockList
void PrintBlock()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);// 读取或更改进程的信号屏蔽字sigprocmask(SIG_BLOCK, &set, &oset);std::cout << "block: ";for(int signo = 31; signo > 0; signo--){if(sigismember(&oset, signo))// 判断一个信号是否在集合里{std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;PrintBlock();sleep(1);}exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);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);::sigaction(2, &act, &oact);while (true){PrintBlock();pause();}
}

对相应的信号做屏蔽,处理完信号之后,设置的屏蔽号就会被自动恢复:

#include <iostream>
#include <signal.h>
#include <unistd.h>//printBlockList
void PrintBlock()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);// 读取或更改进程的信号屏蔽字sigprocmask(SIG_BLOCK, &set, &oset);std::cout << "block: ";for(int signo = 31; signo > 0; signo--){if(sigismember(&oset, signo))// 判断一个信号是否在集合里{std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;PrintBlock();sleep(1);break;}// exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);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);// for(int signo = 1; signo <= 31; signo++)//     sigaddset(&act.sa_mask, signo);::sigaction(2, &act, &oact);while (true){PrintBlock();pause();}
}

即 对于信号的处理过程,当我们正在处理 2号 信号 时,当前 2号 信号不可被递达,因为默认被屏蔽了。

🌠当有一个信号来时,我们正在处理这个信号,此时block表 所对应的该信号的比特位为1,而 pending表 在没有处理信号之前就已经 把对应的 正在处理的信号值的比特位 给置 0 了,原因:

当处理信号完,回来时,如何区分 pending表 比特位中的 1 是 历史的 1 还是 在处理信号期间又收到的 1 呢?区分不了,所以 这个 1 是在我们调用这个信号处理函数 之前 直接被清零了。

#include <iostream>
#include <signal.h>
#include <unistd.h>void PrintPending()
{sigset_t pending;::sigpending(&pending);std::cout << "pending: ";for(int signo = 31; signo > 0; signo--){if(sigismember(&pending, signo))// 判断一个信号是否在集合里{std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signo)
{static int cnt = 0;cnt++;while (true){std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;// PrintBlock();// 在信号处理期间,pending表的 2号 比特位 为 0,// 就说明 我们在执行 void handler(int signo) 【处理信号】之前// 就已经 把pending表 清零了PrintPending();sleep(1);}
}int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);::sigaction(2, &act, &oact);while (true){PrintPending();pause();}
}

✨操作系统是怎么运行的

🍔硬件中断

• OS是计算机开机之后,启动的第一个软件;

• OS启动之后,不退出,除非自己关机;

• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了;
• 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询;
• 由外部设备触发的,中断系统运行流程,叫做硬件中断;

• 所有的 外部设备,要被对应的OS访问到【键盘、显示器、磁盘......】,并不能直接让OS定期去轮询这些设备的状态【设备太多】,

• 所以 外部设备 一般 会在硬件层面上 发起中断,发起中断后,因外设过多,所以 设备并没有在物理上 直连CPU ,而是连在了 中断控制器这里,

• 当对应的某个设备 发生 中断时,中断控制器就会知道【1. 是哪一个设备发起的中断,就可以得到对应设备的中断号;2. 中断控制器 替发起中断的设备 直接向CPU 发起 中断请求[通知CPU]--->[向CPU特定的针脚触发 ,本质就是高电压]】,

通知CPU,CPU就知道有一个硬件中断了,CPU同时也就获得 设备的中断号,【每个设备一旦中断了,都会有自己唯一的中断号,】

• OS为了能够处理每一个设备,OS在编码设计的时候,就给我们提供了一个表结构【中断向量表,这个表的下标就为 中断号】,其中 硬件上 触发中断 让CPU拿到 硬件所匹配的中断号,中断号和中断对应处理的方法是固定的,都是硬件上写好的,所以 中断这一套机制 是软硬件结合的产物。

• CPU来处理中断的时候,可能CPU当前正在调度某个进程,所以CPU里面的各种寄存器,一定保存其他的临时数据,所以CPU的中断的固定处理历程,就要把CPU的寄存器数据保存在中断的上下文里 --- CPU保护现场

• 在软件上 对应的操作系统 和 CPU 根据拿到的中断号 n,去查 中断向量表,包括 CPU保护现场 和 查表【找到对应的方法】这一系列的操作 --- 执行中断处理历程【1. 保存现场,2. 根据中断号 n,3. 调用对应的中断方法, 4. 恢复现场】。

• 执行完毕,恢复现场,处理完毕中断,继续之前的工作。

初始化中断向量表源码:

//Linux内核0.11源码 
void trap_init(void)
{int i;set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。 set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3); /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。 for (i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。 outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。 outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。 set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。 
}void
rs_init (void)
{set_intr_gate (0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4 信
号)。 set_intr_gate (0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3 信
号)。 init (tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。 init (tty_table[2].read_q.data); // 初始化串⾏⼝2。 outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请
求。 
}

🍔时钟中断

• 若外部设备没有一个就绪, 中断就不会被触发?

• 进程可以在OS的指挥下,被调度,被触发,那么OS自己被谁指挥,被谁推动执行呢?

• 外部设备可以触发硬件中断,但是这个是需要用户自己触发,有没有自己可以定期触发的设备?

• OS在计算机硬件上,利用中断的特性,在硬件上存在一个 时钟源【帮我们定期去触发时钟中断的硬件】,固定周期,持续给CPU发送中断,

• 在软件上,OS给时钟源一个固定的中断号,若时钟源一直通过中断控制器 给CPU发送硬件中断,CPU就要一直进行保护现场和查找中断向量表,执行中断方法,若给中断向量表特定的中断号为 n 的下标里,单独设置一个方法【进程调度】,外部设备 一直通过 中断 向CPU发送中断,就逼着 OS 不断的通过 中断向量表 执行中断方法,一直在进行任务调度 ---- 这就是 OS 能一直跑起来的原因【时钟中断,一直在推进OS进行调度】。

什么是操作系统?操作系统就是基于中断向量表,进行工作的!!!

当代的 x86芯片 CPU,因为觉得时钟源每次都占用中断控制器,影响运行速度,所以 已经把 时钟源 集成在CPU内部上了,所以CPU里面就会有一个 主频【每隔1s 向CPU自己发送n次硬件中断】 !这就是为什么 CPU的主频 越快 效率越高,调度的次数越频繁,CPU响应就越快。

操作系统在硬件的推动下,自动调度!!!

// Linux 内核0.11 
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c) 
// 调度程序的初始化⼦程序。 
void sched_init(void)
{...set_intr_gate(0x20, &timer_interrupt);// 修改中断控制器屏蔽码,允许时钟中断。 outb(inb_p(0x21) & ~0x01, 0x21);// 设置系统调⽤中断⻔。 set_system_gate(0x80, &system_call);...
}
// system_call.s
_timer_interrupt:...
;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。 call _do_timer ;// 'do_timer(long CPL)' does everything from
// 调度⼊⼝ 
void do_timer(long cpl)
{...schedule();
}
void schedule(void)
{...switch_to(next); // 切换到任务号为next 的任务,并运⾏之。 
}

🍔死循环

OS要调度 由时钟源查中断向量表,OS要处理IO 直接外部设备准备好 发送中断 OS直接根据中断向量表的方法把数据拿出来。

如果是这样,操作系统就可以躺平了,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!

void main(void) /* 这⾥确实是void,并没错。 */ 
{ /* 在startup 程序(head.s)中就是这样假设的。 */ .../** 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返 * 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任 * 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时), * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没 * 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。 */for (;;)pause();
} // end main

这样,操作系统,就可以在硬件时钟的推动下,自动调度了。

🪄时间片

每个进程可以分到一点时间片,当这个进程的时间片 被触发过来 

时钟中断在触发时,是固定的时间间隔,给进程设置时间片【task_struct {int count = 1000;}】,每一个时间中断到来了,当前进程在调度【CPU是当前进程的相关数据】并且被 触发了时间中断,要调度中断,进程调度不是切换,即让当前进程对应的时间片进行--, 在当前进程运行期间 只做计数器--,若 当前进程的计数器 != 0,时钟中断就什么都不做;若  当前进程的计数器 == 0 ,就会进行进程切换。时间片的本质:就是PCB内部的计数器!每一次时钟中断触发时,只做调度【判断当前进程的计数器,是否减到 0,减到 0 ,进程时间片到了 就做切换】,不一定做切换。

时钟中断,固定时间间隔,1纳秒task_struct{  int count = 1000; }进程调度,task_struct -> count--;不一定是切换!
if(task_struct -> count)// do nothing
else  切换!!

🍔软中断

• 上述外部硬件中断,需要硬件设备触发;

• 有没有可能,因为软件原因,也触发上面的逻辑?有!

• 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(x86 32位下为 int[0x80] 或者 x64位 下 syscall),可以让CPU内部触发中断逻辑。

• 汇编指令,就可以写在软件中了,所以 就可以采用类似的软件的原因 来触发CPU执行中断方法。

 为了能让OS支持进行系统调用,任何CPU芯片,都设计了对应的内部指令集,有对应的汇编指令。

 1. 在软件上使用syscall 或 int[0x80],让CPU拿到中断号[0x80],在中断向量表里面设计一个系统调用 的入口函数【void sys_function(int index) {},直接根据系统调用表,根据系统调用的index下标,就可以进行系统调用 】,把这个函数接口的地址放入 系统调用 里面,OS要提供很多的系统调用【fork、exit、close、open....】,OS在源代码设计上,有一大堆的系统调用,这些系统调用把所有的系统调用的方法全部放在一个数组当中 形成一个系统调用表!未来任何系统调用 在操作系统层面,要调用哪个系统调用 使用该数系统调用的数组下标:系统调用号!

2. 【void sys_function(int index) {}】这个方法干什么呢?给这个方法传入一个参数,根据系统调用表,根据index下标进行系统调用【sys_call_table()】

3. 操作系统有了这个表【void sys_function(int index) {}】,当我们想调用系统调用时 需要把调用的系统调用号 交给OS 并且 在执行syscall xxx 或 int[0x80] 可以让CPU进入到陷入内核的阶段,索引到系统调用,找到系统调用 中断向量表方法的调用逻辑【void sys_function(int index) {}】, 即syscall xxx 或 int[0x80] 让我们开始进入系统调用的 固定历程,【void sys_function(int index) {}】这个方法内部 会直接根据我们传给内核的中断号来执行中断向量表当中的方法,完成系统调用。

问题:

• 用户层怎么把系统调用号给操作系统?提前把系统调用号 写入到CPU的寄存器里面。【寄存器(比如EAX)】

• 操作系统怎么把返回值给用户?-寄存器或者用户传入的缓冲区地址

系统调用的传参 包括 系统调用号 全部都会通过寄存器来传递给OS,然后通过寄存器或者用户传入的缓冲区地址 传递给用户。

• 系统调用的过程,先把我们要调用的 系统调用号 写入到寄存器, 再执行int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。

系统调用号的本质:数组下标!

系统调用,也是通过中断完成的!!!

当我们想要调用系统调用,我们可以不用系统调用的名字,直接写一段汇编代码,把系统调用号 movl 到 寄存器【EAX】里面,接着直接调用 int 0x80 就可以让OS进入系统调用,OS中断方法会自动查表,查表后 自动会调用 指定的方法,然后把结果给我。可是我们用的系统调用 怎么没见过 int 0x80 或者 syscall 呢?都是直接调用 上层的函数的呢?

Linux内核提供的系统调用接口,根本就不是C函数,而是 系统调用号 + 约定的传递参数【返回值的寄存器】 + int 0x80 / syscall 触发软中断的机制 。所以OS给我们提供了 【GNU glibc】给我们把系统调用进行了封装【C语言封装版的系统调用】!所以我们用的所有系统调用,在底层采用的是 C语言 和 汇编语言 混编构成的 系统调用。

🍔缺页中断?内存碎片处理?除零野指针错误?

缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断, 然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来 处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

void trap_init(void)
{int i;set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。 set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3); /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。 for (i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。 outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。 outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。 set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。 
}

• 操作系统就是躺在中断处理例程上的代码块!

• CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱

• CPU内部的软中断,比如除零/野指针等,我们叫做 异常。

🪄OS 是基于中断的,是死循环,躺在中断上的,外部有设备就绪 OS 就运行,外部没有设备就绪 时钟中断 就会一直推动 OS ,时钟中断 每隔固定时间 触发一次,固定时间内 OS 的 CPU 就可以工作在调度进程的这件事,当固定时间到来 OS 就会直接中断 让 OS 执行 中断向量表中的 基于中断服务:进程调度, 调度的时候 就会检测 当前进程的时间片【计数器】,计数器--,减到0,就 再从进程列表里 选择进程执行【大 O(1) 调度算法】。

🪄 系统调用 --> 查 中断向量表 --> 执行软中断【执行系统调用】 --> 根据外部传输的 中断号 直接索引 OS 内的 系统调用函数指针表 执行对应的系统调用 --> 执行完毕,返回结果。

🪄有一个函数A 调度到 函数B,为什么 B返回后 能返回到 A 的下一行代码 并且 还能把返回值拿出来?

当A 在调用 B 时,在给 B 形成栈帧结构时,A函数 会把它的下一条指令的地址 先入栈,把形参各种实例化再入栈,即 B 在调用时,头部就已经有 A 的返回值 和 下一条要执行的指令,B 执行完之后,会弹栈出来。

🪄 普通人也可以使用 int 0x80 或 syscall 进入内核里,进行系统调用,这样子的话操作系统,会不会不安全?

OS只允许使用系统调用的方式来访问OS,当传入错误的系统调用号 或 陷入内核做其他的事情 访问其他数据结构,OS是不允许的,即 系统调用本质就是可以让OS安全访问!

🪄 最早期的OS,是进程加载切换的模块,后来基于 中断处理 设计出来一个大的程序块。

三、🌟如何理解内核态和用户态

1. OS有巨大的 中断向量表,在开机的时候 从外设 直接 拷贝到 内存 当中了,包括 系统调用表【函数方法】和 各种异常处理方法 和 OS 内的各种数据结构,

2. OS --- 其中 OS 内 不管是 系统调用、各种异常处理方法、打开文件、调度进程... 本质都是 通过系统调用方法 去访问 OS 里面的各种数据结构,把数据结构的操作方法 以函数的方式 提供出来,最后把 所有函数包装成 系统调用,让外部就能以硬件或软件中断的方式去调用。

3. Linux 操作系统 让 每一个进程 都有自己的 虚拟地址,[0,4GB] 的空间,其中 [0,3GB] 为 用户区【用户想要访问自己的代码和数据不用任何系统调用,能直接进行访问】,用户区 提供了 用户页表 把代码和数据 进行 虚拟地址 到 物理地址 的 映射,有了虚拟地址 编写可执行程序时 ,可执行程序在编译器编译时 形成对应的代码 全部以 虚拟地址统一 进行编址,加载这个可执行程序时,就可以用这个程序内部的虚拟地址 来初始化地址空间 和 构建页表 ;[3,4GB] 为 内核区;

4. 内核页表,映射关系单一【OS 是 开机后 加载的第一个软件块,所以 OS 在内存当中 所占据的内存位置 往往可以固定下来】,OS 本身 整体通过 内核页表 整体映射到 内核区 [3,4GB],在内核区里 用户最关心的就是 系统调用! 【只能通过系统调用访问OS[由内核用户态决定的]】

5. 用户不关心 系统调用的物理或虚拟地址,只需关心 系统调用号,OS 内部自己会进行 索引 查找系统调用,所以用户在虚拟地址的代码区里 编译我们调用的 系统调用函数 时,只要跟 glibc 合并 链接,C语言 告诉 用户 系统调用号 是多少,用户就可以直接调用系统调用了。接着在自己的代码区 跳转至 内核区 【跳转用 软中断 int 0x80 或 syscall】,陷入内核 通过 寄存器EAX 把 中断号 给 OS  ,OS 内中断处理逻辑的代码 就会 寄存器 里读 中断号 索引 系统调用函数指针表 ,调用要用的方法,调用之前把返回值入栈, 调用完成后 把返回值 弹栈 返回到代码区。所以 我们调用任何函数(库、系统调用),都是我们自己进程的地址空间中进行调用。   

6.  不同进程的虚拟地址空间中的 [0,3GB] 用的 用户页表 全都不一样 使用的是不同的物理内存 使用的是 自己的代码和数据;不同进程的虚拟地址空间中的 [3,4GB] 全部都是使用一样的 物理内存。即 OS 无论怎么切换进程,都能找到同一个 OS !换句话说,OS 系统调用方法的执行,是在进程的地址空间中执行的!  

7. 不管是通过哪一个进程的地址空间【内核区】,进入内核,都是通过 软中断 进入 OS 的!

8. 用户态 VS 内核态

• 硬件上:【修改值】处于 用户态 或 内核态 不仅仅是由软件决定的【当前的内核页表、软件中断 都不重要】,主要是由 CPU 来决定的,CPU 里有一个 cs段寄存器【其中有两个比特位 为 CPL 当前权限级别,0:表示处于 内核态,3:表示处于 用户态】,从 内核态 切换到 用户态 ,让 CPU 修改自己的执行级别 由 用户态 3 --> 内核态 0。

• 软件上:调用 int 0x80 或 syscall。   

9. 用户态 如何进入 内核态 ?

• 时钟/外设中断

• CPU内部出现异常

• 陷进【系统调用 int 0x80 / syscall】

进程会一直 从 用户态 转到 内核态,因为CPU一直都有 时钟中断! 

• 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证 兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性。

• 用户态就是执行用户[0,3]GB时所处的状态;

• 内核态就是执行内核[3,4]GB时所处的状态;

• 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。

• 一般执行 int 0x80 或者 syscall 软中断,CPL会在校验之后自动变更。

 切换流程:

1. 从用户态切换到内核态时,首先用户态可以直接读写寄存器,用户态操作CPU,将寄存器的状态 保存到对应的内存中,然后调用对应的系统函数,传入对应的用户栈地址和寄存器信息,方便后 续内核方法调用完毕后,恢复用户方法执行的现场。

2. 从用户态切换到内核态需要提权,CPU 切换指令集操作权限级别为 ring 0。

3. 提权后,切换内核栈。然后开始执行内核方法,相应的方法栈帧时保存在内核栈中。

4. 当内核方法执行完毕后,CPU切换指令集操作权限级别为 ring 3,然后利用之前写入的信息来恢 复用户栈的执行。

四、可重入函数

• main函数调用insert函数向⼀个链表head中插⼊节点node1,插入操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到 sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插⼊操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后向 链表中插⼊两个节点,而最后只有⼀个节点真正插入链表中了。

• 像上例这样,insert函数被不同的控制流程调用,有可能在第⼀次调用还没返回时就再次进入该函 数,这称为重入;

• insert函数访问⼀个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可 重入函数;反之,如果⼀个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。想⼀ 下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?

• 一个函数,被两个以上的执行流同时进入 --- 重入【重复进入】。

• 出现问题了,就称该函数是 不可重入函数。

• 没出现问题 --- 可重入函数。

• 只要一个函数使用了一个全局的资源【全局链表、数组、红黑树...】,都是不可重入函数。

• 一个函数不会使用任何全局资源,它所使用的所有变量资源 全都是它自己的 函数内部临时的,就为 可重入函数。

 如果⼀个函数符合以下条件之⼀则是不可重入的:

• 调用了malloc或free,因为malloc也是用全局链表来管理堆的。

• 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

五、volatile --- 易变关键字(保持内存可见性)

zxl@Luffy:~/study/stu0604$ cat sig.c 
#include <stdio.h>
#include <signal.h>int flag = 0;
void handler(int signo)
{printf("chang flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}zxl@Luffy:~/study/stu0604$ ./a.out 
^Cchang flag 0 to 1
process quit normal

标准情况下,键⼊ CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不 满足, 退出循环,进程退出。

zxl@Luffy:~/study/stu0604$ cat sig.c 
#include <stdio.h>
#include <signal.h>int flag = 0;
void handler(int signo)
{printf("chang flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}zxl@Luffy:~/study/stu0604$ gcc sig.c -O0
zxl@Luffy:~/study/stu0604$ ./a.out 
^Cchang flag 0 to 1
process quit normalzxl@Luffy:~/study/stu0604$ gcc sig.c -O1
zxl@Luffy:~/study/stu0604$ ./a.out 
^Cchang flag 0 to 1
^Cchang flag 0 to 1
^\Quit (core dumped)zxl@Luffy:~/study/stu0604$ gcc sig.c -O2
zxl@Luffy:~/study/stu0604$ ./a.out 
^Cchang flag 0 to 1
^Cchang flag 0 to 1
^Cchang flag 0 to 1
^Cchang flag 0 to 1
^\Quit (core dumped)

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条 件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据⼆异性的问题。while 检 测的 flag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile

zxl@Luffy:~/study/stu0604$ cat sig.c 
#include <stdio.h>
#include <signal.h>volatile int flag = 0;
void handler(int signo)
{printf("chang flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}zxl@Luffy:~/study/stu0604$ gcc sig.c -O2
zxl@Luffy:~/study/stu0604$ ./a.out 
^Cchang flag 0 to 1
process quit normal

 volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该 变量的任何操作,都必须在真实的内存中进行操作。

六、SIGCHLD信号

不用直接 wait 的方式,来进行子进程的等待。

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD信号的处理函数,这样父进程只需专心处理自己的⼯作,不必关心子进程了,子进程终止时会通 知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。

🍔验证子进程 退出 会向父进程发送 SIGCHLD信号:

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{std::cout << "get a sig: " << signo << " , I am : " << getpid() << std::endl;
}// 1. 验证子进程退出,给父进程发送SIGCHLD
int main()
{signal(SIGCHLD, handler); // 子进程退出后,父进程就能捕捉到子进程发给父进程的退出信号if(fork() == 0)// 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}// 父进程 一直不退出while(true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
get a sig: 17 , I am : 2803028    // 父进程收到子进程的退出信号
^C5秒过后,子进程退出,父进程收到 子进程的 17号退出信号
zxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2803028 2803028 2799396 pts/3    2803028 S+    1001   0:00 ./sig
2803028 2803029 2803028 2799396 pts/3    2803028 S+    1001   0:00 ./sig
2801629 2803048 2803047 2801629 pts/0    2803047 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2803028 2803028 2799396 pts/3    2803028 S+    1001   0:00 ./sig
2803028 2803029 2803028 2799396 pts/3    2803028 Z+    1001   0:00 [sig] <defunct>
2801629 2803053 2803052 2801629 pts/0    2803052 S+    1001   0:00 grep --color=auto sig

🍔基于信号进行子进程回收:

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>void handler(int signo)
{std::cout << "get a sig: " << signo << " , I am : " << getpid() << std::endl;pid_t rid = ::waitpid(-1, nullptr, 0);// -1:等待任意子进程 // nullptr: 不关心子进程的终止状态 // 0: 阻塞等待,直到有子进程终止if(rid > 0){std::cout << "子进程退出了, 回收成功, child id: " << rid << std::endl;}
}// 1. 验证子进程退出,给父进程发送SIGCHLD
// 2. 我们可不可以基于信号进行子进程回收呢?
int main()
{signal(SIGCHLD, handler);if(fork() == 0)// 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}// 父进程 一直不退出while(true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ make
g++ -o sig sig.cc -std=c++11
zxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
get a sig: 17 , I am : 2803362
子进程退出了, 回收成功, child id: 2803363   // 基于信号回收子进程
^Czxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2803362 2803362 2799396 pts/3    2803362 S+    1001   0:00 ./sig
2803362 2803363 2803362 2799396 pts/3    2803362 S+    1001   0:00 ./sig
2801629 2803387 2803386 2801629 pts/0    2803386 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2803362 2803362 2799396 pts/3    2803362 S+    1001   0:00 ./sig
2801629 2803393 2803392 2801629 pts/0    2803392 S+    1001   0:00 grep --color=auto sig

🍔一个进程退出父进程可以等待到,若 多个进程 同时退出 父进程可以等待到吗?

10个信号同时发送给父进程,位图只能记录一次,而 waitpid() 函数 只调用了一次,即便是没有同时到达,先后到达,我们对应的当前进程最多同时记住两个进程 一个正在等待waitpid() ,另一个再来 就会记录到来的信号,但是后续来的位图就不能记录下来了【位图只能保存一个】,正在处理的信号 是被屏蔽的,所以无法被提交,所以 当 多个进程 同时退出 父进程回收所有的子进程 是有风险的【风险:1. 未决信号[已发送给进程但尚未被处理(接收或忽略)的信号] 使用的是位图; 2. 正在处理某一个信号退出,再来其他信号 我们当前被处理的信号是被屏蔽的】

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>void handler(int signo)
{std::cout << "get a sig: " << signo << " , I am : " << getpid() << std::endl;pid_t rid = ::waitpid(-1, nullptr, 0);// -1:等待任意子进程// nullptr: 不关心子进程的终止状态// 0: 阻塞等待,直到有子进程终止if (rid > 0){std::cout << "子进程退出了, 回收成功, child id: " << rid << std::endl;}
}// 1. 验证子进程退出,给父进程发送SIGCHLD
// 2. 我们可不可以基于信号进行子进程回收呢?
int main()
{signal(SIGCHLD, handler);// 问题1:若有 10 个子进程同时退出呢?// 每一个信号都能回收吗?for (int i = 0; i < 10; i++){if (fork() == 0) // 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}}// 父进程 一直不退出while (true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ make
g++ -o sig sig.cc -std=c++11
zxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
子进程退出
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804395
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804396
子进程退出
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804397
子进程退出
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804398
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804399
子进程退出
get a sig: 17 , I am : 2804394
子进程退出了, 回收成功, child id: 2804400
^C// 依旧有子进程 没有被回收完 【还处于僵尸状态】
zxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2804394 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804395 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804396 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804397 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804398 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804399 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804400 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804401 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804402 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804403 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804404 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2801629 2804428 2804427 2801629 pts/0    2804427 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2804394 2804394 2799396 pts/3    2804394 S+    1001   0:00 ./sig
2804394 2804401 2804394 2799396 pts/3    2804394 Z+    1001   0:00 [sig] <defunct>
2804394 2804402 2804394 2799396 pts/3    2804394 Z+    1001   0:00 [sig] <defunct>
2804394 2804403 2804394 2799396 pts/3    2804394 Z+    1001   0:00 [sig] <defunct>
2804394 2804404 2804394 2799396 pts/3    2804394 Z+    1001   0:00 [sig] <defunct>
2801629 2804434 2804433 2801629 pts/0    2804433 S+    1001   0:00 grep --color=auto sig

🍔 所以我们在回收子进程时,循环式回收:

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>void handler(int signo)// 父进程收到子进程的 17号 信号
{std::cout << "get a sig: " << signo << " , I am : " << getpid() << std::endl;while (true)// 循环式回收子进程{pid_t rid = ::waitpid(-1, nullptr, 0);// -1:等待任意子进程// nullptr: 不关心子进程的终止状态// 0: 阻塞等待,直到有子进程终止if (rid > 0){std::cout << "子进程退出了, 回收成功, child id: " << rid << std::endl;}else if(rid < 0)// 当前进程已经没有子进程了{std::cout << "暂时:子进程回收完毕!" << std::endl;break;}}
}// 1. 验证子进程退出,给父进程发送SIGCHLD
// 2. 我们可不可以基于信号进行子进程回收呢?
int main()
{signal(SIGCHLD, handler);// 问题1:若有 10 个子进程同时退出呢?// 每一个信号都能回收吗?for (int i = 0; i < 10; i++)// 创建多个子进程{if (fork() == 0) // 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}}// 父进程 一直不退出while (true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ make
g++ -o sig sig.cc -std=c++11
.zxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
子进程退出
子进程退出
get a sig: 17 , I am : 2804762
子进程退出了, 回收成功, child id: 2804763
子进程退出了, 回收成功, child id: 2804764
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出了, 回收成功, child id: 2804765
子进程退出了, 回收成功, child id: 2804766
子进程退出了, 回收成功, child id: 2804767
子进程退出了, 回收成功, child id: 2804768
子进程退出了, 回收成功, child id: 2804769
子进程退出了, 回收成功, child id: 2804772
子进程退出
子进程退出了, 回收成功, child id: 2804770
子进程退出了, 回收成功, child id: 2804771
暂时:子进程回收完毕!
get a sig: 17 , I am : 2804762
暂时:子进程回收完毕!
^Czxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2804762 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804763 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804764 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804765 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804766 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804767 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804768 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804769 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804770 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804771 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2804762 2804772 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2801629 2804786 2804785 2801629 pts/0    2804785 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2804762 2804762 2799396 pts/3    2804762 S+    1001   0:00 ./sig
2801629 2804791 2804790 2801629 pts/0    2804790 S+    1001   0:00 grep --color=auto sig

🍔问题2:10个子进程,6个退出了!会出现什么问题?

会一直 waitpid() , 就会出现阻塞!一旦阻塞,当前代码 就不能返回,父进程就会受到影响, 所以 使用非阻塞方式去等待子进程

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>void handler(int signo)// 父进程收到子进程的 17号 信号
{std::cout << "get a sig: " << signo << " , I am : " << getpid() << std::endl;while (true)// 循环式回收子进程{//pid_t rid = ::waitpid(-1, nullptr, 0);pid_t rid = ::waitpid(-1, nullptr, WNOHANG);// 非阻塞方式去等待子进程// -1:等待任意子进程// nullptr: 不关心子进程的终止状态// 0: 阻塞等待,直到有子进程终止if (rid > 0){std::cout << "子进程退出了, 回收成功, child id: " << rid << std::endl;}else if(rid == 0)// 当前父进程 没有子进程退出{std::cout << "退出的子进程已经被全部回收了" << std::endl;break;}else if(rid < 0)// 当前进程已经没有子进程了, 等待失败!{std::cout << "wait error!" << std::endl;break;}}
}// 1. 验证子进程退出,给父进程发送SIGCHLD
// 2. 我们可不可以基于信号进行子进程回收呢?
int main()
{signal(SIGCHLD, handler);// 问题1:若有 10 个子进程同时退出呢?// 每一个信号都能回收吗?// 问题2:10个子进程,6个退出了!会出现什么问题?// 会一直 waitpid() , 就会出现阻塞!一旦阻塞,// 当前代码 就不能返回,父进程就会受到影响,// 所以 使用非阻塞方式去等待子进程for (int i = 0; i < 10; i++)// 创建多个子进程{if (fork() == 0) // 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}}// 父进程 一直不退出while (true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ make
g++ -o sig sig.cc -std=c++11
^[[Azxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
子进程退出
get a sig: 17 , I am : 2805761
子进程退出了, 回收成功, child id: 2805762
子进程退出了, 回收成功, child id: 2805763
退出的子进程已经被全部回收了
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
get a sig: 17 , I am : 2805761
子进程退出了, 回收成功, child id: 2805764
子进程退出
子进程退出了, 回收成功, child id: 2805765
子进程退出了, 回收成功, child id: 2805767
子进程退出了, 回收成功, child id: 2805768
子进程退出了, 回收成功, child id: 2805769
子进程退出了, 回收成功, child id: 2805770
子进程退出了, 回收成功, child id: 2805771
退出的子进程已经被全部回收了
get a sig: 17 , I am : 2805761
退出的子进程已经被全部回收了
get a sig: 17 , I am : 2805761
子进程退出了, 回收成功, child id: 2805766
wait error!    // 等待失败,是因为代码中的waitpid()是死循环,
^C             // 一直在等待,所以就算子进程全部退出了,还是会一直 waitzxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2805761 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805762 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805763 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805764 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805765 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805766 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805767 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805768 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805769 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805770 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2805761 2805771 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2801629 2805796 2805795 2801629 pts/0    2805795 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2805761 2805761 2799396 pts/3    2805761 S+    1001   0:00 ./sig
2801629 2805801 2805800 2801629 pts/0    2805800 S+    1001   0:00 grep --color=auto sig

🍔 若 单纯的不想让 子进程 形成僵尸状态,不关心 子进程的 退出结果,就可以手动 设置对 SIGCHLD 进行忽略。将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时 会自动清理掉,不会产生僵尸进程,也不会通知父进程。

zxl@Luffy:~/study/stu0604$ cat sig.cc 
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>int main()
{// Linux下,将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉::signal(SIGCHLD, SIG_IGN);for (int i = 0; i < 10; i++)// 创建多个子进程{if (fork() == 0) // 子进程{sleep(5);std::cout << "子进程退出" << std::endl;exit(0);}}// 父进程 一直不退出while (true){sleep(1);}return 0;
}zxl@Luffy:~/study/stu0604$ make
g++ -o sig sig.cc -std=c++11
zxl@Luffy:~/study/stu0604$ ./sig 
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
子进程退出
^Czxl@Luffy:~$ while :; do ps ajx | head -1 && ps ajx | grep sig; sleep 1; donePPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2805873 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805874 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805875 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805876 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805877 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805878 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805879 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805880 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805881 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805882 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2805873 2805883 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2801629 2805907 2805906 2801629 pts/0    2805906 S+    1001   0:00 grep --color=auto sigPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1     971     971     971 ?             -1 Ssl      0   0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
2799396 2805873 2805873 2799396 pts/3    2805873 S+    1001   0:00 ./sig
2801629 2805913 2805912 2801629 pts/0    2805912 S+    1001   0:00 grep --color=auto sig

🌠对于 子进程的等待方式有 4种 方案:

• 阻塞等待

• 非阻塞等待

• 基于信号进行回收

• 设置对应的::signal(SIGCHLD, SIG_IGN);就可以不让子进程产生僵尸了。

Linux下,SIGCHLD 就为 IGN,直接设置为 空,而 用户设置 IGN 的时候,就可能是 1,Linux 下 和 用户设置的 IGN 是不一样的,Linux 把 为空/为1 做了特殊处理。

如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o  \( •̀ ω •́ )/

相关文章:

  • 2025年智能物联网与电子信息国际会议 (IITEI 2025)
  • #开发环境篇:postMan可以正常调通,但是浏览器里面一直报403
  • 【DAY39】图像数据与显存
  • Educational Codeforces Round 179 (Rated for Div. 2)(A-E)
  • 《复制粘贴的奇迹:原型模式》
  • H5移动端性能优化策略(渲染优化+弱网优化+WebView优化)
  • nest实现前端图形校验
  • 编程技能:格式化打印04,sprintf
  • python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
  • 前端面试真题(第一集)
  • 解决com.jcraft.jsch.JSchException: Algorithm negotiation fail
  • Spring Boot应用开发实战
  • Shopify 主题开发:促销活动页面专属设计思路
  • 极速唤醒:高通平台 Android15 默认跳过锁屏,秒启主界面!
  • 前端表单验证进阶:如何使用 jQuery.validator.addMethod() 编写自定义验证器(全是干货,建议收藏)
  • <el-table>构建树形结构
  • Deepfashion2 数据集使用笔记
  • JavaWeb:前端工程化-Vue
  • 基于大模型的结节性甲状腺肿智能诊疗系统技术方案
  • 简数采集技巧之快速获取特殊链接网址URL方法
  • 新闻类网站怎么做seo/宁波网站推广方案
  • js弹出网站/电脑培训学校排名
  • 慈利县建设局网站/网络公司是做什么的
  • 网站建设维护管理办法/软文平台有哪些
  • 深圳龙华建网站公司/兰州网站开发公司
  • wap手机网站建设方案/it培训班出来现状