此为牛客Linux C++和黑马Linux系统编程课程笔记。
1. fork函数
1.1 fork创建单个子进程
#include<unistd.h>
pid_t fork(void);
作用:创建一个子进程。
pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)
返回值:失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0
注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。
创建失败主要有以下两个原因:
- 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
- 系统内存不足,这时 errno 的值被设置为 ENOMEM
我们常常利用fork()的返回值来判断当前在父进程还是在子进程中。
示例程序:
#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {// 父进程printf("This is parent process, pid is %d\n", getpid());} else if(pid == 0) {// 子进程printf("This is child process, pid is %d, my parent's pid is %d\n", getpid(), getppid());}return 0;
}
运行结果为:
1.2 循环创建多个子进程
如果现在想要使用fork()编写一个能够创建多个子进程的程序,该如何编写?直观的想法是直接循环:
#include <unistd.h>
#include <stdio.h>int main()
{int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();}printf("im a process, my pid is %d\n", getpid());return 0;
}
执行发现,输出了8个语句,说明一共有8个进程。
这是因为fork出的子进程也在执行当前程序,也就是说当前趟循环创建出的子进程在也会执行下一次循环的fork(),创建出子进程的子进程,最后一共创建了1+2+4=7个子进程,故一共有8个进程。
要想只创建当前父进程的3个子进程,需要如此编写:
#include <unistd.h>
#include <stdio.h>int main()
{int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();if(pid == 0) {break;}}printf("im a process, my pid is %d, my ppid is %d\n", getpid(), getppid());return 0;
}
再执行,发现输出了4个语句:
其中前三个都是父进程22000的子进程。
1.3 fork父子进程的虚拟内存空间
父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
以全局变量为例,一旦父进程或子进程要对一全局变量做修改,其对应的子进程或父进程就把该全局变量复制一份到自己的虚拟内存空间中,映射至新的物理内存,看如下示例代码:
#include <unistd.h>
#include <stdio.h>int var = 1;int main()
{int i;pid_t pid = fork();if(pid > 0) {var = 2;printf("parent var = %d\n", var);}else if(pid == 0) {var = 3;printf("child var = %d\n", var);}return 0;
}
执行结果为:
可见全局变量是读时共享写时复制。
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)
特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
2. exec函数族
2.1 介绍
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行,如图:
左边是某进程的虚拟地址空间及内容,该进程内部使用exec执行了a.out可执行文件,右边红色的是a.out文件虚拟地址空间中的用户区,则调用结束以后该进程如图:
替换原进程用户区内容,内核区不变。
exec函数族如下:
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 execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
前六个函数是标准C库函数,最后一个是linux系统函数。前六个函数是调用最后一个函数实现的。
各个函数名都已exec开头,后面为以下字母组合,分别代表不同功能:
- l(list) 参数地址列表,以空指针结尾
- v(vector) 存有各参数地址的指针数组的地址
- p(path) 按 PATH 环境变量指定的目录搜索可执行文件
- e(environment) 存有环境变量字符串地址的指针数组的地址
最常用函数为execl函数。
2.2 execl
int execl(const char *path, const char *arg, ...)
参数:
-
path:需要指定的执行的文件的路径或者名称,推荐使用绝对路径。
-
arg:是执行可执行文件所需要的参数列表,需要注意:
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称,
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
返回值:只有当调用失败,才会有返回值,返回-1,并且设置errno;如果调用成功,没有返回值。
示例程序:
主程序execl.c:
#include <unistd.h>
#include <stdio.h>int var = 1;int main()
{pid_t pid = fork();if(pid > 0) {printf("im parent, pid is %d\n", getpid());sleep(1);}else if(pid == 0) {execl("child", "child", NULL);printf("its execl.c program\n");}return 0;
}
调用的子程序child.c:
#include <unistd.h>
#include <stdio.h>int main()
{printf("im child, pid is %d, ppid is %d\n", getpid(), getppid());return 0;
}
执行主程序,结果如下:
印证了之前的说法,子进程调用execl执行child程序后,进程号不变。
而主程序中的printf("its execl.c program\n");
没有被执行,说明子进程调用execl后程序段已经被替换。
3. wait和waitpid函数
学习wait函数之前,我们最好先要了解什么是孤儿进程,什么是僵尸进程。
3.1 孤儿进程
看下面的示例程序:
#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {printf("im parent, pid is %d\n", getpid());}else if(pid == 0) {sleep(1);printf("im child, my parent pid is %d\n", getppid());}return 0;
}
fork出子进程后,让子进程睡眠1秒后再执行printf语句,此时父进程已经运行结束,该子进程便成了孤儿进程,运行结果如下:
一秒后输出:
可以看到子进程的父进程的pid为1,在linux中正是init进程对应的进程号,可见当出现孤儿进程时,init进程会”收养“该进程。
3.2 僵尸进程
如果父进程调用了wait( )或者waitpid( ),父进程将会释放已经执行完的子进程的PCB资源;如果父进程没有调用了wait( )或者waitpid( ),父进程结束后,init进程收养了子进程后,init进程也将负责释放子进程的PCB资源;但是,如果父进程是一个循环,或者一直在执行,那父进程结束之前它的已经执行完的子进程就成为了僵尸进程。
看如下示例程序:
#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {while(1) {printf("im parent, pid is %d\n", getpid());sleep(1);}} else if(pid == 0) {printf("im child, pid is %d\n", getpid());}return 0;
}
父进程循环,子进程变成了僵尸进程,使用ps -aux
查看进程状态,可以看到子进程的状态为Z+,意思时僵尸进程。
接下来继续介绍wait和waitpid函数。
3.3 wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待当前进程的任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:int *wstatus 为进程退出时的状态信息,传入的是一个int指针类型的变量,传出参数。
返回值:
-
成功:返回被回收的子进程的id
-
失败:-1 (代表所有的子进程都是结束的,调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
示例程序如下:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>int main()
{pid_t pid = fork();if(pid > 0) {int ret = wait(NULL);if(ret == -1) {printf("no child");} else {printf("child %d is dead", ret);}sleep(1);} else if(pid == 0) {while(1) {printf("im child, pid is %d\n", getpid());sleep(1);}}return 0;
}
程序运行后,子进程无限循环,父进程由于调用了wait函数,在等待子进程退出而处于阻塞态,现在我们使用kill -9 杀死子进程后,输出如下:
可见子进程结束后,父进程从wait处开始继续执行,并且返回了子进程的pid。
3.4 waitpid函数
与wait相近,只不过能够指定回收的进程pid。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
pid:
- pid > 0 : 某个子进程的pid
- pid = 0 : 回收当前进程组的所有子进程
- pid = -1 : 回收任意子进程,相当于 wait()
- pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
options:设置阻塞或者非阻塞
- 0 : 阻塞
- WNOHANG : 非阻塞
返回值:
- > 0 返回子进程的id
- 0 : options=WNOHANG, 且子进程正在运行。
- -1 :错误,或者没有子进程了
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。