2.2 循环创建 n 个子进程
一次 fork 函数调用可以创建一个子进程。那么创建 N 个子进程应该怎样实现呢?
简单想, for(i = 0; i < n; i++) { fork() } 即可。但这样创建的是 N 个子进程吗?
从上图我们可以很清晰的看到,当 n 为 3 时候,循环创建了 (2^n)-1 个子进程,而不是 N的子程。需要在循环的过程,保证子进程不再执行 fork ,因此当 (fork() == 0) 时,子进程应该立即 break; 才正确。
练习:通过命令行参数指定创建进程的个数,每个进程休眠 1S 打印自己是第几个被创建的进程。如:第 1 个子进程休眠 0 秒打印:“我是第 1 个子进程”;第 2 个进程休眠 1 秒打印:“我是第 2 个子进程”;第 3 个进程休眠 2 秒打印:“我是第 3 个子进程”。
【 fork1.c 】
通过该练习 掌握框架 :循环创建 n 个子进程,使用循环因子 i 对创建的子进程加以区分。
2.3getpid 函数
获取当前进程 ID
pid_t getpid(void);
2.4 getppid 函数
获取当前进程的父进程 ID
pid_t getppid(void);
区分一个函数是“系统函数”还是“库函数”依据:
② 是否访问内核数据结构
② 是否访问外部硬件资源
二者有任一 → 系统函数;二者均无 → 库函数
2.5 getuid 函数
获取当前进程实际用户 ID
uid_t getuid(void);
获取当前进程有效用户 ID
uid_t geteuid(void);
2.6 getgid 函数
获取当前进程使用用户组 ID
gid_t getgid(void);
获取当前进程有效用户组 ID
gid_t getegid(void);
2.7 进程共享
父子进程之间在 fork 后。有哪些相同,那些相异之处呢?刚 fork 之后:
父子相同处 : 全局变量、 .data 、 .text 、栈、堆、环境变量、用户 ID 、宿主目录、进程工作目录、信号处理方式...
父子不同处 : 1. 进程 ID 2.fork 返回值 3. 父进程 ID 4. 进程运行时间 5. 闹钟 ( 定时器) 6. 未决信号集似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB ,但 pid 不同。真的每 fork 一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循 读时共享写时复制 的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销练习:编写程序测试,父子进程是否共享全局变。
【 fork_shared.c 】
重点注意!躲避父子进程共享全局变量的知识误区!
【重点】:父子进程共享: 1. 文件描述符 ( 打开文件的结构体 ) 2. mmap 建立的映射区 (进程间通信详解 )
特别的, fork 之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算
2.8 exec 函数族
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子
进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的
用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创
建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的 .text 、 .data 替换为所要加载的程序的 .text 、 .data ,然后让进程从新的 .text
第一条指令开始执行,但进程 ID 不变,换核不换壳。
其实有六种以 exec 开头的函数,统称 exec 函数:
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[]);
2.9 execlp 函数
加载一个进程,借助 PATH 环境变量 int execlp(const char *file, const char *arg, ...);
成功:无返回;失败: -1
参数 1 :要加载的程序的名字。该函数需要配合 PATH 环境变量来使用,当 PATH 中所有目录搜索后没有参数 1 则出错返回。 该函数通常用来调用系统程序。如:ls 、 date 、 cp 、 cat 等命令。
2.10 execl 函数
加载一个进程, 通过 路径 + 程序名 来加载。int execl(const char *path, const char *arg, ...);
成功:无返回;失败: -1
对比 execlp ,如加载 "ls" 命令带有 -l , -F 参数
execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在 PATH 中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL); 使用参数 1 给出的绝对路径搜索。
2.11 execvp 函数
加载一个进程,使用自定义环境变量 env
int execvp(const char *file, const char *argv[]);
变参形式: ① ... ② argv[] (main 函数也是变参函数,形式上等同于 int main(int argc, char *argv0,...))
变参终止条件:① NULL 结尾 ② 固参指定
execvp 与 execlp 参数形式不同,原理一致。
练习:将当前系统中的进程信息,打印到文件中。
【 exec_ps.c 】
2.12 exec 函数族一般规律
exec 函数一旦调用成功即执行新的程序,不返回。 只有失败才返回,错误值 -1 。所以通常我们直接在 exec 函数调用后直接调用 perror() 和 exit() ,无需 if 判断。
l (list)
命令行参数列表
p (path)
搜素 file 时使用 path 变量
v (vector)
使用命令行参数数组
e (environment)
使用环境变量数组 , 不使用进程原有的环境变量,设置新加载程序运行的环境变量。事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve ,所以 execve在 man 手册第 2 节,其它函数在 man 手册第 3 节。这些函数之间的关系如下图所示。
2.13 回收子进程
孤儿进程
孤儿进程 : 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init 进程,称为 init 进程领养孤儿进程。
【 orphan.c 】
2.14 僵尸进程
僵尸进程 : 进程终止,父进程尚未回收,子进程残留资源( PCB )存放于内核中,变成僵尸(Zombie )进程。
特别注意,僵尸进程是不能使用 kill 命令清除掉的。因为 kill 命令只是用来终止进程的,
而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?
2.15 wait 函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出
状态同时彻底清除掉这个进程。
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态 ( 退出原因 ) 。
pid_t wait(int *status); 成功:清理掉的子进程 ID ;失败: -1 ( 没有子进程 )
当进程终止时,操作系统的隐式回收机制会: 1. 关闭所有文件描述符 2. 释放用户空间
分配的内存。内核的 PCB 仍存在。其中保存该进程的退出状态。 ( 正常终止→退出值;异常
终止→终止信号 )
可使用 wait 函数传出参数 status 来保存进程的退出状态。借助宏函数来进一步判断进
程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非 0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit 的参数 )
2. WIFSIGNALED(status) 为非 0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
*3. WIFSTOPPED(status) 为非 0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
2.16 waitpid 函数
作用同 wait ,但可指定 pid 进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程 ID ;失
败: -1( 无子进程 )
特殊参数和返回情况:
参数 pid :
> 0 回收指定 ID 的子进程
-1 回收任意子进程(相当于 wait )
0 回收和当前调用 waitpid 一个组的所有子进程
< -1 回收指定进程组内的任意子进程
返回 0 :参 3 为 WNOHANG ,且子进程正在运行。
注意: 一次 wait 或 waitpid 调用只能清理一个子进程 ,清理多个子进程应使用循环。
【 waitpid.c 】
作业:父进程 fork 3 个子进程,三个子进程一个调用 ps 命令, 一个调用自定义程序
1( 正常 ) ,一个调用自定义程序 2( 会出段错误 ) 。父进程使用 waitpid 对其子进程进行回收。