【Linux】进程创建、终止、等待、替换

目录

一、进程创建

[1. fork函数初识](#1. fork函数初识)

[2. fork常规用法](#2. fork常规用法)

[3. fork调用失败的原因](#3. fork调用失败的原因)

二、进程终止

[1. 进程常见的退出方式](#1. 进程常见的退出方式)

[(1)return 0](#(1)return 0)

[(2)exit() 退出函数](#(2)exit() 退出函数)

[(3)return VS exit()](#(3)return VS exit())

[2. 进程退出场景](#2. 进程退出场景)

三、进程等待

[1. 进程等待的必要性](#1. 进程等待的必要性)

[2. wait()方法](#2. wait()方法)

[3. waitpid()方法](#3. waitpid()方法)

[4. 非阻塞轮询方案](#4. 非阻塞轮询方案)

四、程序替换

[1. 替换原理](#1. 替换原理)

2.替换函数


正文:

一、进程创建

1. fork函数初识

在linux中fork函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,而原进程为⽗进程。(要创建子进程必须通过fork创建

include <unistd.h>

pid_t fork ( void );
返回值:子进程中返回 0 ,父进程返回子进程 id ,出错返回 -1

进程调用fork,内核做三步走:

(1)分配新的内存块和内核数据结构给子进程

(2)将父进程部分数据结构内容拷贝至子进程

(3)更改映射关系

fork返回,开始调度器调度

fork创建进程是一种写时拷贝。

进程在加载到内存时,OS通过先描述后组织的方法管理进程,进程会有PCB、虚拟地址空间、页表。fork后的子进程除了拷贝父进程的PCB、虚拟地址空间、页表外,还要管理页表中的权限(读/写)。

fork之前父进程页表项权限是可读写,fork后由于写时复制机制(COW)的原因,内核可能会将可写页标记为只读(即使父进程原本是可写的),以启用 COW。所以当进程尝试写入,操作系统"报错",OS对错误分为两类:1.真错,杀死进程;2 .写入错误,进行内核三步走,在第三步更改映射关系时把全部权限由只读改为可读写。

1.1 写时复制(COW)

为了优化性能,Linux 会标记可写页为 COW。此时:

父子进程的页表项会被改为 只读(即使原来是可写的)。

当任一进程尝试写入该页时,会触发 缺页异常(Page Fault),内核会复制该页,并恢复可写权限。

大家是否疑惑:为什么不直接拷贝父进程把父子数据分开呢?

其实fork创建子进程后并不是什么数据都用,如果父进程数据量大在子进程用不到的情况下会造成内存空间的占用,所以才有了写时拷贝也称为惰性申请,当你需要再给你拷一份。

2. fork常规用法

⼀个⽗进程希望复制自己,使父子进程同时执⾏不同的代码段。例如,父进程等待客户端请求生成子进程来处理请求。

⼀个进程要执行⼀个不同的程序。例如子进程从fork返回后,调用exec函数。

3. fork调用失败的原因

系统中有太多的进程
实际用户的进程数超过了限制

二、进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

1. 进程常见的退出方式

正常终⽌(可以通过 echo $? 查看进程退出码):

  1. 从main返回
  2. 调⽤exit
  3. _exit
    异常退出:
    ctrl + c,信号终⽌

(1)return 0

return 0 表示退出时的退出码 ,可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。
Linux Shell 中的主要退出码:

而返回的退出码会被操作系统或得,用来知道任务执行情况和错误对应原因情况。

可通过指令:

echo $? 获得最近一个进程执行完的退出码

(2)exit() 退出函数

除了return 0 这种常见的退出方式外还有个exit函数。它的底层调用系统的_exit()函数,与直接调用_exit() 函数的区别在于:exit() 函数会将数据刷到内存缓冲区,而_exit() 直接结束退出

(3)return VS exit()

  1. return 表示退出当前函数,exit()表示立即终止整个程序

  2. exit()可以在程序的任意位置调用

2. 进程退出场景

  1. 代码运⾏完毕,结果正确
  2. 代码运⾏完毕,结果不正确
  3. 代码没运行完毕,进程异常

其中,代码异常终止了,退出码本身就无意义!当进程异常时,OS知道后会自动杀掉进程。

三、进程等待

1. 进程等待的必要性

之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
泄漏。
另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的kill -9 也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。
最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。
⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息

之前在进程状态那篇文章讲过,如果子进程要想结束必须由父进程回收,不然子进程会呈现僵尸状态。

进程等待是让父进程通过等待的方式回收子进程PCB,如果需要会获取到子进程的退出信息。

2. wait()方法

include <sys/types.h>

include <sys/wait.h>

pid_t wait ( int * status);
返回值:
成功返回被等待进程 pid ,失败返回 -1 。
参数:
输出型参数,获取⼦进程退出状态 , 不关⼼则可以设置成为 NULL

父进程调用wait表示父进程等待任意一个子进程。如果子进程没有退出,父进程wait时会阻塞;如果子进程退出,父进程wait时会返回子进程pid,让系统自动解决子进程的僵尸问题!!

以下通过代码验证wait()真的回收多个子进程:

为了让大家看到子进程真会等待wait回收,所以故意让父进程休眠5秒后再进行回收,在等待时子进程全部呈现僵尸状态。

执行以上代码后,通过指令:**while :; do ps ajx | grep test && ps ajx | head -1; sleep 1 ; done;**可查询进程状态如下

父进程等待成功回收后不存在子进程,说明调用wait()方法是可以等待子进程进行PCB回收的!

3. waitpid()方法

相比于wait(),这是最佳等待方法

pid_ t waitpid ( pid_t pid, int *status, int options);
返回值:
当正常返回的时候 waitpid 返回收集到的⼦进程的进程 ID ;
如果设置了选项 WNOHANG, ⽽调⽤中 waitpid 发现没有已退出的⼦进程可收集 , 则返回 0 ;
如果调⽤中出错 , 则返回 -1 , 这时 errno 会被设置成相应的值以指⽰错误所在;
参数:
pid :
Pid=-1 , 等待任⼀个⼦进程。与 wait 等效。
Pid>0. 等待其进程 ID 与 pid 相等的⼦进程。
status: 输出型参数
宏形式获取:
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是
否是正常退出)
WEXITSTATUS(status): 若 WIFEXITED非 零,提取⼦进程退出码。(查看进程的
退出码)
options: 默认为 0 ,表示阻塞等待
WNOHANG: 若 pid 指定的⼦进程没有结束,则 waitpid() 函数返回 0 ,不予以等
待。若正常结束,则返回该⼦进程的 ID 。

进程退出的场景有三种形式:

1.代码运行完毕,结果正确

2.代码运行完毕,结果错误

3.代码异常,结果无异议

我们如何知道进程退出对应哪一种情况呢?就是通过进程结束后返回的退出码和退出信息,在Linux中可通过命令:kill -l进行查看所有退出码

可以发现这些数字是从1开始没有0号数字,因为每个数字后面对应的是导致错误的原因,0代表的是程序正常结束,结果正确!这也是为什么main函数中我们return的是0。

代码正常结束退出码为0,退出信号也为0,所以我们实际上是用两个数来表示子进程执行情况,而这两个数最终会合并为一个数放在status中带出让用户看到!

1.进程退出码 -->(status>>8)& 0xFF

2.进程退出信号 --> status & 0x7F

当子进程退出呈现僵尸状态,退出信息会写入到子进程PCB中,父进程通过waitpid()方法获取子进程内属性数据,然后再让OS释放目标PCB 。僵尸状态不能释放是因为数据还没被父进程拿到

以下是waitpid()方法阻塞等待每一个指定进程:

利用宏WIFEXITED(status)和WEXITSTATUS(status)简化代码:

4. 非阻塞轮询方案

上面提到的waitpid都是阻塞式等待子进程,也就是返回值要么等待成功,要么等待失败。

非阻塞等待 只等待一次,还没结束父进程退出直接返回0。它的返回值不再是两个而是三个:

子进程没退 && 等待成功是指:你等的子进程存在,但是还没运行结束这时返回0

  1. 子进程退出 && 等待成功 --> 返回值>0

  2. 子进程没退 && 等待成功 --> 返回值==0

  3. 等待失败(不是你的子进程)-->返回值<0

非阻塞等待写法:

非阻塞轮询等待就是手动加一个循环重复等待直到子进程运行结束为止。

为什么要非阻塞轮询?它的效率为什么高?

非阻塞 时父进程只等待一次,没等到直接退出会导致还在运行的子进程成为孤儿进程被bash托管,这样就不在与显示器终端关联Ctrl+C无法终结,只能用kill命令杀死,所以用非阻塞轮询。

非阻塞轮询不会因为条件没就绪就阻塞,当子进程还没运行完父进程可以去做其他事,提高了效率!!

四、程序替换

fork()创建一个子进程后,子进程(1)执行父进程的一部分代码和数据(2)执行一个全新的程序,有自己的代码和数据是一个真正独立的进程(写时拷贝)

1. 替换原理

⽤fork创建子进程后执行的是和⽗进程相同的程序(但有可能执行不同的代码分⽀),⼦进程往往要调用一种exec函数以执行另⼀个程序。当进程调用⼀种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

2.替换函数

其实有六种以exec开头的函数,统称exec函数:

include <unistd.h>

int execl ( const char *path, const char *arg, ...);
int execlp ( const char *file, const char *arg, ...);
int execle ( const char *path, const char *arg, ..., char * const envp[]);
int execv ( const char *path, char * const argv[]);
int execvp ( const char *file, char * const argv[]);
int execve ( const char *path, char * const argv[], char * const envp[]);
这些函数如果调⽤成功 则加载新的程序从启动代码开始执行,不再返回
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值


以下是父进程创建一个子进程后让其执行程序替换的代码实现:

上面代码执行execl后不再执行printf,因为替换成功,进程执行了ls的代码就不会执行后续代码而替换失败则返回-1,只要返回必是失败!!

程序替换一般都是让子进程执行,例如我们双击桌面一个图标,实际上在双击时创建了子进程,通过写时拷贝再让其程序替换成用户要执行操作的对应代码。


完~感谢观看