016 进程控制 —— 进程创建
🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 进程控制 —— 进程创建
- 一、`fork()` 函数基础
- 1. `fork()` 的作用
- 2. 写时拷贝(Copy-On-Write, COW)
- 二、代码示例
- 示例 1:基础 `fork()` 使用
- 示例 2:循环创建多个子进程
- 三、`fork()` 常见问题
- 1. `fork()` 失败的原因
- 2. 避免子进程成为僵尸
- 3. 父子进程共享的资源
- 四、进阶用法
- 1. 父子进程分工
- 2. 链式创建进程
- 2. 链式创建进程
- 共勉
进程控制 —— 进程创建
一、fork()
函数基础
1. fork()
的作用
- 创建子进程:通过复制父进程的地址空间生成一个新进程。
- 调用一次,返回两次:
- 父进程返回子进程的 PID(即 > 0 or 正数)。
- 子进程返回 0。
- 失败返回 -1。
pid_t fork(void);
2. 写时拷贝(Copy-On-Write, COW)
-
机制:
fork
时不会立刻复制父进程的所有内存页。fork()
后,父子进程 共享物理内存(共享内存页(只读)),直到一方尝试修改数据时,内核才复制该内存页。- 修改时触发“页错误”
- 操作系统才会为该进程分配新的物理页,完成“真正拷贝”
-
优点:
- 提升效率: 减少
fork()
的开销(避免立即复制全部内存)。 - 节省内存开销: 节省物理内存(共享未修改的页)。
- 提升效率: 减少
[!NOTE]
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
二、代码示例
示例 1:基础 fork()
使用
#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;
}
运行结果:
说明:
pid:31612 before! # 父进程打印
pid:31612 after! # 父进程打印
pid:31613 after! # 子进程打印(PID不同)
关键点:
fork()
前的代码仅父进程执行,之后的代码父子进程均执行。- 父子进程的
printf
输出顺序不确定,由调度器决定。
示例 2:循环创建多个子进程
#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;
}
运行结果:
我是子进程:1761, ppid:1760
我是子进程:1762, ppid:1760
我是子进程:1763, ppid:1760
我是子进程:1764, ppid:1760
我是子进程:1765, ppid:1760
(每个子进程完成10次输出后)
关键点:
- 子进程立即退出循环:通过
if (pid == 0)
确保子进程执行runChild()
后调用exit(0)
,避免子进程继续for
循环。 - 父进程管理子进程:父进程在循环中创建所有子进程后退出。但是父进程没有调用
wait
回收子进程,子进程结束后将变成僵尸进程。 - 并发执行:所有子进程同时运行,输出顺序交错(由调度器决定)。
三、fork()
常见问题
1. fork()
失败的原因
- 系统限制:进程数超过
RLIMIT_NPROC
限制。 - 内存不足:无法复制页表或分配 PID。
- 资源耗尽:如内核进程表满。
如果 fork()
返回 -1
,通常是以下原因:
原因 | 描述 |
---|---|
进程数超出限制 | 系统有最大进程数限制(ulimit -u ) |
内存不足 | 无法分配页表或必要资源 |
权限问题 | 某些系统限制普通用户创建大量进程 |
系统负载过高 | 为了保护系统稳定性,内核可能拒绝 fork |
建议加上错误处理:
if (pid < 0)
{perror("fork failed");exit(1);
}
2. 避免子进程成为僵尸
- 父进程需调用
wait()
或waitpid()
回收子进程资源。 - 或忽略
SIGCHLD
信号:signal(SIGCHLD, SIG_IGN); // 自动回收子进程
3. 父子进程共享的资源
- 共享:
- 文件描述符(打开的文件)。
- 信号处理函数(但信号掩码独立)。
- 独立:
- 内存数据(因 COW 机制)。
- 进程 ID、父进程 ID。
四、进阶用法
1. 父子进程分工
pid_t pid = fork();
if (pid == 0)
{execvp("ls", (char* []) { "ls", "-l", NULL }); // 子进程执行任务
}
else
{wait(NULL); // 父进程等待子进程
}
2. 链式创建进程
pid_t pid = fork();
if (pid == 0)
{execvp("ls", (char* []) { "ls", "-l", NULL }); // 子进程执行任务
}
else
{wait(NULL); // 父进程等待子进程
}
2. 链式创建进程
for (int i = 0; i < N; i++)
{if (fork() == 0){printf("Child %d\n", i);exit(0);}
}
共勉