016 进程控制 —— 进程创建

进程控制 —— 进程创建

一、fork() 函数基础

1. fork() 的作用

  • 创建子进程:通过复制父进程的地址空间生成一个新进程。
  • 调用一次,返回两次
    • 父进程返回子进程的 PID(即 > 0 or 正数)。
    • 子进程返回 0。
    • 失败返回 -1。
1
pid_t fork(void);

2. 写时拷贝(Copy-On-Write, COW)

image-20250405192403735

  • 机制fork 时不会立刻复制父进程的所有内存页。fork() 后,父子进程 共享物理内存(共享内存页(只读)),直到一方尝试修改数据时,内核才复制该内存页。

    • 修改时触发“页错误”
    • 操作系统才会为该进程分配新的物理页,完成“真正拷贝”
  • 优点

    • 提升效率: 减少 fork() 的开销(避免立即复制全部内存)。
    • 节省内存开销: 节省物理内存(共享未修改的页)。

[!NOTE]

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

image-20250714185539037


二、代码示例

示例 1:基础 fork() 使用

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 5
int main()
{
printf("pid:%d before!\n", getpid()); // fork 调用前,打印一次
fork(); // 创建子进程
printf("pid:%d after!\n", getpid()); // 父子进程都会执行这一句

return 0;
}

运行结果

image-20250405184713829

1
2
3
4
说明:
pid:31612 before! # 父进程打印
pid:31612 after! # 父进程打印
pid:31613 after! # 子进程打印(PID不同)

关键点

image-20250714185622229

  • fork() 前的代码仅父进程执行,之后的代码父子进程均执行。
  • 父子进程的 printf 输出顺序不确定,由调度器决定。

示例 2:循环创建多个子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h> // 定义 pid_t 类型
#include <unistd.h> // 定义 fork() 函数
#define N 5
void runChild()
{
int cnt = 10;
while (cnt)
{
printf("我是子进程:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}

int main()
{
int i = 0; // 避免 C99 不支持报错
for (i = 0; i < N; i++)
{
pid_t pid = fork(); // 创建子进程
if (pid == 0) // 子进程进入
{
runChild(); // 子进程执行任务
exit(0); // 子进程退出,防止继续 fork
}
else if (pid < 0) // fork失败
{
perror("fork");
exit(1);
}
// 父进程继续循环
}

return 0;
}

运行结果

1
2
3
4
5
6
我是子进程:1761, ppid:1760
我是子进程:1762, ppid:1760
我是子进程:1763, ppid:1760
我是子进程:1764, ppid:1760
我是子进程:1765, ppid:1760
(每个子进程完成10次输出后)

关键点

  1. 子进程立即退出循环:通过 if (pid == 0) 确保子进程执行 runChild() 后调用 exit(0),避免子进程继续 for 循环。
  2. 父进程管理子进程:父进程在循环中创建所有子进程后退出。但是父进程没有调用 wait 回收子进程,子进程结束后将变成僵尸进程。
  3. 并发执行:所有子进程同时运行,输出顺序交错(由调度器决定)。

三、fork() 常见问题

1. fork() 失败的原因

  • 系统限制:进程数超过 RLIMIT_NPROC 限制。
  • 内存不足:无法复制页表或分配 PID。
  • 资源耗尽:如内核进程表满。

如果 fork() 返回 -1,通常是以下原因:

原因 描述
进程数超出限制 系统有最大进程数限制(ulimit -u
内存不足 无法分配页表或必要资源
权限问题 某些系统限制普通用户创建大量进程
系统负载过高 为了保护系统稳定性,内核可能拒绝 fork

建议加上错误处理:

1
2
3
4
5
if (pid < 0)
{
perror("fork failed");
exit(1);
}

2. 避免子进程成为僵尸

  • 父进程需调用 wait()waitpid() 回收子进程资源。
  • 或忽略 SIGCHLD 信号:
    1
    signal(SIGCHLD, SIG_IGN);  // 自动回收子进程

3. 父子进程共享的资源

  • 共享
    • 文件描述符(打开的文件)。
    • 信号处理函数(但信号掩码独立)。
  • 独立
    • 内存数据(因 COW 机制)。
    • 进程 ID、父进程 ID。

四、进阶用法

1. 父子进程分工

1
2
3
4
5
6
7
8
9
pid_t pid = fork();
if (pid == 0)
{
execvp("ls", (char* []) { "ls", "-l", NULL }); // 子进程执行任务
}
else
{
wait(NULL); // 父进程等待子进程
}

2. 链式创建进程

1
2
3
4
5
6
7
8
for (int i = 0; i < N; i++)
{
if (fork() == 0)
{
printf("Child %d\n", i);
exit(0);
}
}