1.再谈进程地址空间

1.用户级页表和内核级页表
对于一个进程,有进程的PCB,进程地址空间,以及页表,以及用户在物理内存中的代码和数据,其实这个页表是用户级页表,如何理解呢?之前小编一直在谈地址空间中0~3GB的用户空间,其实这个0~3GB的虚拟地址是通过用户级页表映射到物理内存中用户自己的代码和数据,我们执行用户的代码是在0~3GB的用户的虚拟地址空间中的正文代码开始执行的。
除了上面说的用户级页表,同样在进程地址空间中还有一个内核级页表,里面存储的是操作系统的代码和数据,因为操作系统是软硬件的管理者,所以在开机的时候,操作系统一定是第一时间被加载到物理内存的,这个内核级页表是独一份的,每一个进程都会属于自己的用户级页表,来映射自己的代码和数据,对于内核级页表,每个进程的都是一样的,当我们使用到系统调用的时候,就会通过内核级页表来查找到对应的执行代码。
2.用户态和内核态
上面提到了在用户级页表映射的用户的代码和数据,但是内核级页表映射的是操作系统的代码和数据,在执行用户的代码和数据的时候,依赖的是用户级页表,执行系统调用时,依赖的是操作系统的代码和数据。CPU是由两种状态的,一种是用户态,一种是内核态,两个在一些情景下面会相互转换,所以可以先粗略的理解在一个进程内调度到CPU上面时,执行用户的代码和数据(比如printf库函数,fopen库函数)时就是处于用户态,当然很多库函数都会封装系统调用,所以当执行到系统调用的时候,CPU上面有很多的寄存器,会先记录当前进程的上下文,就从用户态转化位内核态来执行了,当系统调用执行完了之后,重新执行用户的代码和数据是,就又从内核态转为用户态。
转化为内核态的场景当然不止一个系统调用,之前说的硬件中断,发送异常都会导致CPU从用户态转化为内核态,发生硬件中断,CPU要进入内核态根据操作系统提供的中断向量表去执行对应的中断处理程序,发送异常时,CPU要进入内核异常处理程序。
CPU上面有一个ecs寄存器,当里面的比特位表示00的时候,就表示CPU处于内核态,处于11的时候就处于用户态。
3.时钟芯片
在计算机的硬件中,有一个时间的芯片,会周期性的向发送硬件中断,中断处理器接收到后,会转化为中断号发送到CPU,CPU的寄存器会存储这个中断号,进入内核态,会根据这个中断号根据中断向量表执行对应的中断处理程序,也就意味着操作系统进入了执行流,怎么理解这个执行流呢?其实就是CPU来执行操作系统上面的代码和数据了,如果CPU不执行的话,是不是也就意味者操作系统退出执行流了,CPU根据操作系统的中断向量表执行对应的中断处理程序,这个程序的操作会包含什么呢?更新系统时间,检查当前进程的时间片,时间片耗尽的话保留当前进程的上下文,同时将当前进程拿下来,根据进程调度算法从运行队列中选取一个进程放到CPU上面去执行,不然CPU一直执行我们自己写的代码和数据,还怎么维持其他的程序。
操作系统是基于时钟中断的死循环:时钟中断的到来,不就是让CPU进入内核态来执行中断处理程序,然后维持程序的运行平衡,处理了之后再返回用户态,下一次收到,然后循环的执行对应的操作。
4.操作系统是软硬件的管理者
操作系统是软硬件的管理者,就是因为它提供了一整套完整的处理方法,告诉软硬件在发送什么事情后,根据操作系统的代码和数据执行对应的操作,就相当于校长和学生一样,校长颁布校规,然后来告诉学生怎么做事情。
2.信号的处理

一个进程在CPU上面被调度是,因为中断,异常或者系统调用,CPU就会从用户态转化为内核态,在内核态处理完问题之后就会去检查一下,首先进程会去检测PCB中的pending信号集,pending信号集中保存着信号,即进程是否收到信号,如果收到了信号,之后检测block信号集,查看是否阻塞了该信号,如果阻塞了就不处理,转为用户态,如果没有阻塞,就检查是否把信号转为自定义函数,没有就处理信号,进程也就退出了,如果是忽略信号,返回用户态,如果是自定义函数,就去执行自定义函数,然后会执行特殊的sigreturn再次进入内核,然后从内核返回用户态。
ps:这里其实也不止只有上面的3种情况会进入内核态,当进程的时间片结束了也会进入内核态,然后去检查信号的。

ps:在进行自定义函数的处理的时候,也就是图中最右边的身份切换是不会进行信号检测的。
3.信号的捕捉
补充前置知识:
1.我们知道,当进程收到一个信号的时候,会先将pending信号集对应的比特位位置设置为1,表示进程收到了该信号,如果信号没有被阻塞/屏蔽,那么进行信号的递达工作,操作系统会在递达之前把pending表里面对应的比特位置为0,之后信号才递达(这个递达也就是处理信号)。
2.当进程处理信号替换的自定义函数时,也就是在递达时,操作系统会屏蔽这个信号,也就是该信号在当前进程会被阻塞,递达时,如果又收到同样的信号,操作系统会把pending信号集对应的信号波特位会置为1,但是因为被阻塞,不会再被递达(这样子也就保证了不会嵌套执行一个信号处理函数)。递达信号被递达完毕了之后,会执行特殊的sigreturn函数再次进入内核,内核态要返回用户态时,会再次检测信号,这时候发现pending信号集上面还有信号未处理,这时候再去处理因为在递达时收到的同样信号。如果是在递达收到了不同的信号,会直接进入内核态,处理信号。

sigaction也是捕捉信号。
signum:阻塞的信号。
const struct sigaction*act:主要关心里面的两个参数,一个是要传入的自定义方法,一个是需要屏蔽的信号,当进程收到信号,处理信号的自定义函数的时候,是会将当前信号阻塞的,sa_mask信号集是在处理期间还需要阻塞哪些信号,可以设置进去。
struct sigaction*oldact:用于保存原本的信号参数。
测试1:将2号信号的处理动作换成handler函数。
cpp
1 #include <iostream>
2 #include <signal.h>
3 #include <cstring>
4 #include <unistd.h>
5
6 using namespace std;
7
8 void handler(int signo)
9 {
10 cout << "catch a signal, signo: " << signo << endl;
11 }
12
13 int main()
14 {
15 struct sigaction act, oldact;
16 memset(&act, 0, sizeof(act));
17 memset(&oldact, 0, sizeof(oldact));
18
19 act.sa_handler = handler;
20
21 sigaction(2, &act, &oldact);
22
23 while(true)
24 {
25 cout << "i am a process, pid: " << getpid() << endl;
26 sleep(1);
27 }
28
29 return 0;
30 }
运行结果:

每次收到2号信号都去执行自定义函数。
测试2:测试处理自定义函数期间,会阻塞当前信号,不会处理。
cpp
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "catch a signal, signo: " << signo << endl;
while(true)
{
sleep(1);
}
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = handler;
sigaction(2, &act, &oldact);
while(true)
{
cout << "i am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:

收到信号2之后,处理信号2的自定义函数期间,阻塞2号信号,不处理。
测试3:处理信号的自定义函数期间,收到一次或多次相同信号,会将penging表对应比特位置为1,然后处理自定义函数完了之后,再去处理相同的信号,只会处理一次(这个处理其实是再处理了自定义函数之后,会执行特殊的sigreturn再次进入内核,内核返回用户态的时候处理的)
运行结果:

4.可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构
5.volatile关键字
volatile的作用是告诉CPU对于数据的读取去物理内存访问。
情况1:不加优化
cpp
1 #include <stdio.h>
2 #include <signal.h>
3 int flag = 0;
4 void handler(int sig)
5 {
6 printf("chage flag 0 to 1\n");
7 flag = 1;
8 }
9 int main()
10 {
11 signal(2, handler);
12 while(!flag);
13 printf("process quit normal\n");
14 return 0;
15 }

当我们没有加优化的话,flag原本为0,会while循环,不退出,当收到信号时,会改变flag的值,把flag改为1,也就跳出循环,进程退出。
情况2:加优化


在编译时后面加上面的就代表优化代码的编译,O3的优化也就很高了,这个时候,CPU会把一些它觉得经常使用并且正常情况下没有修改的值存入缓存里面,flag明显符合这个条件,之后CPU对于flag就直接从缓存里面取,所以其实flag在物理内存里面已经改变了,但是CPU从缓存读取,没有改变,所以也就出现了flag值不改变的情况。
情况3:解决情况2的问题,从CPU每次都去物理内存读取
cpp
1 #include <stdio.h>
2 #include <signal.h>
3 volatile int flag = 0;
4 void handler(int sig)
5 {
6 printf("chage flag 0 to 1\n");
7 flag = 1;
8 }
9 int main()
10 {
11 signal(2, handler);
12 while(!flag);
13 printf("process quit normal\n");
14 return 0;
15 }
使用volatile声明flag参数,这样子CPU每次都会取物理内存读取,避免优化。

哪怕优化的情况下,还是会读取到真实的flag值。
6.SIGCHLD信号
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
代码实现:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}