关于c的子进程 fork()

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() 的行为

  1. 创建一个子进程

    当调用 fork() 时,操作系统会复制父进程的地址空间,并创建一个新的子进程。子进程的代码、数据、堆栈等资源几乎是父进程的副本。它们是独立的进程,有各自的进程 ID,但共享一部分资源,如打开的文件描述符。

  2. 父进程与子进程的执行顺序

    • 父进程fork() 会返回子进程的 PID(进程 ID),父进程可以使用这个 PID 来执行与子进程相关的操作,比如等待子进程退出等。
    • 子进程fork() 在子进程中返回 0,子进程会从 fork() 调用的下一行代码开始执行。注意,子进程与父进程是独立的,它们的执行顺序是不确定的,取决于操作系统的调度。
  3. 子进程的副本

    子进程会得到父进程几乎完全相同的副本,包括内存空间、堆栈、程序计数器等。但是,它们拥有不同的进程 ID。两个进程之间的状态是独立的,父进程对子进程的修改不会影响子进程,反之亦然。

  4. 文件描述符的共享

    父进程和子进程会共享文件描述符。这意味着,如果父进程打开了一个文件,子进程也能访问这个文件(直到文件描述符被关闭)。这对于进程间的通信非常重要。

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() 用法

  1. 进程创建

    • fork() 用于创建子进程,父子进程可以独立执行不同的任务。
    • 例如,一个父进程可以用 fork() 创建一个子进程来执行某些计算,而父进程继续执行其他任务。
  2. 进程替换(与 exec 系列函数结合使用)

    • 父进程通常会使用 fork() 创建子进程后,子进程可以调用 exec() 系列函数(如 execl(), execp(), execvp() 等)来替换自己当前的映像,加载另一个程序。
    • 这使得父进程可以启动另一个程序,而无需修改父进程的代码。
  3. 进程间通信(IPC)

    • 父进程和子进程可以通过共享内存、管道、消息队列等机制进行进程间通信(IPC)。fork() 后,父子进程可以利用共享资源进行数据交换。
  4. 创建守护进程(Daemon)

    • 在某些情况下,父进程会创建一个子进程,并让它成为一个后台守护进程。通常在这种情况下,子进程会使用 setsid() 来脱离控制终端,成为一个新的会话领导者。

fork() 的常见问题

  1. 进程资源消耗

    • 虽然 fork() 会创建一个新进程,但新进程的内存和资源是父进程的副本,这可能会导致系统资源消耗较大。在现代操作系统中,fork() 使用了 写时复制(Copy-on-write, COW) 技术,子进程和父进程在最初共享内存,直到有一方修改了内存,才会复制内存页面。
  2. 僵尸进程

    • 如果父进程没有及时等待(wait())子进程结束,子进程会变成 僵尸进程(Zombie Process)。僵尸进程会继续占用进程表项,直到父进程读取其退出状态。
  3. 孤儿进程

    • 如果父进程在子进程终止之前就结束,子进程会成为 孤儿进程(Orphan Process) ,操作系统会把它的父进程设为 init 进程(PID 1),并由 init 进程负责回收。

小结

  • 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() 后的地址空间虽然一开始相似,但随着进程的执行,它们逐渐会变得更加独立。

相关推荐
Lbs_gemini060314 分钟前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
没有名字的鬼6 小时前
C_字符串的一些函数
c语言·开发语言·算法
Heris996 小时前
零基础快速掌握——c语言基础【二维数组】
c语言·开发语言·数据结构·算法
tjsoft7 小时前
delphi 12 idhttpsever(S)+idhttp(C) 实现简单的JSON API服务
c语言·开发语言·json
武昌库里写JAVA7 小时前
SpringBoot
c语言·开发语言·数据结构·算法·二维数组
Ning_.8 小时前
C语言链表分区问题
c语言·网络·链表
敲代码的飞8 小时前
短视频矩阵的营销策略:批量混剪实现高效传播
大数据·c语言·人工智能·线性代数·矩阵
天赐学c语言9 小时前
指针(上)
c语言
小小的橙菜吖!9 小时前
STM32F103 HSE时钟倍频以及设置频率函数(新手向,本人也是新手)
c语言·stm32·单片机·嵌入式硬件
(●'◡'●)知9 小时前
常见的数据结构---队列、树与堆的深入剖析
c语言·开发语言·数据结构·c++·学习