一、fork函数
fork 函数原型:
#include <unistd.h>
pid_t fork(void);
在 Linux 中,fork 函数是非常重要的函数,它从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。它和其他函数的区别在于:它执行一次返回两个值。其中父进程的返回值是子进程的进程号,而子进程的返回值为0。若出错则返回-1。因此可以通过返回值来判断是父进程还是子进程。
fork 函数创建子进程的过程为:使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。
通过这种复制方式创建出子进程后,原有进程和子进程都从函数 fork 返回,各自继续往下运行。也就是说,fork 不仅仅复制了进程的资源,更复制了原有进程的运行状态,所以复制出的新的进程虽然什么都和父进程一样,但它从 fork 函数后面开始运行。但是原进程的 fork 函数返回值与子进程的 fork 返回值不同,在原进程中,fork 返回子进程的 PID。而在子进程中,fork 返回0,如果 fork 返回负值,则表示创建子进程失败。
vfork 函数的作用和返回值与 fork 相同,但有一些区别。二者都创建一个子进程,但是它并不是将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec (或exit),所以也就不会存放该地址空间。而且 vfork 保证子进程比父进程先运行,在它调用 exec 或 exit 之后父进程才能被调度运行。
fork 以后的子进程自动继承了父进程打开的文件,继承以后,父进程关闭打开的文件不会对子进程造成影响。
二、进程的终止
进程的终止有 5 种方式:
- main 函数的自然返回;
- 调用 exit 函数;
- 调用 _exit 函数;
- 接收到某个信号,如 ctrl+c SIGINT,ctrl+\ SIGQUIT;
- 调用 abort 函数,它产生 SIGABRT 信号,所以是上一种方式的特例。
前三种方式为正常终止,后两种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常执行,如对象的析构、atexit 函数的执行等。
exit 和 _exit 函数都是用来终止进程的。当程序执行到 exit 和 _exit 时,进程会无条件的停止剩下的所有操作,清除包括 PCB 在内的各种数据结构,并终止本程序的运行。但是它们是有区别的,exit 和 _exit 的区别如下所示:
exit 函数和 _exit 函数的最大区别在于 exit 函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理 I/O 缓冲”。
由于在 Linux 的标准函数库中有一种被称作“缓冲 I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用 _exit 函数直接将进程关闭,缓冲区中的数据就会丢失。因此,如果想保证数据完整性,建议使用 exit 函数。
exit 和 _exit 函数的原型:
#include <stdlib.h> //exit 的头文件
#include <unistd.h> //_exit 的头文件
void exit(int status);
void _exit(int status);
status 是一个整型参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他数值表示出现了错误,进程非正常结束。
三、wait 和 waitpid 函数
用 fork 函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被 PID 为 1 的进程(即 init)接管。孤儿进程退出后,它的清理工作由祖先进程 init 自动处理。但在 init 进程清理子进程之前,一直消耗系统的资源,所以要尽量避免。
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将会成为僵尸进程(defunct),在系统中如果存在僵尸进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait 和 waitpid 都将暂停父进程,等待一个子进程退出,并进行清理工作;
wait 函数随机的等待一个子进程退出,并返回该子进程的 PID;
waitpid 函数等待指定 PID 的子进程退出;如果为 -1 表示等待所有子进程,同样返回该子进程的 PID。
status 参数是传出参数,它会将子进程的退出码保存到 status 指向的变量中,如 waipid(pid, &n, 0);其中 n 是之前定义的整型变量。
通常用下面的两个宏来获取状态信息:
WIFEXITED(n) 如果子进程正常结束,它就取一个非 0 值。
WEXITSTATUS(n) 如果 WIFEXITED 非 0 ,它返回子进程的退出码,即子进程的 exit 函数的参数值,或者 return 返回的值。
options 用于改变 waitpid 的行为,其中最常用的是 WNOHANG,它表示无论子进程是否退出都将立即返回,不会将调用者的执行挂起。
四、exec 函数族
exec * 由一组函数组成:
extern char * * enveiron;
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[]);
exec 函数族的作用是运行第一个参数指定的可执行程序。但其工作过程与 fork 完全不同,fork 是在复制一份原进程,而 exec 函数执行第一个参数指定的可执行程序之后,这个新程序运行起来后也是一个进程,而这个进程会覆盖原有进程空间,即原有进程的所有内容都被新运行起来的进程全部覆盖了,所以 exec 函数后面的所有代码都不再执行,但它之前的代码当然是可以被执行的。
path 是包括执行文件名的全路径名。
file 既可以是全路径名,也可以是可执行文件名。
arg 是可执行文件的全部命令行参数,可以用多个,注意最后一个参数必须为 NULL。
argv 是一个字符串的数组 char * argv[] = {“full path”, “param1”, “param2”, … NULL};
envp 指定新进程的环境变量 char * envp[] = {“name1 = val1”, “name2 = val2”, … NULL};
exec 函数族的参数传递有两种方式:一种是逐个列举的方式,另一种是将所有参数整体构造指针数组传递。在这里是以函数名的第 5 位字母来区分的,字母为 “l”(list) 的表示逐个列举的方式,其语法为 char * arg;字母为 “v”(vertor) 的表示将所有命令行参数整体构造指针数组传递,其语法为 char * const argv[]。
以字母 p 结尾的函数通过搜索系统 PATH 这个环境变量来查找新程序的可执行文件的路径。如果可执行程序不在 PATH 定义的路径中,我们就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数。
对有参数 envp 的函数调用,其以字母 e 结尾,函数通过传递 envp 传递字符串数组作为新程序的环境变量。新进程中的全局变量 environ 指针指向的环境变量数组将会被 envp 中的内容替代。
注意:对于有参数 envp 的函数,它会使用程序员自定义的环境变量,如果自定义的环境变量中包含了将要执行的可执行程序的路径,那么第一个参数中也必须写全路径。因为我们自定义的环境变量不是用来寻找这个可执行程序的,而是在这个可执行程序运行起来之后给新进程用的。
可以用 env 命令查看环境变量。
exec 函数族的作用是根据指定的文件名找到可执行文件,并且用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何 Linux 下可执行的脚本文件,如果不是可执行的文件,那么就解释成为一个 shell 文件,sh** 执行。
以上 6 条函数,参数 argc 指出了运行该程序时命令行参数的个数,数组 argv 存放了所有的命令行参数,数组 envp 存放了所有的环境变量。环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境变量是 PATH ,它指出了应到哪里去搜索应用程序,如 /bin;HOME 也是比较常见的环境变量,它指出了我们在系统中的个人目录。环境变量一般以字符串 “XXX=xxx” 的形式存在, XXX 表示变量名,xxx 表示变量的值。
值得一提的是,argv 数组和 envp 数组存放的都是指向字符串的指针,这两个数组都以一个 NULL 元素表示数组结尾。
前三个函数都是以 execl 开头的,后三个函数都是以 execv 开头的,它们的区别在于,execv 开头的函数是以 “char * argv[]” 这样的形式传递命令行参数的,而 execl 开头的函数把参数一个一个列出来,然后以一个 NULL 表示结束。这里的 NULL 的作用和 argv 数组里的 NULL 作用一样,建议使用 (char *)0 代替 NULL。
在全部 6 个函数中,只有 execle 和 execve 使用了 char * envp[] 传递环境变量,其他 4 个函数都没有这个参数,这并不意味着它们不传递环境变量,这 4 个函数将把默认的环境变量不做任何修改的传递给被执行的应用程序。而 execle 和 execve 会用指定的环境变量去替代默认的那些。
还有 2 个以 p 结尾的函数 execlp 和 execvp,它们的第一个参数 file 可以仅仅是一个文件名,这两个函数可以自动到环境变量 PATH 指定的目录里去寻找。
五、system 函数
system 函数原型为:
#include <stdlib.h>
int system(const char * string);
system 函数通过调用 shell 程序 /bin/sh -c 来执行 string 所指定的命令,该函数在内部是通过调用 fork、execve("/bin/sh", …)、waitpid 函数来实现的。通过 system 创建子进程后,原进程和子进程各自运行,互相间关联较少。如果 system 调用成功,将返回 0。
六、popen 函数
popen 函数类似于 system 函数,与 system 的不同之处在于它使用管道工作,原型为:
#include <stdio.h>
FILE * popen(const char * command, const char * type);
int pclose(FILE * stream);
command 为可执行文件的全路径和执行参数;
type 可选参数为 “r” 或 “w” ,如果为 “w” ,则 popen 返回的文件流作为新进程的标准输入流,即 stdin ,如果为 “r” ,则 popen 返回的文件流作为新进程的标准输出流。
如果 type 是 “r” (即 command 命令执行的输出结果作为当前进程的输入结果),被调用程序的输出就可以被调用程序使用,调用程序利用 popen 函数返回的 FILE * 文件流指针就可以通过常用的 stdio 库函数 (如 fread) 来读取被调用程序的输出;
如果 type 是 “w” (即当前进程的输出结果作为 command 命令的输入结果),调用程序就可以用 fwrite 向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。
pclose 等待新进程的结束,而不是杀死新进程。