目录
一、进程创建
在之前,我们学过linux中的非常重要的函数——fork。他可以从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。
1.写时拷贝
我们知道,fork之后,父子代码共享,经常会出现同一个变量,父子通过操作的不同,这个变量的值也不同,这个时候就会发生写时拷贝。写时拷贝是如何进行的呢?
通过这张图可以看到,fork之后数据段变成了只读, 子进程需要对数据进行写入,就得需要写时拷贝,写时拷贝需要重新申请空间,进行拷贝,再修改页表,这都是操作系统在帮我们处理的,那么操作系统怎么知道你这一份数据需要进行写时拷贝呢?
父进程创建子进程的时候首先将自己的读写权限修改成只读,然后再创建子进程,这些操作用户并不知道,可能对某些数据进行写入,这样在页表处就会进行权限判断,发现用户没有权限,操作系统此时就会介入,操作系统会判断用户的操作
如果该区域本该是可读可写的,是操作系统修改为只读的,因此操作系统会认为用户的操作不算错误,就会触发重新申请内存再拷贝内容的策略机制,这就是写时拷贝。
如果出错,就直接报错,不做额外处理。
写时拷贝完成后,再将对应的内容在页表中修改为可读可写(没有进行写实拷贝的内容依然是只读的)。这样用户就可以正常访问了。
这是一种惰性分离,每次发生写时拷贝都要开辟空间,将写时拷贝的时间越往后延迟,操作系统就有更多的资源。
这里还有一个小问题:你要写入的时候写就完事了,为何还要拷贝一份呢?
因为覆盖和修改是不一样的,很多情况,我们只是想要修改内容的某一部分,这样先拷贝再修改会更合适一点。
2.创建多个进程
我们知道fork的常规用法如下两种
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
如果要创建多个进程来帮我们处理,应该怎么做呢? 直接上代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#define N 10
typedef void (*callback)();
void Work()
{
int cnt = 10;
while(cnt)
{
printf("我是一个子进程, pid: %d, ppid :%d, cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
}
void CreateProcess(int n,callback cb)
{
int i = 0;
for(;i<n;i++)
{
sleep(1);
pid_t id = fork();
if(id == 0)
{
//child
printf("子进程创建成功: %d\n",i);
cb();
exit(0);
}
}
}
int main()
{
CreateProcess(N,Work);
sleep(100);
return 0;
}
这代码对于学过fork的我们来讲,并不算难,多了一个函数指针而已,下面是运行代码。
二、进程终止
进程退出的场景如下三种
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
1.main函数的返回值
我们写C语言程序时,main函数一般都会return 0。只要执行到了return语句,证明我们的代码肯定是运行完毕了的,只是结果还不知道是否正确。
在多进程环境中,我们创建子进程的目的是完成父进程不方便办的事,那我们怎么知道子进程办得怎么样,虽然我们可以打印出来看看结果,但在有一些情况下不方便或者不能打印出来看看,此时就可以通过return的值来查看的,main函数的返回值,就叫做进程的退出码,0通常表示成功,非0表示失败。父进程可以通过获取子进程退出码(即main函数的返回值)来得知子进程做得咋样。
成功的还好,知道你吧事情办得很好,如果返回非0,代表这个事没办好,我们得知道是因为什么原因失败的,我们可以用不同的数字表示不同的原因。但纯数字能表示出错的原因,但是不便于人阅读,因此有一个函数交 strerror 函数。
如下可以打印出strerror各个数字代表的出错原因
有很多很多原因
2.bash中的$?
在bash命令中输入echo $? 可以打印出最近一个子进程执行完毕时的退出码,有点类似于之前我们学习的环境变量,变量名为?,加了$可以打印出变量里的内容。
如下代码中return 10,执行该进程,bash最后获取到的子进程退出码就为10
但是我们继续执行echo $? 后面退出码就会变成0,因为echo也是bash的一个子进程,执行echo语句后,echo语句就是最后一个子进程了,echo又是正常退出的,因此再输入echo $? 得到的值为0。
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果
3.自定义退出码
退出码可以使用C语言内置的,也可以自定义,自己对退出码做解释,因为退出码退出多少(也就是return 返回多少是你自己设置的)
如下就是自定义的退出码,如果你的代码根据用户的操作出现了错误,可以返回响应的值,来知道发生了什么错误。
4.C语言的错误码
在学习C语言的时候,我们接触过一个名叫 errno 的全局变量,他会在程序在运行过程中调用某些库函数或者系统接口出错的时候,被自动设置。也是记录最后一次出错的信息。
如下代码,只读的方式打开一个不存在文件,我们看一下erron的变化与出错信息
发现错误码为2,错误信息为没有该文件
5.错误码与退出码的区别
- 错误码通常是衡量调用库函数或者系统调用接口的调用情况。(系统调用也能更改错误码是因为Linux是用C语言写的,提供了C式接口)
- 退出码通常是一个进程退出的时候,他的退出结果。
他们两个共同的地方在于当失败的时候,用来衡量函数、进程出错时的详细原因。
如下,让错误码与退出码保持了一致
6.代码异常终止
前面五点主要学习的是进程正常退出的问题,可能会有出错码和退出码,如果一个进程异常终止,那么他的退出码也就没有了意义。
比如代码中存在 /0 错误,又比如段错误,栈溢出等等,程序就会崩溃,进程就异常了,就不会继续运行了,本质是操作系统将该进程杀掉了,操作系统会用信号的方式将进程杀掉。
输入 kill -l 可以查看 kill命令的信号
这里我们一直运行一个进程,然后输入kill -8 + 进程pid,就可以通过浮点数错误的方式终止该进程。输入其他方式杀死,也会有相应的错误报告。
因此,查看进程是否出现异常,我们只需看有没有收到信号即可。
7.exit函数
C语言退出函数 exit() ,括号内部可以添加数字,这也是退出码的一种。
exit与return的区别在于:
在非main函数中return 并不会终止进程,main函数会终止进程。
在任意函数中exit都会终止进程。
8.总结
查看进程运行完毕,结果是否正确,只需要看退出码即可
查看进程异常终止,只需要查看收到的信号是什么即可。