C 语言编程 — fork 进程操作
Posted 范桂飓
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C 语言编程 — fork 进程操作相关的知识,希望对你有一定的参考价值。
目录
文章目录
用户进程
User Process 由 Kernel 创建、调度和销毁,运行在 User Space 中,是系统资源分配的单元。
在 Kernel 中 User Process 使用一一对应的 task_struct 结构体表示。task_struct 是一个非常庞大的数据结构,存储了 User Process 的所有信息,Kernel 以此来对 User Process 进行管理,所以也称为 PCB(Process Control Block,进程控制块),或进程描述符(Process Descriptor)。
// include/linux/sched.h
struct task_struct
volatile long state; // 进程状态
void *stack; // 进程堆栈指针
struct list_head tasks; // 进程链表指针
pid_t pid; // 进程 ID
pid_t tgid; // 进程组 ID
struct task_struct *parent; // 父进程指针
struct list_head children; // 子进程链表指针
struct mm_struct *mm; // 进程地址空间指针
struct files_struct *files; // 进程文件描述符表指针
struct signal_struct *signal; // 进程信号表指针
struct sighand_struct *sighand; // 进程信号处理函数表指针
struct task_struct *group_leader; // 进程组领导指针
struct completion *vfork_done; // vfork 子进程完成事件
int vfork_mode; // vfork 子进程标志
...
;
进程调度的状态机
- 创建状态(New):进程正在被创建中,尚未到就绪状态。
- 就绪状态(Ready):进程已处于准备运行状态,即:进程获得了除了 CPU 之外所需的一切资源,一旦得到 CPU,即可运行。
- 运行状态(Running):进程正在 CPU 上运行。
- 阻塞状态(Waiting):进程正在等待某一事件而暂停运行,此时不会占用 CPU,例如:I/O 场景中,在等待 Buffer 就绪。
- 结束状态(terminated):释放进程资源。
子进程
- Parent 调用 fork() 指示创建 Child:Kernel 首先会新建 Child PCB,设置 PID,并关联 Parent PCB。
#include <unistd.h>
pid_t fork(void);
-
fork() 会特殊的返回 2 次:程序需要通过 if/else 语句来判断 2 个不同的 pid 数值,以此区分 Parent 和 Child 的处理逻辑。
- pid > 0:是返回给 Parent 的 Child PID。
- pid = 0:表示当前代码块运行在 Child。
- pid < 0:创建失败,可以通过 errno 变量获取错误码。失败的原因可能是系统资源不足,或者是 Parent 已经创建了太多的 Childs。
-
Parent 调用了 exec() 后,Child 才正式启动:Kernel 为 Child 分配 VAS(Virtual Address Space,虚拟地址空间),并将 Parent VAS 的数据 Copy 到 Child VAS,然后初始化 Child VAS 中的 PC(Process Count,程序计数器)值,开始 CPU 执行。可见,Parent 和 Child 的唯一区别就是 PID 不同而已。
NOTE:exec() 实现了一种 COW(Copy On Write,写时复制)机制,即:只有在需要的时候才开始 Copy 数据,避免不必要的开销,因为 Parent VAS 中的数据量可能非常庞大。
-
Child 完成 exec() 后,跟着调用 exit() 指示开始退出:Child 需要向 Parent 返回执行状态。此时的 Child 首先会进入僵尸进程状态,然后等待 Parent 切实接收到返回。
-
Parent 调用 wait() 等待 Child 退出,并获取 Child 的执行状态:切实收到 Child 的返回后,才开始释放 Child 的所有资源。以此来避免了 Child 执行状态的丢失。
示例代码:通过 pid 数值判断,在 if/else 代码块中实现父子进程各自的逻辑。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
pid_t pid;
int status;
pid = fork();
if (pid == 0)
// 子进程中执行 ls 命令
execlp("ls", "ls", "-l", NULL);
printf("exec failed\\n");
else if (pid > 0)
// 父进程中等待子进程结束
wait(&status);
printf("child process exited with status %d\\n", status);
else
printf("fork failed\\n");
return 0;
Child exec 函数族
这些函数都会在当前的 Parent 中执行一个新的 Child 程序。
- execl 函数:
- path 参数:可执行程序的文件名(绝对路径);
- arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
int execl(const char *path, const char *arg, ...);
- execle 函数:
- path 参数:可执行程序的文件名(绝对路径);
- arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
- envp 参数:环境变量数组,最后一个元素必须是 NULL。
int execle(const char *path, const char *arg, ..., char *const envp[]);
- execlp 函数:
- file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
- arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
int execlp(const char *file, const char *arg, ...);
- execv 函数:
- path 参数:可执行程序的文件名(绝对路径);
- argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
int execv(const char *path, char *const argv[]);
- execvp 函数:
- file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
- argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
int execvp(const char *file, char *const argv[]);
- execvpe 函数:
- file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
- argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
- envp 参数:环境变量数组,最后一个元素必须是 NULL。
int execvpe(const char *file, char *const argv[], char *const envp[]);
Parent wait 函数族
这些函数都是阻塞调用,即 Parent 会一直等待直到 Child 结束才会返回。
- wait 函数:等待一个 Child 退出。
- status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
- 函数返回值:
- 成功:返回退出的 Child PID。
- 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
pid_t wait(int *status);
- waitpid 函数:等待一个指定 PID 的 Child 退出。
- pid 参数:指定 Child。如果 pid 为 -1,则等待任意一个 Child 退出。
- status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
- options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
- 函数返回值:
- 成功:返回退出的 Child PID。
- 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
pid_t waitpid(pid_t pid, int *status, int options);
- wait3 函数:等待一个 Child 退出,同时返回 Child 的资源使用情况,如 CPU 占用时间、内存使用情况等。
- status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
- options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
- rusage 参数:存储 Child 的资源使用情况。
- 函数返回值:
- 成功:返回退出的 Child PID。
- 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
pid_t wait3(int *status, int options, struct rusage *rusage);
- wait4 函数:等待一个指定 PID 的 Child 退出,同时返回 Child 的资源使用情况,如 CPU 占用时间、内存使用情况等。
- pid 参数:指定 Child。如果 pid 为 -1,则等待任意一个 Child 退出。
- status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
- options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
- rusage 参数:存储 Child 的资源使用情况。
- 函数返回值:
- 成功:返回退出的 Child PID。
- 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
如果关心 Child 的退出状态,可以使用 WIFEXITED()、WEXITSTATUS()、WIFSIGNALED()、WTERMSIG()、WIFSTOPPED() 和 WSTOPSIG() 宏来解析 status 参数。
多进程
IPC(多进程间通信)
Linux 操作系统中提供了多种不同的 IPC(进程间通讯)方式,来支持 Multi-Processes 之间的数据共享和通信。
-
匿名管道(pipe):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
-
命名管道(named pipe): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道。命名管道以普通文件的形式存在,严格遵循 FIFO,可以实现本机任意两个进程间通信。
-
共享内存(shared memory):是一种高效的进程间通信方式。使得多个进程可以访问同一块物理内存空间,多个进程间可以互相看见对方对共享数据的更新。Linux 提供了多种共享内存方式,例如:mmap 共享内存、XSI 共享内存、POSIX 共享内存等。这种方式需要依赖同步原语,如:互斥锁、信号量等。
-
套接字(UNIX Socket):一个进程作为服务器监听 UNIX Socket,并接收客户端的请求;另外的进程作为客户端连接到 UNIX Socket,并向服务器发送请求。
-
消息队列(Message Queuing):消息队列是一个存放在内存中的链表结构,由 Kernel 管理,具有特定的格式。
-
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
pipe() 匿名管道
C 语言的匿名管道 pipe() 定义在 unistd.h(Unix standard)中。
函数作用:创建一个管道,本质是一个 Kernel Byte Steam Buffer(字节流缓冲区),大小为 4KB,支持 FIFO 队列,数据写入管道的一端,可以从另一端读取出来。
函数原型:
- pipefd[2] 参数:是一个长度为 2 的整数数组,它包含了两个 fd(文件描述符),一个用于读取数据,另一个用于写入数据。
- pipefd[0]:读端口,从队头(Front)读。
- pipefd[1]:写管道,从犯队尾(Rear)写。
- 函数返回值:
- 成功:0,并会代表管道两端的 2 个 fd 存储在数组中。
- 失败:-1,并设置了 errno。
#include <unistd.h>
int pipe(int pipefd[2]);
示例程序:程序创建一个子进程,并创建了一个管道,子进程向管道写入一条消息,父进程从管道读取这条消息并输出到终端上。
由于子进程和父进程拥有完全相同的变量,因此子进程也有对应 pipefd[2](管道读端和写端)的两个 fd。之后,只需要关闭一侧的读端和另一侧的写端,就可以实现进程间的通信。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 256
int main()
int pipefd[2];
char buf[BUF_SIZE];
pid_t pid;
if (pipe(pipefd) == -1)
perror("pipe");
exit(EXIT_FAILURE);
pid = fork();
if (pid == -1)
perror("fork");
exit(EXIT_FAILURE);
if (pid == 0) // child process
close(pipefd[0]); // close the read end of the pipe
strcpy(buf, "Hello, parent process!\\n");
write(pipefd[1], buf, strlen(buf));
close(pipefd[1]); // close the write end of the pipe
exit(EXIT_SUCCESS);
else // parent process
close(pipefd[1]); // close the write end of the pipe
while (read(pipefd[0], buf, BUF_SIZE) > 0)
printf("Received message: %s", buf);
close(pipefd[0]); // close the read end of the pipe
exit(EXIT_SUCCESS);
dup2() 管道重定向
dup2() 函数,可以用于将一个文件描述符复制到另一个文件描述符上。在管道场景中,可通过 dup2 修改 fds,继而用于实现管道读写端的重定向。
函数原型:
- oldfd 参数:是需要被复制的 fd;
- newfd 参数:是重定向的目标 fd。如果 newfd 已经被打开了,那么 dup2() 会先关闭它,然后将 oldfd 复制到 newfd 上。
#include <unistd.h>
int dup2(int oldfd, int newfd);
程序示例:将 STDOUT_FILENO(标准输出文件描述符)重定向到一个文件中。首先 open() 一个文件,并指定了写入权限。然后,使用 dup2() 函数将 fd 复制到 STDOUT_FILENO 上。这样,所有输出 STDOUT_FILENO 的数据都会最终重定向到 output.txt 文件中。
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
int fd;
fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
perror("open");
return -1;
if (dup2(fd, STDOUT_FILENO) == -1)
perror("dup2");
return -1;
printf("Hello, world!\\n"); // this will be written to "output.txt"
close(fd);
return 0;
命名管道
匿名函数使用简单,但问题是只能用于父子进程之间,因为在父进程创建的 pipe,可以通过 fork() 的方式复制到子进程,然后两者使用同一个 pipe 进行通信。
而对于非相关进程而言,则需要使用命名管道。为了保证数据的安全,同样采用了阻塞的 FIFO,让写操作变成原子操作,行为与匿名管道类似。
函数原型:命名管道,在 Linux 文件系统中以文件的形式存在,由 filename 指定名称,而 mode 则指定了文件的读写权限。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
由于是一个真实的文件,所以命名管道也可以使用 CLI 来进行创建:
$ mkfifo fifo_file
$ mknod fifo_file p
以上是关于C 语言编程 — fork 进程操作的主要内容,如果未能解决你的问题,请参考以下文章
linux C语言 clone() 和 fork() 的区别,fork函数的用法,主要应用场景(clone是fork的升级版本,可以将创建出来的进程变成父进程的兄弟进程)
子进程是否从 Fork 函数 后 开始执行,执行函数后的代码。Fork函数之前的不执行?