Linux 系统调用函数fork、vfork、clone详解

news2024/11/18 3:00:38

文章目录

    • 1 fork
      • 1.1 基本介绍
      • 1.2 fork实例
        • 1.2.1多个fork返回值
        • 1.2.2 C语言 fork与输出
        • 1.2.3 fork 💣
    • 2 vfork
      • 2.1 基本介绍
      • 2.2 验证vfork共享内存
    • 3 clone
      • 3.1 基本介绍
      • 3.2 clone使用

1 fork

1.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() 之后通常会有一个基于返回值的分支结构,以区分父进程和子进程的执行路径。

1.2 fork实例

1.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

1.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

1.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炸弹。

2 vfork

2.1 基本介绍

  • 描述

    #include <sys/types.h>
    #include <unistd.h>
    
    pid_t vfork(void);
    

    vfork() 系统调用用于创建一个子进程,与 fork() 类似,但它使用父进程的地址空间,而不是复制父进程的地址空间。vfork() 调用后,父进程会阻塞,直到子进程调用 exec 函数或执行了 exit 函数。这是因为子进程需要独占父进程的地址空间,以确保数据一致性。一。在子进程调用 exec 函数或执行了 exit 函数之后,子进程将获得自己的内存空间。

  • 返回值

    fork一致

  • 重点

    1. vfork() 创建的子进程会继承父进程的环境,但不会继承父进程的堆栈。
    2. 在子进程执行这些execexit操作之前,父进程和子进程可能会访问相同的内存地址,这可能导致数据竞争和不一致。
    3. vfork() 调用成功后,子进程应该立即调用 exec 函数或执行 exit 函数。如果在子进程中修改除了用于存储从 vfork() 返回值的 pid_t 类型变量之外的任何数据,或者从调用 vfork() 的函数返回,或在成功调用 _exit()exec() 函数族中的一个函数之前调用其他任何函数,则行为是未定义的。这可能会导致程序崩溃或表现出不可预测的行为。
      因此,使用 vfork() 时,必须确保子进程在调用 exec 函数或执行 exit 函数之前不执行任何可能影响共享内存的操作。
    4. vfork() 系统调用会阻塞父进程,直到子进程完成 exec 调用或 exit 调用。父进程不需要显式调用 wait()waitpid() 来等待子进程结束。

2.2 验证vfork共享内存

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <assert.h>

int main() {
    // 在父进程中分配内存并初始化
    char *buffer = malloc(1024);
    assert(buffer != NULL);
    memset(buffer, 'A', 1024);

    // 使用vfork创建子进程
    pid_t pid = vfork();
    if (pid < 0) {
        perror("vfork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process: PID = %d\n", getpid());

        // 修改内容
        memset(buffer, 'B', 1024);
        // 子进程
        char * const argv[] = {"/bin/echo", "Hello, Linux!", NULL};
        char * const envp[] = {NULL};
        // 执行exec函数
        execve(argv[0], argv, envp);
    } else {
        // 父进程
        printf("Parent process: PID = %d, child's PID = %d\n", getpid(), pid);
        // 验证内存内容是否被子进程修改
        for (int i = 0; i < 1024; i++) {
            if (buffer[i] != 'B') {
                printf("Memory corruption detected at index %d\n", i);
                exit(EXIT_FAILURE);
            }
        }
        printf("Memory is consistent\n");
    }
    return 0;
}

这个程序的目的是验证在 vfork() 之后,子进程和父进程是否共享内存。首先在父进程中分配一块内存 ,并将其初始化为字符 ‘A’。然后,父进程调用 vfork() 创建一个子进程。在子进程中,程序试图将内容修改为字符 ‘B’,并执行 execve()。在父进程中,程序检查缓冲区的内容是否被修改为字符 ‘B’,以验证内存是否被正确共享。

程序运行结果如下:

image-20240313132239776

3 clone

3.1 基本介绍

  • 描述

    #define _GNU_SOURCE
    #include <sched.h>
    int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...
                     /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
    

    clonefork类似,是用于创建新进程的系统调用,但clone提供了更精确的控制,可以确定在调用进程(父进程)和子进程之间共享哪些执行上下文的部分。例如,调用者可以控制两个进程是否共享虚拟地址空间、文件描述符表和信号处理程序表。这些系统调用还允许将新的子进程放置在单独的命名空间中。

  • 参数

    • fn是指向新进程要执行的函数的指针,这个函数接受一个 void* 参数,并返回一个 int 类型的值,这个返回值将被 clone 系统调用捕获,并作为子进程的退出状态;

    • child_stack是新进程的堆栈地址,由于子进程和调用进程可能共享内存,因此子进程不可能与调用进程在同一堆栈中执行。调用进程必须为子堆栈设置内存空间,并将指向该空间的指针传递给clone()

    • flags可以设置新进程的属性(通过二进制位设置),包括是否与原进程共享地址空间(CLONE_VM)、是否共享文件描述符表(CLONE_FILES)、是否共享信号处理器(CLONE_SIGHAND)等等;

      int flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS;

      标志含义
      CLONE_PARENT创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
      CLONE_FS子进程与父进程共享相同的文件系统,包括root、当前目录、umask
      CLONE_FILES子进程与父进程共享相同的文件描述符(file descriptor)表
      CLONE_NEWNS在新的namespace启动子进程,namespace描述了进程的文件hierarchy
      CLONE_SIGHAND子进程与父进程共享相同的信号处理(signal handler)表
      CLONE_PTRACE若父进程被trace,子进程也被trace
      CLONE_VFORK父进程被挂起,直至子进程释放虚拟内存资源
      CLONE_VM子进程与父进程运行于相同的内存空间
      CLONE_PID子进程在创建时PID与父进程一致
      CLONE_THREADLinux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
    • arg是传递给新进程的参数;

    • 可选参数,包括 pid_t *parent_tid等。

  • 返回值

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

  • 重点

    1. clone 可以创建新的进程或线程,Linux创建线程使用的系统调用就是clone。而 forkvfork只能创建进程。这意味着 clone 可以在单个进程中创建多个线程,而 fork 则总是创建一个新的进程。
    2. clone 提供比 forkvfork 更多的选项,可以指定子进程或线程的堆栈、信号处理、权限等。
    3. clone 的使用比 forkvfork 更复杂,需要正确设置 flags、child_stack、parent_pidptr、ptr、stack_size 和 tls 等参数。

3.2 clone使用

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024) /* Stack size for cloned child */
// 宏,简化错误处理
#define ERREXIT(msg) { perror(msg); exit(EXIT_FAILURE); }
// 安全分配内存函数,分配失败报告错误
#define CHECKALLOC(ptr, msg)  ({ void *p = ptr; if (p == NULL) {ERREXIT(msg);} p;})

/*
 * 子进程函数
 * params: 接受一个void *类型参数,但是没有被使用过,后面的声明是用于告诉编译器这个参数是未被使用的
 */
static int childFunc(void *arg __attribute__((unused))) {
    puts("child: start");
    sleep(2);
    puts("child: terminate");
    return 0; /* Child terminates now */

}

int main(int argc, char *argv[]) {
    /* Start of stack buffer */
    char **stacks;
    /* Child process's pids */
    pid_t *pids;
    size_t nproc, i;

    // 接受两个参数
    if (argc != 2) {
        puts("Wrong way to execute the program:\n"
                "\t\t./waitpid nProcesses\n"
                "example:\t./waitpid 2");

        return EXIT_FAILURE;

    }
    // 初始化nproc,表示要创建的子进程数
    nproc = atol(argv[1]);  /* Process count */

    // 分配内存空间
    stacks = CHECKALLOC(malloc(nproc * sizeof(void *)), "malloc");
    pids = CHECKALLOC(malloc(nproc * sizeof(pid_t)), "malloc");

    for (i = 0; i < nproc; i++) {
        char *stackTop; /* End of stack buffer */
        stacks[i] = CHECKALLOC(malloc(STACK_SIZE), "stack malloc");
        // 得到栈顶位置
        stackTop = stacks[i] + STACK_SIZE;

        /*
         * 创建子进程
         * 第一个标志表示在子进程清除线程组ID(TID),目的是为了避免子进程与父进程或其他子进程的线程组ID冲突
         * 第二个表示告诉在子进程中设置线程ID,目的是为了允许父进程在子进程中追踪线程
         * 告诉 clone 系统调用在子进程中重新安装信号处理程序,目的是为了允许子进程捕获和处理信号,而不是传递给父进程。
         */
        pids[i] = clone(childFunc, stackTop, CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID | SIGCHLD, NULL);
        if (pids[i] == -1)
            ERREXIT("clone");
        printf("clone() returned %ld\n", (long)pids[i]);

    }

    sleep(1);

    // 等待所有子进程
    for (i = 0; i < nproc; i++) {
        // 第一个参数为子进程id,第二个参数表示不关心子进程的退出状态,第三个参数表示等待任何子进程
        if (waitpid(pids[i], NULL, 0) == -1)
            ERREXIT("waitpid");
        printf("child %ld has terminated\n", (long)pids[i]);

    }

    // 回收内存空间
    for (i = 0; i < nproc; i++)
        free(stacks[i]);
    free(stacks);
    free(pids);
    return EXIT_SUCCESS;

}

运行:gcc clone-example.c && ./a.out 5,其中5为nproc,表示要创建的进程数。

运行结果如下:

image-20240313212733984

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1521777.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年【危险化学品经营单位主要负责人】找解析及危险化学品经营单位主要负责人模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位主要负责人找解析考前必练&#xff01;安全生产模拟考试一点通每个月更新危险化学品经营单位主要负责人模拟考试题目及答案&#xff01;多做几遍&#xff0c;其实通过危险化学品经营单位主要负责人…

Grok的开源的一些想法

Grok是埃隆马斯克的人工智能团队开发的大模型&#xff0c;自马斯克发布消息称将开源大模型&#xff0c;其热度就居高不下。Grok的开源能迅速帮助国内建立起AI应用的能力。 从xAI公布的数据来看&#xff0c;Grok在主流的测试方法中均已超过GPT-3.5&#xff0c;而其是开源发展速度…

如何使用Python进行数据可视化:Matplotlib和Seaborn指南【第123篇—Matplotlib和Seaborn指南】

如何使用Python进行数据可视化&#xff1a;Matplotlib和Seaborn指南 数据可视化是数据科学和分析中不可或缺的一部分&#xff0c;而Python中的Matplotlib和Seaborn库为用户提供了强大的工具来创建各种可视化图表。本文将介绍如何使用这两个库进行数据可视化&#xff0c;并提供…

高可用系统有哪些设计原则

1.降级 主动降级&#xff1a;开关推送 被动降级&#xff1a;超时降级 异常降级 失败率 熔断保护 多级降级2.限流 nginx的limit模块 gateway redisLua 业务层限流 本地限流 gua 分布式限流 sentinel 3.弹性计算 弹性伸缩—K8Sdocker 主链路压力过大的时候可以将非主链路的机器给…

性能测试工具——wrk的安装与使用

前言 想和大家来聊聊性能测试&#xff0c;聊到了性能测试必须要说的是性能测试中的工具&#xff0c;在这些工具中我今天主要给大家介绍wrk。 ​介绍 wrk是一款开源的性能测试工具 &#xff0c;简单易用&#xff0c;没有Load Runner那么复杂&#xff0c;他和 apache benchmar…

【Unity】读取Json的三种方法(JsonUtility,LitJson,Newtonsoft)

介绍 在Unity开发过程中&#xff0c;Json是比较常用的一种数据存储文本&#xff0c;尤其是在和第三方交互中&#xff0c;基本都是json格式。 先给出一个Json示例&#xff0c;我们来看看是如何解析的。 {"Player": [{"id": 1001,"name": "…

Linux网络编程: IP协议详解

一、TCP/IP五层模型 物理层&#xff08;Physical Layer&#xff09;&#xff1a;物理层是最底层&#xff0c;负责传输比特流&#xff08;bitstream&#xff09;以及物理介质的传输方式。它定义了如何在物理媒介上传输原始的比特流&#xff0c;例如通过电缆、光纤或无线传输等。…

湖南麒麟SSH服务漏洞

针对湖南麒麟操作系统进行漏洞检测时&#xff0c;会报SSH漏洞风险提醒&#xff0c;具体如下&#xff1a; 针对这些漏洞&#xff0c;可以关闭SSH服务&#xff08;前提是应用已经部署完毕不再需要通过SSH远程访问传输文件的情况下&#xff0c;此时可以通过VNC远程登录方法&#x…

RocketMQ学习笔记四(黑马)项目

课程地址&#xff1a; 1.Rocket第二章内容介绍_哔哩哔哩_bilibili &#xff08;视频35~88&#xff0c;搭建了一个电商项目&#xff09; 待学&#xff0c;待完善。

linux网络服务学习(1):nfs

1.什么是nfs NFS&#xff1a;网络文件系统。 *让客户端通过网络访问服务器磁盘中的数据&#xff0c;是一种在linux系统间磁盘文件共享的方法。 *nfs客户端可以把远端nfs服务器的目录挂载到本地。 *nfs服务器一般用来共享视频、图片等静态数据。一般是作为被读取的对象&…

对cPanel面板上的单个网站做备份

这两天有一个老客户联系我&#xff0c;这个客户的网站是在两三年前交付的&#xff0c;这期间程序代码一直没有过更新&#xff0c;他担心有漏洞容易被入侵&#xff0c;所以再次联系我们帮他升级代码。 在更新之前&#xff0c;客户要求我们先帮他的网站做一个备份以防万一&#x…

JavaScript 基础知识

一、初识 JavaScript 1、JS 初体验 JS 有3种书写位置&#xff0c;分别为行内、内部和外部。 示例&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"wid…

LeetCode 面试经典150题 26.删除有序数组中的重复项

题目&#xff1a; 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯一元素的数量…

Java-并发编程--ThreadLocal、InheritableThreadLocal

1.ThreadLocal 作用 作用&#xff1a;为变量在线程中都创建副本&#xff0c;线程可访问自己内部的副本变量。该类提供了线程局部 (thread-local) 变量&#xff0c;访问这个变量&#xff08;通过其 get 或 set 方法&#xff09;的每个线程都有自己的局部变量&#xff0c;它独立…

深度强化学习(六)(改进价值学习)

深度强化学习(六)(改进价值学习) 一.经验回放 把智能体与环境交互的记录(即经验)储存到 一个数组里&#xff0c;事后反复利用这些经验训练智能体。这个数组被称为经验回放数组&#xff08;replay buffer&#xff09;。 具体来说, 把智能体的轨迹划分成 ( s t , a t , r t ,…

C#,深度好文,精致好码,文本对比(Text Compare)算法与源代码

Vladimir I. Levenshtein 一、文本对比的列文斯坦距离(编辑距离)算法 在日常应用中,文本比较是一个比较常见的问题。文本比较算法也是一个老生常谈的话题。 文本比较的核心就是比较两个给定的文本(可以是字节流等)之间的差异。目前,主流的比较文本之间的差异主要有两大类…

力扣112、113、101--树

112. 路径总和 题目描述&#xff1a; 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。 判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。 如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c…

海豚调度系列之:任务类型——SPARK节点

海豚调度系列之&#xff1a;任务类型——SPARK节点 一、SPARK节点二、创建任务三、任务参数四、任务样例1.spark submit2.spark sql 五、注意事项&#xff1a; 一、SPARK节点 Spark 任务类型用于执行 Spark 应用。对于 Spark 节点&#xff0c;worker 支持两个不同类型的 spark…

TinTin Web3 动态精选:以太坊坎昆升级利好 Layer2,比特币减半进入倒计时

TinTin 快讯由 TinTinLand 开发者技术社区打造&#xff0c;旨在为开发者提供最新的 Web3 新闻、市场时讯和技术更新。TinTin 快讯将以周为单位&#xff0c; 汇集当周内的行业热点并以快讯的形式排列成文。掌握一手的技术资讯和市场动态&#xff0c;将有助于 TinTinLand 社区的开…

【SpringBoot3】整合Druid数据源和Mybatis 项目打包和运行

文章目录 一、整合Druid数据源二、整合Mybatis2.1 MyBatis整合步骤2.1 Mybatis整合实践2.1 声明式事务整合配置2.1 AOP整合配置 三、项目打包和运行命令启动和参数说明 总结web 与 springboot 打包区别JDK8的编译环境 执行17高版本jar 一、整合Druid数据源 创建模块 &#xff1…