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

第四讲 进程控制

1. 进程创建

1.1. fork初识

1.1.1. fork基本用法

fork函数是用来在程序中创建进程的函数, 他从一个已经存在的进程中创建新的进程. 新进程为子进程, 而原来的进程为父进程.

#include <unistd.h>
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1

当调用fork函数时, 操作系统开始创建进程. 那操作系统此时正在做什么呢?

我们知道, 一个进程 = 相关的数据结构(task_strcut, mem_strcut...) + 对应的代码和数据. 因此操作系统先在内核中创建一个task_strcut, 虚拟地址空间等等, 之后再把父进程的代码和数据映射到虚拟地址空间, 页表中去.

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

如何理解进程的独立性问题?

: 所谓的进程独立性, 并不是指的是两个进程一点关系没有, 指的是一个进程出现问题, 其他进程不受干扰(影响). 而操作系统虽然让父子进程对代码和数据进行了共享, 但是也可以通过写时拷贝的方式对其修改数据进行分离, 实现进程独立.

1.1.2. fork返回值

对于fork函数, 如果成功的话有两个返回值,

  • 对于父进程, 返回子进程的pid
  • 对于子进程, 返回0
  • 如果fork失败, 则只对父进程返回-1.

为什么fork函数对于父进程返回的是子进程的pid, 而给子进程返回的是0 ?

: fork函数返回的是子进程的返回值, 告诉父进程fork函数的执行结果, 这样方便父进程对子进程进行管理和标识. 而对于子进程, 他没有子进程, 成功了因此返回0.

1.1.3. fork常规用法

通常, 我们新建一个进程通常有下面用法:

  • 代码分流: 一个父进程希望复制自己, 使父子进程分流, 父进程执行一部分代码, 子进程执行一部分代码.
  • 执行不同的功能: 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.1.4. fork函数调用失败的原因

虽然fork函数大多数时候可以调用成功, 但是也有一些情况调用失败.

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

2. 进程的终止

2.1. 进程的终止在做什么?

还是说一下, 我们的进程 = 相关数据结构 + 对应代码和数据. 因此, 对于进程的终止是:

  • 释放对应代码和数据
  • 释放相关数据结构
  • 返回错误码(退出码)

释放相关数据结构

不过这里需要注意的是, 对于释放相关的数据结构, 操作系统往往会延迟处理, 当释放对应代码和数据之后进程会进入Z(僵尸)状态, 等待父进程对其进行回收, 接收其错误码.

错误码:

用来标识进程处理事情的结果. 在bash中用了?对最近的进程退出错误码进行了保存.

echo $?

不过需要明白的是, 错误码可以使用自定义哈.

错误码的意义: 在于程序正常情况下来标识返回结果是否正确.

2.2. 进程终止的三种情况

  1. 代码跑完, 结果正确
  2. 代码跑完, 结果错误
  3. 代码异常

前面我们已经说过错误码来标识代码跑完情况下, 结果是否正确, 以及为啥不正确的.

代码异常理解: 而对于代码异常, 本质上是接收到了来自操作系统的"信号", 而这个信号, 用户可以让操作系统被动发送, 也可以是操作系统查到问题操作系统自己主动发送. 一旦出现异常, 错误码不再具有意义.

举例: 为了更好的理解, 我们可以创建一个死循环的进程, 之后kill -11 pid, 我们就可以看到相关的异常报错, 再比如我们写一个10/0在程序内, 操作系统也会主动终止程序. 不过本质上都是操作系统发给进程信号让其终止就是了.

下面是操作系统主动给进程发送终止信号的例子:

下面是用户让操作系统让其对某进程终止:

发送kill -11 pid

2.3. 如何终止进程?

return: 在main函数内, return 即可终止进程. 但是return如果是在调用的函数中, 则表示返回上一层函数.

exit: 这是一个C语言库函数, 内部调用系统调用_exit, 不但可以在程序的任意位置终止进程, 还可以在终止进程之前冲刷缓冲区.

_exit: 这是一个操作系统的系统调用接口, 也是可以在程序的任意位置终止进程, 但是不能冲刷缓冲区, 因为缓冲区在_exit的上层封装, _exit不能进行调用.

return: C语言关键字, 即使不包含任何头文件都可以使用.

exit: C库函数, 包含在头文件为<stdlib.h>的头文件中

_exit: 操作系统接口, 头文件是<unistd.h>

3. 进程等待

3.1. 进程等待的概念

任何子进程, 在退出时, 一般都必须要被父进程进行等待. (进程在退出时, 若父进程不管, 则该进程会进入Z状态, 进而引发内存泄漏问题).

3.2. 进程等待的原因

  • 回收系统资源: 父进程通过等待子进程, 解决子进程僵尸问题, 回收系统资源.
  • 获取子进程信息: 获取子进程的退出信息, 了解子进程退出原因.

3.3. 进程等待的实现

父进程等待通过两个函数完成对子进程的等待.

  • wait
  • waitpid

我们下面依次来介绍wait和waitpid两个函数, 介绍wait函数时候, 我们来说一下回收僵尸问题.

#include<sys/types.h> #include<sys/wait.h>

pid_t wait(int*status);

返回值:

成功返回被等待进程的pid, 如果失败则返回-1

参数:

输出型参数, 获取子进程的状态信息, 不关心则可以设置为NULL

监控脚本: while : ; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done

父进程在等待时候做其他事情了吗? 没有, 父进程一直处在阻塞状态. 在这个过程中, 父进程在等待某种软件条件就绪, 如何理解阻塞呢? 不局限于硬件设备的等待, 软件也可以进行等待.

waitpid:

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:

WNOHANG: 若pid指定的子进程没有结束, 则waitpid()函数返回0, 不予以等待. 若正常结束, 则返回该进程的ID.

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

获取子进程的状态信息:

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

问: 为什么不定义两个全局变量去接收status呢? 而是用这么一个参数?

: 这是因为写时拷贝问题.

status的处理:

  • 我们可以对(status>>7)&0xFF提取信号, (status) & 0X7F来提取退出码. 当然, 也可以通过使用宏的方式.
    • WIFEXIED(status) //如果正常终止子进程, 返回真, 查的是信号
    • WEXITSTATUS(status) //如果WIFEXITSED位0, 提取子进程退出码, 查的是退出码.

非阻塞等待: WNOHANG

为啥叫这个名字呢? 服务器卡住了->服务器"HANG"住了, 有点等待的含义在里面.

非阻塞等待的好处? 就是可以让父进程去做一些自己的事情.

pid_t > 0 等待成功, 子进程退出了, 父进程回收成功.

pid_t < 0, 等待失败

pid_t = 0, 检测是成功的, 子进程还没有退出, 需要重复对子进程进行访问. (非阻塞等待).

非阻塞轮询: 非阻塞等待 + 循环.

4. 进程的替换

4.1. 进程替换的原理

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

4.2. 进程替换用到的函数

函数及其解释:

#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[]); (注意: 这个属于系统调用接口, 并不属于C语言库函数)

exec函数族中函数带 p(比如execlpexecvp)的表示会自动搜索环境变量PATH.类似的函数名出现以下字符都表示特定的意义:
l(list):表示参数采用列表。
v(vector):表示参数用数组。
e(env) : 表示自己维护环境变量

path表示的是可执行文件的路径

arg表示命令行参数的某个元素。第一个必须是程序名,其余每一个arg参数表示一条选项用逗号隔开。最后以NULL结尾(具体用法看上面例子)。

file表示可执行文件名,在execvp、execlp中会在其环境变量中去找。

enp[]表示环境变量数组,可提供给替换程序作为环境变量

argv[]表示命令行参数数组,以命令名为首,以NULL结尾。给替换程序提供命令选项。

函数之间的关系:

4.3. 进程替换的例子

下面是一个进程替换的例子:

5. 练习: 编写一个简单的shell

http://www.xdnf.cn/news/12842.html

相关文章:

  • Power Query动态追加查询(不同工作簿下)
  • 论文略读:Position: AI Evaluation Should Learn from How We Test Humans
  • PLC入门【2】PLC的接线
  • 系统模块与功能设计框架
  • 对F1分数的基本认识
  • 【AI论文】VS-Bench:评估多智能体环境中的视觉语言模型(VLM)在策略推理与决策制定方面的能力
  • 个人感悟-构建1000人商业帝国的战略计划
  • vulnyx lower2 writeup
  • 【优选算法】分治
  • Java线程池
  • nginx配置文件
  • leetcode238-除自身以外数组的乘积
  • 【JVM面试篇】高频八股汇总——Java内存区域
  • 华为OD机考 - 水仙花数 Ⅰ(2025B卷 100分)
  • 8. 二叉树(随想录)
  • 本地缓存在Java中的实现方式
  • “图像说话,文本有图”——用Python玩转跨模态数据关联分析
  • 【2025CVPR】模型融合新范式:PLeaS算法详解(基于排列与最小二乘的模型合并技术)
  • 飞云控盘指标-副图指标-买点一持仓操作技术图文解说
  • 初级程序员入门指南
  • 跟进一下目前最新的大数据技术
  • 设备驱动与文件系统:06 目录与文件
  • 骨盆-x光参数
  • python生成器
  • SWAN(Scade One) 语言原理介绍
  • Linux中《进程控制》详细介绍
  • RootSIFT的目标定位,opencvsharp。
  • DOM(文档对象模型)深度解析
  • 开源项目实战学习之YOLO11:12.6 ultralytics-models-tiny_encoder.py
  • 【深度学习-Day 25】告别过拟合:深入解析 L1 与 L2 正则化(权重衰减)的原理与实战