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

文章目录

    • 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.mzph.cn/news/749554.shtml

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

相关文章

PS学习-抠图-蒙版-冰块酒杯等透明物体

选中图&#xff0c;ctrlA 全选 ctrlC复制 创建一个蒙版图层 选中蒙版Alt 点击进入 ctrlv 复制 ctrli 反转 原图层 ctrldelete填充为白色 添加一个背景&#xff0c;这个方法通用 首选创建一个 拖到最底部 给它填充颜色 这个可能是我图片的原因。视频是这样做的

五子棋小游戏(sut实验报告)

实验目的 实现人与人或人与电脑进行五子棋对弈 实验内容 启动游戏&#xff0c;显示游戏参数设置界面&#xff0c;用户输入参数后进入游戏界面&#xff0c;显示棋盘及双方博弈过程&#xff0c;游戏过程中可选择退出游戏。判定一方获胜后结束本局游戏&#xff0c;可选择继续下…

Fiddler抓不到包

fiddler该设置的设置好之后&#xff0c;为啥就是抓不到包 以下都是以谷歌浏览器为例子 方法一&#xff1a; 将fidder证书导入到浏览器&#xff0c;设置搜索证书-->安全-->管理证书 这里可以看到将证书导入之后样子&#xff0c;名字为&#xff1a;DO_NOT_TRUST_Fiddler…

Linux操作系统裸机开发-环境搭建

一、配置SSH服务 1、下载安装ssh服务输入以下命令 sudo apt-get install nfs-kernel-server portmap2、建立一个供SSH服务使用的文件夹如以下命令 mkdir linux 3、完成前两步之后需要将其文件路径放到/etc/exports文件里输入以下命令&#xff1a; sudo vi /etc/esports 4.打…

线性回归 quickstart

构建一元一次方程 100个&#xff08;X, y &#xff09;&#xff0c;大概是’y3x4’ import numpy as npnp.random.seed(42) # to make this code example reproducible m 100 # number of instances X 2 * np.random.rand(m, 1) # column vector y 4 3 * X np.random…

最详细数据仓库项目实现:从0到1的电商数仓建设(数仓部分)

1、数仓 数据仓库是一个为数据分析而设计的企业级数据管理系统&#xff0c;它是一个系统&#xff0c;不是一个框架。可以独立运行的&#xff0c;不需要你参与&#xff0c;只要运行起来就可以自己运行。 数据仓库不是为了存储&#xff08;但是能存&#xff09;&#xff0c;而是…

创业板指399006行情数据API接口

# 测试&#xff1a;返回不超过10条数据&#xff08;2年历史&#xff09; https://tsanghi.com/api/fin/index/CHN/daily?tokendemo&ticker399006&order2Python示例 import requestsurl f"https://tsanghi.com/api/fin/index/CHN/daily?tokendemo&ticker399…

EtherCAT 开源主站 IGH 在 linux 开发板的移植和伺服通信测试

手边有一套正点原子linux开发板imax6ul&#xff0c;一直在吃灰&#xff0c;周末业余时间无聊&#xff0c;把EtherCAT的开源IGH主站移植到开发板上玩玩儿&#xff0c;搞点事情做。顺便学习研究下EtherCAT总线协议及其对伺服驱动器的运动控制过程。实验很有意思&#xff0c;这里总…

【JDBC编程】 Java程序操作数据库

目录 一、数据库编程的必备条件 二、什么是JDBC&#xff1f; 三、JDBC的使用 1. 准备工作 2. 建立数据库连接 2.1 加载驱动程序 2.2 数据库连接池技术 3. 正式操作 四、JDBC的局限性与MyBatis的优势 一、数据库编程的必备条件 编程语言&#xff0c;如Java&#xff0…

创业新手看过来!四招助你开启成功之旅

如果你每个月的薪资仅有几千块&#xff0c;还背负着债务的重担&#xff0c;家中的老少都期盼着你为他们撑起一片天&#xff0c;那么&#xff0c;你每日都可能为了如何打破这一困境而焦虑不安。不过&#xff0c;请稍安勿躁&#xff0c;今天我将为你提供四个建议&#xff0c;或许…

Transformer学习笔记(二)

一、文本嵌入层Embedding 1、作用&#xff1a; 无论是源文本嵌入还是目标文本嵌入&#xff0c;都是为了将文本中词汇的数字表示转变为向量表示&#xff0c;希望在这样的高维空间捕捉词汇间的关系。 二、位置编码器Positional Encoding 1、作用&#xff1a; 因为在Transformer…

解锁区块链游戏数据解决方案

作者&#xff1a;stellafootprint.network 随着区块链技术的日新月异&#xff0c;游戏行业正迎来一场革命&#xff0c;催生了区块链游戏的崛起。这一变革不仅为用户带来了全新的互动体验&#xff0c;也开辟了全新的盈利渠道。然而&#xff0c;在这一新兴领域&#xff0c;数据的…

html--花瓣

代码 <!DOCTYPE html> <html lang"en" ><head> <meta charset"UTF-8"> <title>Petals</title><link rel"stylesheet" href"css/style.css"></head><body><div class"…

Jpg图片变gif怎么操作?1分钟教你在线生成

jpg是一种常见的图像文件格式&#xff0c;它使用有损压缩算法来减小文件大小。这意味着JPG图像可以在保持较高质量的同时减小文件大小&#xff0c;但会导致一些细节的损失。JPG图像适用于存储照片和其他需要高质量图像的场景。而GIF是一种支持动画的图像文件格式。与JPG不同&am…

【类脑智能】脑网络通信模型分类及量化指标(附思维导图)

脑网络通信模型分类及量化指标(附思维导图) 参考论文&#xff1a;Brain network communication_ concepts, models and applications 概念 脑网络通信模型是一种使用图论和网络科学概念来描述和量化大脑结构中信息传递的模型。这种模型可以帮助研究人员理解神经信号在大脑内…

管理类联考–复试–政治--二十大--记忆宫殿

文章目录 整体记忆宫殿门床头柜床书桌阳台 口诀记忆法 整体 记忆宫殿 要有逻辑的放到房间了 何为逻辑&#xff0c;如下大佬总结的便是&#xff0c;或者可自行总结&#xff0c;有前后顺序&#xff0c;做事逻辑即可 第一步&#xff1a;将逻辑的点放到房间里的点&#xff0c;…

CASA模型在陆地生态系统碳循环研究中的应用探讨

植被&#xff0c;作为陆地生态系统的重要基石&#xff0c;对维护生态环境功能具有不可替代的作用。其中&#xff0c;植被净初级生产力&#xff08;NPP&#xff09;是衡量植被生态系统健康与功能的关键指标。它反映了单位面积上绿色植被通过光合作用生产的有机质总量在扣除自养呼…

基于springboot实现房源出租信息系统项目【项目源码+论文说明】

基于springboot实现房源出租信息系统演示 摘要 近些年来在一线城市的房子需求量在逐步递增&#xff0c;其中租房子和出租房子的需求业务不断增加。那么租房对于我们一线二线城市来说是一个非常大&#xff0c;而且具有经济提升能力的业务场景。那么信息技术在此行业的加持早已是…

SwiftUI自定义ButtonStyle

SwiftUI自定义ButtonStyle 记录一下如何通过自定义SwiftUI自定义ButtonStyle&#xff0c;来给按钮设计一个点击样式 import SwiftUI /*本文章 通过创建ButtonStyle&#xff0c;来自定义按钮的点击动画*/struct PressButtonStyle: ButtonStyle {func makeBody(configuration: …

2024-03-14 Android app runOnUiThread 函数,它的作用是让一个Runnable对象在主线程(UI线程)上运行。

一、看到别人app有这么一个runOnUiThread 函数用法。 二、在Android中&#xff0c;runOnUiThread 是一个非常重要的方法&#xff0c;它的作用是让一个Runnable对象在主线程&#xff08;UI线程&#xff09;上运行。在Android中&#xff0c;主线程是负责更新UI的线程&#xff0c;…