课程内容#
什么是进程#
- 进程是程序在内存中的镜像,是正在运行的程序,是程序的实例化,是一个复杂的集合体
- 包含开辟的内存空间、用户信息、组信息、权限、占用的资源、正在跑的代码、打开的文件等等
- 与之对应
- ① 什么是程序
- 程序是编译好的可执行的二进制文件,放在磁盘上
- 就是一个普通文件,有 x 权限
- 程序的集合是应用
- 程序是编译好的可执行的二进制文件,放在磁盘上
- ② 什么是线程
- 线程代表一系列有顺序的、需要 CPU 执行的指令
- 一个进程可能由一个或多个线程组成,同时执行指令
- [PS] 进程是 CPU资源分配的基本单位,线程是 CPU调度的基本单位
- ① 什么是程序
fork#
创建一个子进程 [进程接口]
- man fork
- 原型 + 描述
- 返回值 [类型:pid_t]:进程 id
- 通过复制调用 fork 的进程 [父进程] 创建一个新进程 [子进程],父进程和子进程运行在相互独立的内存空间
- fork 完成时,两者有一样的内容,之后内存的写、文件映射不会互相影响
- 当内存发生变化时,才会发生真正的拷贝 [写拷贝的概念]
- 否则共用的是同一份内存空间
- 父子主要有以下的不同点:
- 孩子有自己唯一的 PID,并且不和任何已存在的 PID 相同
- 孩子认为的父亲 PID [getppid] 与真实的父亲 PID 相同
- 孩子不会继承父亲的内存锁
- 孩子的资源使用量和 CPU 使用时间都会重置为 0
- 孩子不会继承待执行的信号、信号量、记录锁、计时器、异步 IO 操作
- 返回值
- 成功:在父进程中返回孩子的 PID,子进程中返回 0
- 父亲无法再通过其他方式获得该孩子的 PID,儿子可通过 getppid 获得父亲的 PID
- 失败:返回 - 1,并设置 errno [没有创建子进程]
wait#
等待进程状态的改变
- man wait
- 原型
- wstatus [int *]:返回子进程的状态
- 如子进程中 return、exit 的值
- 需要使用宏来解析,如 WIFEXITED (wstatus),详见代码演示 —wait— 二
- 描述
- 等待对象:调用进程的孩子
- 状态改变情况:孩子被终止、被信号中断、被信号唤醒
- 当有被终止的孩子时,
- wait 命令可以使系统释放孩子相关的资源
- 否则 [不执行 wait 命令],被终止的子进程就会变成僵尸进程[👇]
- 死了的孩子没有被父进程察觉,其资源没有被释放
- 可使用 top 查看
- zombie 即僵尸进程
- 只要一个孩子已经改变状态,wait 命令就会被立马返回
- 否则,会阻塞直到有孩子改变状态或者信号中断
- 返回值
- 返回被终止的孩子 PID 或者 - 1 [出错,并设置 errno]
exec 族#
执行一个文件 [一切皆文件]
- man exec
- 原型
- 有很多兄弟
- 描述
- 将用一个全新的进程镜像【替换】当前的进程镜像
- [让孩子有一个全新的世界]
- 第一个参数都是要执行文件的名字
- path:完整路径
- file:可以是 PATH 环境变量里的命令或完整路径
- 整个族可概括为:"exec + l/v + p/e/pe"
- 参数名 arg,表示是前面参数 path 的参数
- l-list,所有参数放入一整个字符串中 [参数的传递方式]
- 按惯例,arg0 应与要执行文件的名字有关联
- 必须以 (char *) NULL 结尾
- v-vector,所有参数放入一个字符串数组中 [参数的传递方式]
- 必须以 null 指针结尾
- p-path,可执行文件的查找范围包括 PATH 环境变量
- 复制了 Shell 查找命令的过程
- e-env,允许指定环境变量
- 变量 - 数值对
- 返回值
- 只在错误发生时返回 - 1
flock#
在打开的文件上操作建议锁
[本质上是为了保护数据]
- man 2 flock
- 原型 + 描述
- 通过文件描述符 fd 操作
- 主要三种操作
- LOCK_SH:共享锁
- LOCK_EX:互斥锁
- 互斥锁:如果有一个人访问,其他人就不能访问了
- 举例:很多人上一个卫生间
- LOCK_UN:解锁
- 返回值
- 0,成功;-1,失败
代码演示#
fork#
一、复制缓冲区、行缓冲
- 输出结果
- ❗ fork 后面已经没有输出函数了,为什么输入 suyelu,会输出两个suyelu?
- 【事实】虽然 fork 后代码复制了一份给子进程,但是子进程只会执行 fork 后的代码
- 【关键】缓冲区被复制了,里面还存有 suyelu
- printf 中没有换行符,而标准 I/O 是行缓冲I/O,第 13 行执行完并不会刷新缓冲区
- 当程序结束时,才触发刷新缓冲区的条件
- [PS] zsh 下可能只会输出一次 suyelu,可能是 zsh 的优化?bash 下有俩
二、父子进程相互独立
- 输出结果
- ❗ 父进程一定先执行吗?
- 不一定,父子进程完全独立,不相干,本质上谁先执行是由内核调度决定
- 但父进程极大概率先执行,因为内核调度的每个进程有一个运行时间,父进程生了孩子后,它的运行时长还没到
- [PS]
- 1 号进程 <pid 为 1 的进程> 是 init 进程,其它进程都是由它生出来的
- 与人类世界相反,计算机世界的第一个进程一直活着,等着给子进程收尸
三、创建 10 个子进程,并打印自己的序号
10 个子进程是亲兄弟
- 如果不加 18 行的 break
- 将产生 2^10 个进程:1 -> 2 -> 4 -> 8 -> 16 -> 32 -> ... -> 2^10
- 统计运行的父、子进程数量:ps -ef | grep -v grep | grep Ten | wc -l
- [Ten 为可执行程序名]
- sleep 时长不会叠加
- 进程遇到 sleep,系统调度换到其它进程运行,最后等待时间只体现约 10s
- i 变量被子进程带走后就独立了,不会因父进程中 i 变量的改变而改变
wait#
一、制造僵尸进程
- 不用 wait 感知子进程的终止,即会产生僵尸进程
- 用户有多种查看僵尸进程的方式 [让程序在后台运行:./a,out &]
- 基于 ps,查看有 defunct 或 Z 标记的进程
- 基于 top
- 利用 pstree 可以看到僵尸进程的血缘关系
- [PS] 杀僵尸进程需要杀其父进程;该父进程的父进程是 zsh,程序结束后,zsh 会告知系统对父子进程收尸
二、感知子进程返回状态
- 程序在运行约 2s 后,输出如下:
- ❗ 为什么子进程 return 1,父进程 wait 得到的 status 是 256
- 16 位 int 型变量值为 256 👉 其二进制对应第 8 位为 1、其余位均为 0
- 再参考下图 [Linux-UNIX 系统编程手册 (上册)—26.1.3 节],问题有了答案
- 其实,在 man 手册中提到了可使用宏来检查状态
- WEXITSTATUS (wstatus) 则可以解析退出状态
- 在源码中,每个宏对应了下面的位操作
- 所以在 printf 状态时,根据需求,通过宏处理以下即可
exec 族#
【替换为全新的进程】
- 子进程在第 17 行被替换为全新的进程 [vim],之后的代码永远不会被执行
- fork 后直接 exec:不会在 fork 时复制父进程的内存空间又在 exec 时马上启用 [写拷贝概念:当内存发生变化时,才会发生真正的拷贝]
- wait (NULL) 负责收尸
- execlp 的第二个参数可以任意,但和第一个参数有关联更有意义
- 在下面可以体现该参数的某方面意义
- 如果将 exec 代码换成第 17 行,第二个参数随意取名
- 生成可执行文件 Test 的源文件 test.c 如下:
- 输出 argv [0] 的值
- 执行上下两份代码的结果如下:
- 可见,第二个参数体现在了 argv [0] 变量里
附加知识点#
- 使用 while (1){} 时在循环体里加 sleep,对 CPU 更友好
- 否则可能导致 CPU 利用率蹿升、空转、过热
- pstree 可以方便地看到进程的继承关系,-p 可以显示 pid
- 查看僵尸进程:ps、ps -aux、ps -ef、top 均可
- 死锁:两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出
- 计算机中的同步与生活中的不太一样
- 不是做同样的操作
- 而是事件发生的顺序是确定的,是有因果关系的
思考点#
Tips#
- du [-h]:查看当前目录以及所有子目录的大小 [human-readable]
- 对于多进程的输出,使用 more 会将不同进程的输出独立显示
- 推荐电影:《她》2013