参考引用
- UNIX 环境高级编程 (第3版)
- 黑马程序员-Linux 系统编程
1. 进程相关概念
1.1 程序和进程
- 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源 (CPU、内存、打开的文件、设备、锁…)
- 程序 → 剧本 (纸)
- 进程与操作系统原理联系紧密。进程是活跃的程序,占用系统资源,在内存中执行 (程序运行起来,产生一个进程)
- 进程 → 戏 (舞台、演员、灯光、道具…)
同一个剧本(程序)可以在多个舞台(进程)同时上演。同样,同一个程序也可以加载为不同的进程 (彼此之间互不影响)。如:同时开两个终端,各自都有一个 bash 但彼此 ID 不同
1.2 并发
- 并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一个时刻点上仍只有一个进程在运行。
- 例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。
- 分时复用 CPU
- 1-4 表示不同进程获得 CPU 的顺序,并不是一次只分给单个进程(采用时钟中断来保证),而是每个进程每次分配(缓冲)一点
1.3 单道、多道程序设计
1.3.1 单道程序设计
- 所有进程一个一个排对执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了
- 例如:微软的 DOS 系统
1.3.2 多道程序设计
- 在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行,多道程序设计必须有硬件基础作为保证
- 时钟中断:即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃 CPU。因此系统需要一种强制让进程让出 CPU 资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行
- 在多道程序设计模型中,多个进程轮流使用 CPU (分时复用 CPU 资源)。而当下常见 CPU 为纳秒级,1 秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行
1.4 CPU 和 MMU
- 存储介质
- 金字塔越往下存储的量越大,但是存储速度越慢
- 硬盘读取是物理操作,内存读取是电信号,所以内存读取速度比磁盘快得多
- cache 缓存是内存和寄存器之间的中间产物
- 寄存器存储大小为 4 字节(32 位操作系统)
- MMU 虚拟内存映射单元
- 虚拟内存和物理内存映射关系
1.5 进程控制块 PCB
- 每个进程在内核中都有一个进程控制块来维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体
- /usr/src/linux-headers-5.4.0-152-generic/include/linux/sched.h 文件中可以查看 struct task_struct {} 结构体定义,其内部成员有很多,重点掌握以下部分即可
- 进程 id
- 系统中每个进程有唯一的 id,在 C 语言中用 pid_t 类型表示,是一个非负整数
- 查看全部:ps aux 或 查看指定:ps aux | xxx
- 进程的状态
- 进程切换时需要保存和恢复的一些 CPU 寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- umask 掩码
- 文件描述符表
- 包含很多指向 file 结构体的指针
- 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限
- 进程 id
1.6 进程状态
- 进程基本的状态有 5 种
- 分别为初始态,就绪态,运行态,挂起态与终止态
- 其中初始态为进程准备阶段,常与就绪态结合来看
1.7 环境变量
- 环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征
- 字符串 (本质)
- 有统一的格式:名 = 值[:值]
- 值用来描述进程环境信息
- 存储形式:与命令行参数类似。char *[] 数组,数组名 environ,内部存储字符串,NULL 作为哨兵结尾
- 使用形式:与命令行参数类似
- 加载位置:位于用户区,高于 stack 的起始位置
- 引入环境变量表:须声明环境变量。extern char ** environ
常见环境变量
-
环境变量字符串都是 name = value 这样的形式,大多数 name 由大写字母加下划线组成,一般把 name 的部分叫做环境变量,value 的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下
-
PATH
- 可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名 /bin/ls,然而通常执行当前目录下的程序 a.out 却需要提供完整的路径名 ./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录 /bin,却不包含 a.out 所在的目录
- PATH 环境变量的值可以包含多个目录,用 : 号隔开
- 在 Shell 中用 echo 命令可以查看这个环境变量的值
$ echo $PATH /opt/ros/melodic/bin:/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:/home/yue/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
-
SHELL
- 当前 Shell,它的值通常是 /bin/bash
-
TERM
- 当前终端类型,在图形界面终端下它的值通常是 xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行
-
LANG
- 语言和 locale,决定了字符编码以及时间、货币等信息的显示格式
-
HOME
- 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置
2. 进程环境
2.1 main 函数
- main 函数的原型
- argc 是命令行参数的数目,argv 是指向参数的各个指针所构成的数组
int main(in argc, char* argv[]);
- 当内核执行 C 程序时 (使用一个exec 函数),在调用 main 前先调用一个特殊的启动例程
- 可执行程序文件将此启动例程指定为程序的起始地址:这由连接编辑器设置,而连接编辑器则由 C 编译器调用
- 启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用 main 函数做好安排
2.2 进程终止
- 有 8 种方式使进程终止 (termination)
- 其中 5 种为正常终止,它们是
- (1) 从 main 返回
- (2) 调用 exit
- (3) 调用 _exit 或 _Exit
- (4) 最后一个线程从其启动例程返回
- (5) 从最后一个线程调用 pthread_exit
- 异常终止有 3 种方式,它们是
- (6) 调用 abort
- (7) 接到一个信号
- (8) 最后一个线程对取消请求做出响应
- 其中 5 种为正常终止,它们是
2.2.1 退出函数
-
3 个函数用于正常终止一个程序
- _exit 和 _Exit 立即进入内核
- exit 则先执行一些清理处理,然后返回内核
#include <stdlib.h> void exit(int status); void _Exit(int status); #include <unistd.h> void _exit(int status);
-
3 个退出函数都带一个整型参数,称为终止状态 (或退出状态,exit status)。大多数 UNIX 系统 shell 都提供检查进程终止状态的方法
- 如果 (a) 调用这些函数时不带终止状态,或 (b) main 执行了一个无返回值的 return 语句,或 © main 没有声明返回类型为整型,则该进程的终止状态是未定义的
- 但是,若 main 的返回类型是整型,且 main 执行到最后一条语句时返回 (隐式返回),那么该进程终止状态是 0
-
main 函数返回一个整型值与用该值调用exit 是等价的
// 下两行等价 exit(0); return(0);
-
对一段程序进行编译,然后运行,并打印终止状态
$ gcc hello.c $ ./a.out hello world $ echo $? # 打印终止状态 0
2.2.2 函数 atexit
#include <stdlib.h>
int atexit(void (*function)(void));
-
函数返回值
- 若成功,返回 0
- 若出错,返回非 0
-
按照 ISO C 的规定,一个进程可以登记多至 32 个函数,这些函数将由 exit 自动调用。称这些函数为终止处理程序,并调用 atexit 函数来登记这些函数
-
atexit 的参数是一个函数地址,当用此函数时无需向它传递任何参数,也不期望它返回一个值。exit 调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,也会被调用多次
-
一个 C 程序是如何启动和终止的
注意,内核使程序执行的唯一方法是调用一个 exec 函数。进程自愿终止的唯一方法是显式或隐式地 (通过调用 exit) 调用 _exit 或 _Exit。进程也可非自愿地由一个信号使其终止
2.3 命令行参数
- 当执行一个程序时,调用 exec 的进程可将命令行参数传递给该新程序
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/stat.h> #include <dirent.h> int main(int argc, char* argv[]) { int i; for (i = 0; argv[i] != NULL; ++i) { printf("argv[%d] : %s\n", i, argv[i]); } exit(0); }
$ gcc exec.c -o exec $ ./exec arg1 TEST foo argv[0] : ./exec argv[1] : arg1 argv[2] : TEST argv[3] : foo
2.4 环境表
- 每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符的地址。全局变量 environ 则包含了该指针数组的地址:
extern char **environ;
- 例如,如果该环境包含 5 个字符,那它看起来如下图所示
- 其中,每个字符的结尾处都显式地有一个 NULL 字节
- 称 environ 为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串
2.5 C 程序的存储空间布局
- 历史沿袭至今,C 程序一直由下列几部分组成
- 正文段
- 这是由 CPU 执行的机器指令部分
- 初始化数据段
- 通常将此段称为数据段,它包含了程序中需明确地赋初值的变量
- 未初始化数据段
- 通常将此段称为 bss 段,意思是 “由符号开始的块” (block started by symbol),在程序开始执行之前,内核将此段中的数据初始化为 0 或空指针
- 栈
- 自动变量以及每次函数调用时所需保存的信息都存放在此段中
- 每次函数调用时,其返回地址以及调用者的环境信息 (如某些机器寄存器的值) 都存放在栈中
- 最近被调用的函数在栈上为其自动和临时变量分配存储空间
- 堆
- 通常在堆中进行动态存储分配
- 堆位于未初始化数据段和栈之间
- 正文段
- size 命令报告正文段、数据段和 bss 段的长度 (以字节为单位)
$ size /usr/bin/cc /bin/sh text data bss dec hex filename 1025621 15120 10600 1051341 100acd /usr/bin/cc 110609 4816 11312 126737 1ef11 /bin/sh
2.6 共享库
- 在不同的系统中,程序可能使用不同的方法说明是否要使用共享库
- 比较典型的有 cc 和 ld 命令的选项
$ gcc -static hello.c # 阻止 gcc 使用共享库
$ gcc hello.c # gcc 默认使用共享库
2.7 存储空间分配
-
ISO C 说明了 3 个用于存储空间动态分配的函数
- (1) malloc,分配指定字节数的存储区。此存储区中的初始值不确定
- (2) calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为 0
- (3) realloc,增加或减少以前分配区的长度
- 当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定
#include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size); void free(void *ptr);
-
函数返回值
- 若成功,返回非空指针
- 若出错,返回 NULL
-
函数 free 释放 ptr 指向的存储空间。被释放的空间通常被送入可用存储区池,以后,可在调用上述 3 个分配函数时再分配
-
可能产生的致命性的错误
- 释放一个已经释放了的块
- 调用 free 时所用的指针不是 3 个 alloc 函数的返回值
如若一个进程调用 malloc 函数,但却忘记调用 free 函数,那么该进程占用的存储空间就会连续增加,这被称为泄漏 (leakage)。如果不调用 free 函数释放不再使用的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性能下降
2.8 环境变量
2.8.1 获取环境变量值 getenv
#include <stdlib.h>
char* getenv(const char* name);
- 函数返回值
- 指向与 name 关联的 value 的指针
- 若未找到,返回 NULL
- 注意,此函数返回一个指针,它指向 name = value 字符串中的 value
- 应当使用 getenv 从环境中取一个指定环境变量的值,而不是直接访问 environ
2.8.2 设置环境变量值 setenv
#include <stdlib.h>
int putenv(char *string);
- 函数返回值
- 若成功,返回 0
- 若出错,返回非 0
- putenv 取形式为 name = value 的字符串,将其放到环境表中。如果 name 已经存在,则先删除其原来的定义
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
-
函数返回值
- 若成功,返回 0
- 若出错,返回 -1
-
setenv 将 name 设置为 value。如果在环境中 name 已经存在,那么
- (a) 若 overwrite 非 0,则首先删除其现有的定义
- (b) overwrite 为 0,则不删除其现有定义 (name 不设置为新的 value,而且也不出错)
-
unsetenv 删除 name 的定义。即使不存在这种定义也不算出错
3. 进程控制
3.1 进程标识
-
每个进程都有一个非负整型表示的唯一进程 ID
- 因为进程 ID 标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性
-
虽然进程 ID 是唯一的,但是进程 ID 是可复用的
- 当一个进程终止后,其进程 ID 就成为复用的候选者。大多数 UNIX 系统实现延迟复用算法,使得赋予新建进程的 ID 不同于最近终止进程所使用的 ID。这防止了将新进程误认为是使用同一 ID 的某个已终止的先前进程
-
系统中有一些专用进程,但具体细节随实现而不同
- ID 为 0 的进程通常是调度进程,常常被称为交换进程 (swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程
- ID 为 1 的进程通常是 init 进程,在自举过程结束时由内核调用
- init 可成为所有孤儿进程的父进程
- 每个 UNIX 系统实现都有它自己的一套提供操作系统服务的内核进程,例如,在某些 UNIX 的虚拟存储器实现中,进程 ID 2 是页守护进程 (page daemon),此进程负责支持虚拟存储器系统的分页操作
-
除了进程 ID,每个进程还有一些其他标识符,下列函数返回这些标识符
#include <unistd.h> pid_t getpid(void); // 返回值:调用进程的进程 ID pid_t getppid(void); // 返回值:调用进程的父进程 ID uid_t getuid(void); // 返回值:调用进程的实际用户 ID uid_t geteuid(void); // 返回值:调用进程的有效用户 ID gid_t getgid(void); // 返回值:调用进程的实际组 ID gid_t getegid(void); // 返回值:调用进程的有效组 ID
3.2 函数 fork
#include <unistd.h>
pid_t fork(void);
- 一个现有的进程可以调用 fork 函数创建一个新进程(称为子进程,child process)
- fork 函数被调用一次,但返回两次
- 两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID
- 将子进程 ID 返回给父进程的理由
- 因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程 ID
- fork 使子进程得到返回值 0 的理由
- 一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID (进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0)
- 子进程和父进程继续执行 fork 调用之后的指令,子进程是父进程的副本
- 例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分
- fork 函数被调用一次,但返回两次
- 函数返回值
- 子进程返回 0,父进程返回子进程 ID
- 若出错,返回 -1
-
在 fork 之后处理文件描述符有以下两种常见的情况
- (1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新
- (2) 父进程和子进程各自执行不同的程序段。在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的
-
父进程和子进程之间的对比
- 不同点
- fork 的返回值不同
- 进程 ID 不同
- 这两个进程的父进程 ID 不同
- 子进程的父进程 ID 是创建它的进程的 ID,而父进程的父进程 ID 则不变
- 子进程的 tms_utime、tms_stime、tms cutime 和 tms_ustime 的值设置为 0
- 子进程不继承父进程设置的文件锁
- 子进程的未处理闹钟被清除
- 子进程的未处理信号集设置为空集
- 相同点(刚 fork 后)
- data 段、text 段
- 堆、栈
- 环境变量、全局变量
- 宿主目录位置、进程工作目录位置
- 信号处理方式
- 不同点
-
使 fork 失败的两个主要原因
- 系统中已经有了太多的进程 (通常意味着某个方面出了问题)
- 该实际用户 ID 的进程总数超过了系统限制
-
fork 有以下两种用法
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段
- 这在网络服务进程中是常见的:父进程等待客户端的服务请求。当这种请求到达时,父进程调用 fork,使子进程处理此请求,父进程则继续等待下一个服务请求
- 一个进程要执行一个不同的程序
- 这对 shell 是常见的情况。在这种情况下,子进程从 fork 返回后立即调用exec
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段
案例 1
- 子进程对变量所做的改变并不影响父进程中该变量的值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
int globvar = 6;
char buf[] = "a write to stdout\n";
int main(int argc, char* argv[]) {
int var;
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) {
perror("write error");
exit(1);
}
printf("before fork\n");
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
globvar++;
var++;
} else {
sleep(2); // 父进程使自己休眠 2s,以此使子进程先执行
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);
return 0;
}
$ gcc fork.c -o fork
$ ./fork
a write to stdout
before fork
pid = 2244, glob = 7, var = 89 # 子进程的变量值改变了
pid = 2243, glob = 6, var = 88 # 父进程的变量值没改变
- 一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信
案例 2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <dirent.h>
int main(int argc, char* argv[]) {
printf("before fork-1-\n");
printf("before fork-2-\n");
printf("before fork-3-\n");
printf("before fork-4-\n");
pid_t pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
} else if (pid == 0) {
printf("---child is created, pid = %d, parent-pid : %d\n", getpid(), getppid());
} else if (pid > 0) {
sleep(1); // 给父进程增加一个等待命令,这样能保证子进程完成时,父进程处于执行状态,子进程就不会成孤儿
printf("---parent process : my child is %d, my pid : %d, my parent pid : %d\n", pid, getpid(), getppid());
}
printf("------end of file\n");
return 0;
}
$ gcc fork2.c -o fork2
$ ./fork2
before fork-1-
before fork-2-
before fork-3-
before fork-4-
---child is created, pid = 2475, parent-pid : 2474
------end of file
---parent process : my child is 2475, my pid : 2474, my parent pid : 1887
------end of file
# 写的所有进程都是 bash 的子进程
$ ps aux | grep 1887
yue 1887 0.0 0.0 25124 6048 pts/0 Ss 08:41 0:00 bash
yue 2477 0.0 0.0 16180 1088 pts/0 S+ 09:39 0:00 grep --color=auto 1887
案例 3
- 循环创建多个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <dirent.h>
int main(int argc, char* argv[]) {
int i;
pid_t pid;
for (i = 0; i < 5; i++) {
if (fork() == 0) {
break;
}
}
if (5 == i) {
sleep(5);
printf("I'm parent \n ");
} else {
sleep(i);
printf("I'm %dth child\n", i + 1);
}
return 0;
}
$ gcc mulfork.c -o mulfork
$ ./mulfork
I'm 1th child
I'm 2th child
I'm 3th child
I'm 4th child
I'm 5th child
I'm parent
案例 4
- 父子进程共享:读时共享,写时复制(主要针对全局变量)
- 共享两个东西:文件描述符和 mmap 映射区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int var = 100; //.data
int main(void) {
pid_t pid;
pid = fork();
if(pid == -1){ // son
perror("fork error");
exit(1);
} else if (pid > 0) {
var = 288;
printf("parent, var = %d\n", var);
printf("I'm parent pid = %d, getppid = %d\n", getpid(), getppid());
} else if (pid == 0) {
var = 200;
printf("I'm child pid = %d, ppid = %d\n", getpid(), getppid());
printf("child, var = %d\n", var);
}
printf("------finish------\n");
return 0;
}
$ gcc shared.c -o shared
$ ./shared
parent, var = 288
I'm parent pid = 2702, getppid = 1887
------finish------
I'm child pid = 2703, ppid = 2702
child, var = 200
------finish------
案例 5
- 父、子进程 gdb 调试
- 使用 gdb 调试的时候,gdb 只能跟踪一个进程。可以在 fork 函数调用之前,通过指令设置 gdb 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程
- set follow-fork-mode child 命令设置 gdb 在 fork 之后跟踪子进程
- set follow-fork-mode parent 设置跟踪父进程
注意,一定要在 fork 函数调用之前设置才有效
$ gcc mulfork.c -o mulfork -g
$ gdb mulfork
(gdb) list
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <pthread.h>
6 #include <sys/stat.h>
7 #include <dirent.h>
8
9 int main(int argc, char* argv[]) {
10 int i;
(gdb) l
11 pid_t pid;
12
13 for (i = 0; i < 5; i++) {
14 if (fork() == 0) {
15 break;
16 }
17 }
18
19 if (5 == i) {
20 sleep(5);
(gdb) b 13
Breakpoint 1 at 0x6e9: file mulfork.c, line 13.
(gdb) r
Starting program: /home/yue/test/mulfork
Breakpoint 1, main (argc=1, argv=0x7fffffffdbe8) at mulfork.c:13
13 for (i = 0; i < 5; i++) {
(gdb) n
14 if (fork() == 0) {
(gdb) set follow-fork-mode child
(gdb) n
[New process 2831]
[Switching to process 2831]
main (argc=1, argv=0x7fffffffdbe8) at mulfork.c:15
15 break;
(gdb) I'm 2th child
I'm 3th child
I'm 4th child
I'm 5th child
I'm parent
n
19 if (5 == i) {
(gdb) n
23 sleep(i);
(gdb) n
24 printf("I'm %dth child\n", i + 1);
(gdb) n
I'm 1th child
27 return 0;
(gdb)
3.3 函数 exit
-
有 8 种方式使进程终止 (termination)
- 其中 5 种为正常终止,它们是
- (1) 从 main 返回
- (2) 调用 exit
- (3) 调用 _exit 或 _Exit
- (4) 最后一个线程从其启动例程返回
- (5) 从最后一个线程调用 pthread_exit
- 异常终止有 3 种方式,它们是
- (6) 调用 abort
- (7) 接到一个信号
- (8) 最后一个线程对取消请求做出响应
- 其中 5 种为正常终止,它们是
-
不管进程如何终止,最后都会执行内核中的同一段代码
- 这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等
-
对上述任意一种终止情形,都希望终止进程能够通知其父进程它是如何终止的
- 对于 3 个终止函数 (exit、_exit 和 _Exit),实现方法是:将其退出状态作为参数传送给函数 (返回给父进程)
- 在异常终止情况,内核 (不是进程本身) 产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用 wait 或 waitpid 函数取得其终止状态
-
孤儿进程
- 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init 进程,称为 init 进程领养孤儿进程
-
僵尸进程 (zombie)
- 在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理 (获取终止子进程的有关信息、释放它仍占用的资源) 的进程,子进程残留资源 (PCB) 存放于内核中
- ps(1) 命令将僵死进程的状态打印为 Z
- 如果编写一个长期运行的程序,它 fork 了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵尸进程
-
一个由 init 程收养的进程终止时会不会变成个僵尸进程?
- 不会。因为 init 被编写成无论何时只要有一个子进程终止,init 就会调用一个 wait 函数取得其终止状态。这样也就防止了在系统中塞满僵尸进程
案例 1:孤儿进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]) {
pid_t pid;
pid = fork();
if (pid == 0) {
while (1) {
printf("I am child, my parent pid = %d\n", getppid());
sleep(1);
}
} else if (pid > 0) {
printf("I am parent, my pid is = %d\n", getpid());
sleep(9);
printf("------parent going to die------\n");
} else {
perror("fork");
return 1;
}
return 0;
}
$ gcc orphan.c -o orphan
$ ./orphan
I am parent, my pid is = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
------parent going to die------
I am child, my parent pid = 1112
I am child, my parent pid = 1112
I am child, my parent pid = 1112
...
# 父进程死亡前
$ ps ajx
4231 4383 4383 4231 pts/0 4383 S+ 1000 0:00 ./orphan
4383 4384 4383 4231 pts/0 4383 S+ 1000 0:00 ./orphan
# 父进程死亡后
$ ps ajx
1112 4384 4383 4231 pts/0 4231 S 1000 0:00 ./orphan
案例 2:僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]) {
pid_t pid;
pid = fork();
if (pid == 0) {
printf("------child, my parent = %d, going to sleep 10s\n", getppid());
sleep(10);
printf("------child die------\n");
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
$ gcc zoom.c -o zoom
$ ./zoom
I am parent, pid = 4660, myson = 4661
------child, my parent = 4660, going to sleep 10s
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
------child die------
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
...
# 子进程死亡前
$ ps ajx
4505 4660 4660 4505 pts/3 4660 S+ 1000 0:00 ./zoom
4660 4661 4660 4505 pts/3 4660 S+ 1000 0:00 ./zoom
# 子进程死亡后
$ ps ajx
4505 4660 4660 4505 pts/3 4660 S+ 1000 0:00 ./zoom
4660 4661 4660 4505 pts/3 4660 Z+ 1000 0:00 [zoom] <defunct> # defunct 代表死亡
# 每个进程结束后都必然会经历僵尸态,时间长短的差别而已
# 回收僵尸进程,得 kill 它的父进程,让孤儿院去回收它
$ kill -9 4660
3.4 函数 wait 和 waitpid
- 一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件 (这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知
- 父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数 (信号处理程序)
- 调用 wait 或 waitpid 的作用
- 如果其所有子进程都还在运行,则阻塞(阻塞等待子进程退出)
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态后立即返回(回收子进程残留资源)
- 如果它没有任何子进程,则立即出错返回(获取子进程结束状态/退出原因)
一次 wait/waitpid 函数调用,只能回收一个进程
#include <sys/wait.h>
pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
-
函数返回值
- 若成功,返回进程 ID
- 若出错,返回 0 或 -1
-
这两个函数的区别
- 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一选项,可使调用者不阻塞
- waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程
- 如果子进程已经终止,并且是一个僵尸进程,则 wait 立即返回并取得该子进程的状态;否则 wait 使其调用者阻塞,直到一个子进程终止
- 如果调用者阻塞而且它有多个子进程,则在其某子进程终止时,wait 就立即返回。因为 wait 返回终止子进程的进程 ID,所以它总能了解是哪一个子进程终止了
-
这两个函数的参数 status 是一个整型指针
- 如果 status 不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内
- 如果不关心终止状态,则可将该参数指定为空指针 NULL
-
检查 wait 和 waitpid 所返回的终止状态的宏
-
如果要等待一个指定的进程终止 (假设知道要等待进程的 ID) 那么该如何做呢?
- 在早期的 UNIX 版本中,必须调用 wait,然后将其返回的进程 ID 和所期的进程 ID 比较
- 如果终止进程不是所期望的,则将该进程 ID 和终止状态保存起来,然后再次调用 wait。反复这样做,直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程列表,若其中已有要等待的进程,则获取相关信息,否则调用 wat
- 其实,需要的是等待一个特定进程的函数。POSIX 定义了 waitpid 函数以提供这种功能对于 waitpid函数中 pid 参数的作用解释如下
- pid = -1 回收任一子进程。此种情况下,waitpid 与 wait 等效
- pid > 0 回收指定 ID 的子进程
- pid = 0 回收和当前调用 waitpid 一个进程组的所有子进程
- pid < -1 回收指定进程组内的任一子进程
- 在早期的 UNIX 版本中,必须调用 wait,然后将其返回的进程 ID 和所期的进程 ID 比较
-
waitpid 函数返回终止子进程的进程 ID,并将该子程的终止状态存放在由 ststus 指向的存储单元中。对于 wait,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时也可能返回另一种出错),但是对于 waitpid,如果指定的进程或进程组不存在,或者参数 pid 指定的进程不是调用进程的子进程,都可能出错
-
waitpid 的 options 常量
- waitpid 函数提供了 wait 函数没有提供的 3 个功能
- (1) waitpid 可等待一个指定/特定的进程,而 wait 则返回任一终止子进程的状态
- (2) waitpid 提供了一个 wait 的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞
- (3) waitpid 通过 WUNTRACED 和 WCONTINUED 选项支持作业控制
wait 案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]) {
pid_t pid, wpid;
int status;
pid = fork();
// 返回的值为 0,表示当前进程是子进程
if (pid == 0) {
printf("---child, my id = %d, going to sleep 5s\n", getpid());
sleep(10);
printf("------child die------\n");
// 子进程执行完毕后,将返回 66,表示子进程正常终止
return 66;
} else if (pid > 0) {
// wpid = wait(NULL); // 不关心子进程结束原因
wpid = wait(&status); // wait() 函数会使当前进程阻塞,直到一个子进程终止
if (wpid == -1) {
perror("wait error");
exit(1);
}
if (WIFEXITED(status)) { // 判断子进程是否正常终止
printf("child exit with %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { // 判断子进程是否被信号终止
printf("child kill with signal %d\n", WTERMSIG(status));
}
printf("------parent wait finish: %d\n", wpid);
} else {
perror("fork");
return 1;
}
return 0;
}
$ gcc zoom_test.c -o zoom_test
$ ./zoom_test
---child, my id = 2774, going to sleep 10s
------child die------
child exit with 66
------parent wait finish: 2774
# 测试子进程被信号终止
$ ./zoom_test
---child, my id = 2864, going to sleep 5s
child kill with signal 9
------parent wait finish: 2864
# 另开一个终端,输入下列指令
$ kill -9 2864
waitpid 案例 1
- 指定回收一个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[]) {
int i;
pid_t pid, wpid, tmpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
if (i == 2) {
tmpid = pid;
printf("--------pid = %d\n", tmpid);
}
}
if (5 == i) { // 父进程, 从表达式 2 跳出
// sleep(5);
//wait(NULL); // 一次wait/waitpid函数调用,只能回收一个子进程.
//wpid = waitpid(-1, NULL, WNOHANG); // 回收任意子进程,没有结束的子进程,父进程直接返回0
//wpid = waitpid(tmpid, NULL, 0); // 指定一个进程回收, 阻塞等待
printf("i am parent , before waitpid, pid = %d\n", tmpid);
//wpid = waitpid(tmpid, NULL, WNOHANG); // 指定一个进程回收, 不阻塞
wpid = waitpid(tmpid, NULL, 0); // 指定一个进程回收, 阻塞回收
if (wpid == -1) {
perror("waitpid error");
exit(1);
}
printf("I'm parent, wait a child finish : %d \n", wpid);
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
$ gcc waitpid_test.c -o waitpid_test
$ ./waitpid_test
--------pid = 3133
i am parent , before waitpid, pid = 3133
I'm 1th child, pid= 3131
I'm 2th child, pid= 3132
I'm 3th child, pid= 3133
I'm parent, wait a child finish : 3133
$ I'm 4th child, pid= 3134
I'm 5th child, pid= 3135
waitpid 案例 2
- 回收多个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[]) {
int i;
pid_t pid, wpid, tmpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (5 == i) {
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {
if (wpid > 0) {
printf("wait child %d\n", wpid);
} else if (wpid == 0) {
sleep(1);
continue;
}
}
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n", i+1, getpid());
}
return 0;
}
$ gcc waitpid_while.c -o waitpid_while
$ ./waitpid_while
I'm 1th child, pid = 3360
wait child 3360
I'm 2th child, pid = 3361
wait child 3361
I'm 3th child, pid = 3362
I'm 4th child, pid = 3363
wait child 3362
wait child 3363
I'm 5th child, pid = 3364
wait child 3364
3.5 函数 exec
- 用 fork 函数创建新的子进程后,子进程往往要调用一种 exec 函数以执行另一个程序
- 当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行
- 因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变
- exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
- 将当前进程的 .text、.data 替换为所要加载的程序的 .text、.data,然后让进程从新的 .text 第一条指令开始执行,但进程 ID 不变,换核不换壳
- 用 fork 可以创建新进程,用 exec 可以初始执行新的程序。exit 函数和 wait 函数处理终止和等待终止
#include <unistd.h>
extern char **environ;
// 字母 p(path) 表示该函数取 filename 作为参数,并且用 PATH 环境变量寻找可执行文件
// 字母 l(list) 表该函数取一个参数表,它与字母 v 互斤
// 字母 v(vector) 表示该函数取一个 argv[] 矢量
// 字母 e(environment) 表示该函数取 envp[] 数组,而不使用当前环境
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, 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[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
-
函数返回值
- 若成功,不返回
- 若出错,返回 -1
-
在很多 UNIX 实现中,这 7 个函数中只有 execve 是内核的系统调用。另外 6 个只是库函数,它们最终都要调用该系统调用。这 7 个函数之间的关系如下图
- 库函数 execlp 和 execvp 使用 PATH 环境变量,查找第一个包含名为 filename 的可执行文件的路径名前缀。fexecve 库函数使用 /proc 把文件描述符参数转换成路径名,execve 用该路径名去执行程序
案例 1
- execlp 函数通常用来调用系统程序。如 ls、date、cp、cat 命令
- execl 函数加载一个进程,通过(路径 + 程序名)来加载
- execvp 函数加载一个进程,使用自定义环境变量 env
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <dirent.h>
int main(int argc, char* argv[]) {
pid_t pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
} else if (pid == 0) {
// execlp("ls", "-l", "-h", NULL); // 错误,可变参数是从 argv[0] 开始计算
// execlp("ls", "ls", "-l", "-h", NULL);
// execlp("date", "date", NULL);
// execl("/bin/ls", "ls", "-l", "-h", NULL);
// NULL 为必须提供的,称为 “哨兵”
char* argv[] = {"ls", "-l", "-h", NULL};
execvp("ls", argv);
perror("exec error");
exit(1);
} else if (pid > 0) {
sleep(1); // 让父进程延时 1 秒,保证终端提示符不和输出干扰
printf("I'm parent : %d\n", getpid());
}
return 0;
}
$ gcc fork_exec.c -o fork_exec
$ ./fork_exec
total 152K
-rwxrwxr-x 1 yue yue 8.6K 9月 16 15:42 a.out
-rwxrwxr-x 1 yue yue 8.2K 9月 16 16:15 exec
-rw-rw-r-- 1 yue yue 282 9月 16 16:15 exec.c
-rwxrwxr-x 1 yue yue 8.2K 9月 15 16:55 fcntl
-rwxrwxr-x 1 yue yue 8.3K 9月 15 18:46 fcntl2
-rwxrwxr-x 1 yue yue 8.5K 9月 17 08:52 fork
-rwxrwxr-x 1 yue yue 8.5K 9月 17 09:38 fork2
-rw-rw-r-- 1 yue yue 672 9月 17 09:37 fork2.c
-rw-rw-r-- 1 yue yue 627 9月 17 08:52 fork.c
-rwxrwxr-x 1 yue yue 8.4K 9月 17 16:33 fork_exec
-rw-rw-r-- 1 yue yue 447 9月 17 16:33 fork_exec.c
-rwxrwxr-x 1 yue yue 8.6K 9月 15 19:33 ls-R
-rw-rw-r-- 1 yue yue 943 9月 15 19:31 ls-R.c
-rwxrwxr-x 1 yue yue 12K 9月 17 14:18 mulfork
-rw-rw-r-- 1 yue yue 398 9月 17 11:30 mulfork.c
-rw-r--r-- 1 yue yue 262 9月 15 18:46 mycat.c
-rwxrwxr-x 1 yue yue 8.4K 9月 17 11:32 shared
-rw-rw-r-- 1 yue yue 572 9月 17 11:32 shared.c
I'm parent : 3964
案例 2
- 使用 execlp 执行进程查看,并将结果输出到文件里
- 等价于实现 ps aux 指令
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <dirent.h>
int main(int argc, char* argv[]) {
int fd;
fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
close(fd);
return 0;
}
$ gcc exec_ps.c -o exec_ps
$ ./exec_ps
$ cat ps_out