fork()
是一个非常重要的系统调用,用于在 Unix-like 操作系统中创建一个新的进程。它会将当前进程(父进程)复制成一个新的进程(子进程)。子进程会从父进程的代码处继续执行,但具有不同的进程 ID。
fork()
的语法
c
#include <unistd.h>
pid_t fork(void);
-
返回值 :
fork()
会返回一个pid_t
类型的值,表示新创建的子进程的进程 ID。- 父进程 :如果
fork()
在父进程中执行,它会返回子进程的进程 ID(正整数)。 - 子进程 :如果
fork()
在子进程中执行,它会返回 0。 - 错误 :如果
fork()
调用失败,它会返回-1
,并设置errno
来指示错误原因。
- 父进程 :如果
fork()
的行为
-
创建一个子进程 :
当调用
fork()
时,操作系统会复制父进程的地址空间,并创建一个新的子进程。子进程的代码、数据、堆栈等资源几乎是父进程的副本。它们是独立的进程,有各自的进程 ID,但共享一部分资源,如打开的文件描述符。 -
父进程与子进程的执行顺序:
- 父进程 :
fork()
会返回子进程的 PID(进程 ID),父进程可以使用这个 PID 来执行与子进程相关的操作,比如等待子进程退出等。 - 子进程 :
fork()
在子进程中返回 0,子进程会从fork()
调用的下一行代码开始执行。注意,子进程与父进程是独立的,它们的执行顺序是不确定的,取决于操作系统的调度。
- 父进程 :
-
子进程的副本 :
子进程会得到父进程几乎完全相同的副本,包括内存空间、堆栈、程序计数器等。但是,它们拥有不同的进程 ID。两个进程之间的状态是独立的,父进程对子进程的修改不会影响子进程,反之亦然。
-
文件描述符的共享 :
父进程和子进程会共享文件描述符。这意味着,如果父进程打开了一个文件,子进程也能访问这个文件(直到文件描述符被关闭)。这对于进程间的通信非常重要。
fork()
的典型用法
1. 父进程和子进程的分支执行
通常,父进程和子进程会根据 fork()
返回值进行不同的处理:
c
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
// fork失败
perror("fork failed");
return 1;
}
else if (pid == 0) {
// 子进程执行
printf("This is the child process.\n");
} else {
// 父进程执行
printf("This is the parent process. Child PID: %d\n", pid);
}
return 0;
}
- 父进程 :
pid
是子进程的 PID(一个正整数)。 - 子进程 :
pid
是 0。
2. 错误处理
如果 fork()
返回 -1
,表示创建子进程失败,通常需要检查 errno
以确定失败的原因(例如,系统资源不足、进程数达到上限等)。
c
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
}
3. 父子进程的同步
父进程和子进程是并发执行的,通常需要某种方式来协调它们的行为。例如,父进程可能会使用 wait()
或 waitpid()
等函数来等待子进程结束,以确保父进程在子进程完成后继续执行。
c
#include <sys/wait.h>
pid_t pid = fork();
if (pid == 0) {
// 子进程执行
printf("Child process\n");
} else {
// 父进程等待子进程结束
wait(NULL);
printf("Parent process after child ends\n");
}
常见的 fork()
用法
-
进程创建:
fork()
用于创建子进程,父子进程可以独立执行不同的任务。- 例如,一个父进程可以用
fork()
创建一个子进程来执行某些计算,而父进程继续执行其他任务。
-
进程替换(与
exec
系列函数结合使用):- 父进程通常会使用
fork()
创建子进程后,子进程可以调用exec()
系列函数(如execl()
,execp()
,execvp()
等)来替换自己当前的映像,加载另一个程序。 - 这使得父进程可以启动另一个程序,而无需修改父进程的代码。
- 父进程通常会使用
-
进程间通信(IPC):
- 父进程和子进程可以通过共享内存、管道、消息队列等机制进行进程间通信(IPC)。
fork()
后,父子进程可以利用共享资源进行数据交换。
- 父进程和子进程可以通过共享内存、管道、消息队列等机制进行进程间通信(IPC)。
-
创建守护进程(Daemon):
- 在某些情况下,父进程会创建一个子进程,并让它成为一个后台守护进程。通常在这种情况下,子进程会使用
setsid()
来脱离控制终端,成为一个新的会话领导者。
- 在某些情况下,父进程会创建一个子进程,并让它成为一个后台守护进程。通常在这种情况下,子进程会使用
fork()
的常见问题
-
进程资源消耗:
- 虽然
fork()
会创建一个新进程,但新进程的内存和资源是父进程的副本,这可能会导致系统资源消耗较大。在现代操作系统中,fork()
使用了 写时复制(Copy-on-write, COW) 技术,子进程和父进程在最初共享内存,直到有一方修改了内存,才会复制内存页面。
- 虽然
-
僵尸进程:
- 如果父进程没有及时等待(
wait()
)子进程结束,子进程会变成 僵尸进程(Zombie Process)。僵尸进程会继续占用进程表项,直到父进程读取其退出状态。
- 如果父进程没有及时等待(
-
孤儿进程:
- 如果父进程在子进程终止之前就结束,子进程会成为 孤儿进程(Orphan Process) ,操作系统会把它的父进程设为
init
进程(PID 1),并由init
进程负责回收。
- 如果父进程在子进程终止之前就结束,子进程会成为 孤儿进程(Orphan Process) ,操作系统会把它的父进程设为
小结
fork()
用于创建一个新的进程(子进程),它几乎是父进程的复制品。- 父进程和子进程会从
fork()
调用后的下一行代码开始并行执行,区分它们的方式是通过fork()
的返回值:父进程得到子进程的 PID,子进程得到 0。 fork()
通常与wait()
或exec()
等函数结合使用,以实现进程间的同步或程序的替换。
关于内存
在调用 fork()
后,子进程的程序地址空间会与父进程的地址空间有所不同,尽管它们最初是相同的。具体来说,父子进程的地址空间是在 虚拟内存 上分离的,每个进程都有独立的内存映射和虚拟地址空间。
fork()
后的地址空间
fork()
系统调用会创建一个新的进程(子进程),子进程是父进程的几乎完全副本,包括代码段、数据段、堆、栈等。然而,由于 写时复制(Copy-on-write, COW) 技术的存在,父进程和子进程最初会共享相同的内存页面,直到其中一个进程尝试修改这些内存页面为止。
下面是一些关键点,描述 fork()
后的内存状态:
1. 地址空间复制
fork()
创建一个新的进程(子进程),这个子进程最初会复制父进程的整个地址空间。- 父子进程的地址空间是独立的,每个进程有自己的虚拟地址,但它们会映射到操作系统内的不同物理页面(虽然这些页面最开始会共享)。
2. 写时复制(COW)
- 在
fork()
后,父进程和子进程会共享相同的物理内存页面,直到其中一个进程对共享页面进行写操作。这就是所谓的 写时复制(Copy-on-write,COW)。 - COW 的目的是延迟内存的实际复制,直到一个进程真的修改内存页面。这可以显著提高性能,因为在很多情况下,父子进程在
fork()
后可能不会修改内存。
例如:
- 父进程和子进程都指向相同的内存页面(共享内存)。
- 如果父进程或子进程修改了某个内存页面,操作系统会将该页面复制一份给修改的进程,确保父进程和子进程不再共享该页面。此时,父子进程的地址空间就变得完全独立。
3. 栈与堆
- 栈 :栈是每个进程的独立部分。
fork()
后,子进程会有自己的栈,栈的初始内容会与父进程的栈一致,但它们是独立的,互不干扰。 - 堆 :堆也是独立的,但对于动态分配的内存,父进程和子进程在
fork()
后会共享这些堆内存,直到某一进程修改该堆内存时才会触发 COW。实际情况下,每个进程会拥有自己的堆。
4. 文件描述符
- 文件描述符(File Descriptors)是父子进程共享的。父进程打开的文件会在子进程中也有效,直到某个进程关闭文件描述符或修改它们。文件描述符指向的内存区域(如文件映射内存)可能会受到 COW 的影响。
5. 地址变化的原因
- 在
fork()
后,父子进程的虚拟地址空间是相同的,但物理内存可能会不同(尤其在 COW 模式下,直到父或子进程写入共享页面,才会触发物理内存的复制)。如果子进程修改了某些数据或堆栈,虚拟地址空间会有所变化。 - 对于共享内存(如文件映射),虽然父进程和子进程可能在虚拟地址上共享一部分内存,但它们的物理地址可能是不同的。
6. 子进程地址空间的唯一性
- 即使在 COW 情况下,父进程和子进程最终会有各自独立的物理内存页面。因此,即使它们的虚拟地址相同,底层的物理内存会不同,导致它们的内存是独立的。每个进程的地址空间(即虚拟地址空间)是唯一且独立的。
小结
- 在
fork()
后,父进程和子进程的虚拟地址空间是独立的,但它们的物理地址最初可能会共享,直到某个进程修改内存页面时,才会触发物理内存的复制。 - 在 COW 模式下,只有在进程修改内存时,内存才会被复制,减少了不必要的内存开销。
- 虽然虚拟地址空间在父子进程中保持一致,但由于物理地址可能会有所不同,程序中的指针和内存地址可能会有所变化,特别是当某个进程修改内存内容时。
fork()
后的地址空间虽然一开始相似,但随着进程的执行,它们逐渐会变得更加独立。