目录
前言
进程创建的初次了解(创建进程的原理)
什么是fork函数?
初识fork函数
写时拷贝
fork函数存在的意义
fork调用失败的原因
进程终止
运行完毕结果不正确
main函数返回
库函数函数exit
系统调用接口_exit
进程异常终止
进程等待
进程等待是什么
进程等待为什么要进行
进程等待怎么做
阻塞和非阻塞轮询
前言
上节我们已经讲了进程的概念了,大家应该对进程有感悟同时也有更深入的思考,上节课介绍了那么多进程,但是进程该怎么创建呢,今天就来给大家讲解一下 进程的创建
进程创建的初次了解(创建进程的原理)
创建新进程在Linux的下是由父进程来完成的,创建完成的新进程是子进程。
新进程的地址空间有两种可能性:
子进程是父进程的复制品(除了PID和task_struct是子进程自己的,其余的都从父进程复制而来)
子进程装入另一个程序。
在Linux下的fork函数用于创建一个新的进程,使用fork函数来创建一个进程时,子进程只是完全复制父进程的资源。这样得到的子进程和父进程是独立的,具有良好的并发性。但是进程间通信需要专门的机制。
什么是fork函数?
之前我们在Linux下启动一个进程的时候利用的是./可执行程序
,那是否有其他办法去启动一个进程呢?
初识fork函数
当然是有的,那就是使用fork()
这个函数。在使用之前呢我们要先去查看一下这个函数该如何使用------ 使用man 手册查询一下 fork 函数的使用
man 2 fork
- 可以看到,这个函数的功能就是去创建一个子进程,其返回值为
pid_t
- 注意:这里的 pid_t 类型 是无符号整数
函数说明:
-
通过复制调用进程创建一个新进程。
-
fork 有两个返回值。
-
父子进程代码共享,数据各自私有一份(采用写时拷贝)。
接下来,我们来测试一段代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // getpid, getppid, fork, sleep
#include <sys/types.h> // getpid, getppid
int main()
{
printf("before: I am a process\n");
fork();
printf("after: 创建一个新进程\n");
return 0;
}
调用fork函数后,内核做了下面的工作:
1、创建了一个子进程的PCB结构体、并拷贝一份相同的进程地址空间和页表(PCB结构体中的一个指针指向该空间)
2、子进程和父进程起初共享代码和数据,并且页表中的虚拟地址和物理地址的映射关系是一样的,所以也指向相同的物理空间。
3、fork返回后将子进程添加到系统的进程列表中,由调度器调用(每个进程开始自己的旅程)
4、一旦其中任意一方尝试修改数据,那么就会发生写时拷贝,会开辟一块新的物理内存,然后改变页表的映射关系。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
当父进程形成子进程之后,子进程写入,发生写时拷贝,重新申请空间,进行拷贝,修改页表(OS)
但是,我们怎么知道发生了写时拷贝呢?写时拷贝的内容都是由操作系统来完成的
其实父进程创建子进程的时候首先将自己的读写权限改成只读,然后再创建子进程。
此时是操作系统在做,用户并不知道,而且用户可能会对某一数据进行写入,这时页表转换就会出现问题,操作系统就会介入,就触发了我们重新申请内存拷贝内容的策略机制
fork函数存在的意义
fork函数常规用法:
1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。 (进程替换
fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
进程终止
问题引入:为什么main函数要返回0?返回多少的意义是什么???
成功只有一种情况,但是失败可以有无数的原因和理由!! 所以main函数的本质是进程运行时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因!
进程退出场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
运行完毕结果不正确
正常终止(可以通过 echo $? 查看进程退出码): $?->保存最后一次进程退出的退出码
1. 从main返回
2. 调用exit
3. _exit
main函数返回
进程中,谁会关心我的运行情况呢??——>父进程 !
我们之前写代码中,main函数只能return 0吗?
答案是肯定不是!
在多进程环境中,我们创建子进程的目的就是协助父进程办事,但是父进程怎么知道子进程把事情办得怎么样?所以父进程要知道子进程办的怎么样,就有了退出码,而main函数的返回值,就是进程的退出码!
其实main函数本质上也是一个被别人调用的函数,所以他return的结果其实是想告诉他的父进程自己的运行情况。
返回 0 就表示成功,其他数字就表示进程失败的原因,每个不同的数字代表不同的原因!
我们可以通过 strerror 函数来直接查看每个数字代表的意义
它可以返回描述错误码的字符串
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
我们打印结果来看看
退出码 0 正好对应的是成功!
当我们134位置处时,发现已经没有错误信息了。
注意:错误码我们可以自己自定义!
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果
int main()
{
return 31;
}
我们可以直接用 echo $? 指令查看进程的退出码:
我们可以发现指令:echo $? 返回的是上一个进程的错误码。当读取了一次之后,再读取就变成了0
库函数函数exit
exit和return的区别:return和exit在main函数里是等价的,因为exit表示退出进程,而main函数恰好执行完return也会退出进程,但是return在其他函数中代表的是函数返回。
系统调用接口_exit
#include void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit与_exit的区别
首先他们二者都可以让进程终止,并且使用方法也一样,那他们到底有什么区别呢?我们用代码来一探究竟!
//代码一:
int main()
{
printf("Hello");
exit(0);
}
......
//代码二:
int main()
{
printf("Hello");
_exit(0);
}
为什么会出现这种情况呢?
printf打印如果不使用\n换行的话,数据会被存储到缓冲区里。exit函数会帮助我们刷新缓冲区的数据,然而_exit函数不会。因为exit函数在调用exit之前将所有缓存数据都写入了,所以在终止进程时,会将数据打印在屏幕上!
exit比_exit多做了一层最重要的工作就是刷新缓存,我们还可以得出另一个结论就是:缓冲区绝对不在内核区!!因为如果在内核区的话,系统调用的_exit在终止的时候也必然会把缓冲区刷新一下,因为现代操作系统不做任何浪费时间和空间的事情,所以肯定不是由内核维护缓存区,而是由用户区在维护!!(_exit压根看不到缓冲区,所以这个工作只能有exit去完成)
进程异常终止
用退出码可以告诉父进程自己的执行情况,那如果是异常中止了呢??那就连运行完毕这个条件都完成不了,更别谈结果是否正确了,所以我们可以知道异常必然是最先需要被知道的!因为一旦异常了,一般代码都没跑完,即使跑完了,错误码也不能让人相信,此时退出码就没有意义了!
举个例子:就好比我们平时考试一样,你考不好的时候大家会关心你为啥考不好,但如果你作弊了,性质就变了,即考得再好都让人觉得不可相信。
所以进程结束后应该优先判断该进程是否异常了,然后才能确定退出码能不能用!!
// 当我们在运行这样的代码时
int a = 100;
a /= 0;
......
int *p = NULL;
*p = 100;
......
第一种情况: Floating point exception
第二种情况: Segmentation fault
当然不止这两个情况,但是它们都会让程序进程异常终止!
其实一旦程序出现了异常,操作系统就是通过 信号 的方式来杀掉这个进程!
而我们的前面两种情况正好对应了kill -8 和 kill -11
类似除0、野指针这样的错误,会触发一些硬件级别的错误,比如除0,cpu的状态寄存器会出现溢出的错误,而野指针,也就是们即将访问的虚拟地址在页表中找不到对应的映射,或者是建立的映射关系只有只读权限,反正最终会转化成一些硬件级别的信号来给操作系统。
所以,父进程需要关心子进程为什么异常,以及发生何种异常,系统会通过信号来告诉我们的进程发生了异常!!
while(1)
{
printf("hello Linux, pid: %d\n", getpid());
sleep(1);
}
所以我们最关键的是要看父进程是否收到了信号,如果没有收到就没有异常(具体如何收到,就涉及到进程等待的知识)
进程等待
进程等待是什么
首先在开始之前我们提个问题,到底什么是进程等待? - 是什么
进程等待的概念:
- 我们通常说的进程等待其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程,父进程必须等待这个子进程结束后,处理它的代码和数据!
进程等待为什么要进行
在了解完进程等待的概念后,新的问题出现了,我们为什么要进行进程等待?-为什么
- 在前面的文章中讲过,子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而会造成内存泄露。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 指令也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
进程等待怎么做
我们进行了进程等待分析,发现进程等待非常的有必要。那么进程等待具体是怎么做的? - 如何做
父进程通过调用wait/waitpid方法来解决僵尸进程回收问题,以及获取子进程退出情况
wait方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
-
返回值:成功,返回被等待进程的 pid,失败返回-1。
-
参数:输出型参数,获取子进程的退出状态,不关心则可以设置成为 NULL。
waitpid方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
返回值:
- 当正常返回的时候 waitpid 返回等待到的子进程的进程 ID;
- 如果设置了选项 WNOHANG,而调用的过程中没有子进程退出,则返回0;
- 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在。
pid:
- pid = -1 表示等待任意一个子进程。与 wait 等效;
- pid > 0 表示等待进程 ID 与 pid 相等的子进程。
status:
- WIFEXITED(status):查看子进程是否正常退出。若为正常终止子进程返回的状态,则为真;WEXITSTATUS(status):查看进程的退出码。若非零,提取子进程的退出码。
options:
- 0:表示父进程以阻塞的方式等待子进程,即子进程如果处在其它状态,不处在僵尸状态(Z状态),父进程会变成 S 状态,操作系统会把父进程放到子进程 PCB 对象中维护的等待队列中,以阻塞的方式等待子进程变成僵尸状态,当子进程运行结束,操作系统会检测到,把父进程重新唤醒,然后回收子进程;
- WNOHANG:非阻塞轮询等待,若 pid 指定的子进程没有结束,处于其它状态,则 waitpid() 函数返回0,不予等待。若正常结束,则返回该子进程的 ID。
小Tips:wait 和 waitpid 都只能等待该进程的子进程,如果等待了其它的进程那么就会出错。
阻塞和非阻塞轮询
父进程只等待一个进程(阻塞式等待)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0);
}
else
{
int cnt = 10;
// parent
while(cnt)
{
printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
int ret = wait(NULL);
if(ret == id)
{
printf("wait success!\n");
}
sleep(5);
}
return 0;
}
前五秒父子进程同时运行,紧接着子进程退出变成僵尸状态,五秒钟后父进程对子进程进行了等待,成功将子进程释放掉,最后再五秒钟后父进程也退出,整个程序执行结束。
父进程等待多个子进程(阻塞式等待)
一个 wait 只能等待任意一个子进程,因此父进程如果要等待多个子进程可以通过循环来多次调用 wait 实现等待多个子进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 5
// 父进程等待多个子进程
void RunChild()
{
int cnt = 5;
while(cnt--)
{
printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
return;
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();// 创建一批子进程
if(id == 0)
{
// 子进程
RunChild();
exit(0);
}
// 父进程
printf("Creat process sucess:%d\n", id);
}
sleep(10);
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("Wait process:%d, success!\n", id);
}
}
sleep(5);
return 0;
}
如果子进程不退出,父进程在执行 wait
系统调用的时候也不返回(默认情况),默认叫做阻塞状态。由此可以看出,一个进程不仅可以等待硬件资源,也可以等待软件资源,这里的子进程就是软件。
获取子进程的退出信息(阻塞式等待)