Linux fork函数详解

1 基本介绍

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void)
  • 描述

    fork用于创建一个子进程,它与父进程的唯一区别在于其PID和PPID,以及资源利用设置为0。文件锁和挂起信号(指已经被内核发送给一个进程,但尚未被该进程处理的信号)不会被继承,其他和父进程几乎完全相同:会获得父进程的内存空间、栈、数据段、堆、打开的文件描述符、信号处理函数、进程优先级、环境变量等资源的副本。

  • 返回值

    成功时,在父进程中返回子进程的 PID,在子进程中返回 0 0 0。失败时,父进程返回 − 1 -1 1,不创建子进程,并适当设置 errno。

    其中errno是一个全局变量,它用于表示最近一次系统调用或库函数调用产生的错误代码。当系统调用或库函数失败时,它们通常会设置 errno 以指示错误的原因。

    以下是一些常见的 errno 错误代码及其含义:

    • EAGAIN:资源暂时不可用,通常是因为达到了系统限制,如文件描述符或内存限制。
    • ENOMEM:内存不足,无法分配请求的资源。
    • EACCES:权限不足,无法访问某个资源。
    • EINTR:系统调用被信号中断。
    • EINVAL:无效的参数。
  • 重点

    fork() 函数创建的子进程会从父进程复制执行顺序。具体来说,子进程会从父进程复制当前的执行上下文,包括指令指针(instruction pointer)和寄存器的状态。这意味着子进程将从 fork() 系统调用之后的指令开始执行,与父进程在 fork() 之后应该执行的指令完全相同。因此,fork() 之后通常会有一个基于返回值的分支结构,以区分父进程和子进程的执行路径。

2 fork实例

2.1 多个fork返回值

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid1 = fork();
    pid_t pid2 = fork();
    pid_t pid3 = fork();
    pid_t pid  = getpid();
    printf("The PID of the current process is %d\n Hello World from (%d, %d, %d)\n", pid, pid1, pid2, pid3);
    return 0;
}

这段程序包含了三个 fork() 调用,每个 fork() 都会创建一个新的子进程。由于每次 fork() 调用都会导致进程数翻倍,所以总共会有 2 3 = 8 2^3=8 23=8个进程 (包括最初的父进程)。每个进程都会打印出它的进程 ID (pid) 以及三个 fork() 调用的返回值 (pid1, pid2, pid3)。

得到的输出结果如下:

image-20240312193151556

我们画个状态机来理解它们的输出,假设最初的父进程PID为291871:

fork_information

2.2 C语言 fork与输出

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int n = 2;
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello\n");
  }
  for (int i = 0; i < n; i++) {
    wait(NULL);
  }
}

这段代码中,按我们的理解,第一次fork后有2个进程,然后一起执行printf输出,得到两个Hello,然后第二次fork后有4个进程,然后执行printf,得到四个Hello,则会有6个``Hello`,如下:

image-20240312200038027

但是当我们将输出通过管道传给cat等命令时,会看到8个Hello

image-20240312200714610

这是因为标准输出一般是行缓冲的,碰到\n,缓冲区中的内容会被刷新,即输出到终端或文件中。这种缓冲方式的目的是为了提高效率,因为这样可以减少对磁盘 I/O 的调用次数。

如果标准输出被重定向到管道,它可能不再是行缓冲的,而是变为全缓冲的。这意味着缓冲区可能会在填满时刷新,而不是在每次遇到换行符时刷新。如果缓冲区足够大,以至于可以容纳所有的 Hello 输出,那么fork的时候子进程也会复制缓冲区,导致最后每个进程中的缓冲区都有2个Hello,最后输出为8个。

如果为了确保缓冲区在需要的时候被刷新,可以在 printf 调用之后显式地调用 fflush(stdout) 来刷新标准输出缓冲区。这样可以确保所有的输出都被立即写入,而不会受到缓冲行为的影响。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int n = 2;
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello\n");
    fflush(stdout);
  }
  for (int i = 0; i < n; i++) {
    wait(NULL);
  }
  return 0;
}

image-20240312201140424

2.3 fork 💣

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  while(1) {
      fork();
  }
  return 0;
}

这段代码会无限循环地调用 fork() 函数,每次循环都会创建一个新进程。由于每次 fork() 调用都会成功创建一个新进程,而且这个新进程又会立即进入下一次循环并再次调用 fork(),因此进程的数量会以指数速度增长,很快就会耗尽系统的可用资源。

绝对不要在任何生产环境或您没有权限的任何系统上运行fork炸弹。

相关推荐

  1. Python中函数详解

    2024-03-13 12:26:04       32 阅读
  2. 回调函数详解

    2024-03-13 12:26:04       40 阅读
  3. MySQL 窗口函数详解

    2024-03-13 12:26:04       28 阅读
  4. Linux Shell 函数详解

    2024-03-13 12:26:04       20 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-13 12:26:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-13 12:26:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-13 12:26:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-13 12:26:04       20 阅读

热门阅读

  1. 面试几个问题总结

    2024-03-13 12:26:04       18 阅读
  2. Mac上.bashrc转.zshrc时,设置PATH环境变量注意事项

    2024-03-13 12:26:04       19 阅读
  3. Mac 免费模拟器推荐适配m1芯片

    2024-03-13 12:26:04       19 阅读
  4. 在linux中查询运行日志的方法

    2024-03-13 12:26:04       19 阅读
  5. VUE+VScode+elementUI开发环境

    2024-03-13 12:26:04       17 阅读
  6. YoloV8实战:YoloV8-World应用实战案例

    2024-03-13 12:26:04       20 阅读
  7. c语言读写日志代码实现

    2024-03-13 12:26:04       22 阅读
  8. 力扣-中等

    2024-03-13 12:26:04       19 阅读
  9. OpenCV-交互相关接口

    2024-03-13 12:26:04       22 阅读
  10. 突破编程_C++_设计模式(责任链模式)

    2024-03-13 12:26:04       20 阅读