用户态--fork函数创建进程

2023-11-15

我们一般使用Shell命令行来启动一个程序,其中首先是创建一个子进程。但是由于Shell命令行程序比较复杂,为了便于理解,我们简化了Shell命令行程序,用如下一小段代码来看怎样在用户态创建一个子进程。

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


int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0)   
    { 
        /* error occurred */
    } 
    else if (pid == 0) 
    {
        /* child process */  
    } 
    else 
    {  
        /* parent process  */
    }
}

库函数fork

库函数fork是用户态创建一个子进程的系统调用API接口。对于判断fork函数的返回值,我们可能会很迷惑,因为fork在正常执行后,if条件判断中除了if (pid < 0)异常处理没被执行,else if (pid == 0)和else两段代码都被执行了。

实际上fork系统调用把当前进程又复制了一个子进程,也就一个进程变成了两个进程,两个进程执行相同的代码,只是fork系统调用在父进程和子进程中的返回值不同。其实是if语句在两个进程中各执行了一次,由于判断条件不同,输出的信息也就不同。父进程没有打破if else的条件分支的结构,在子进程里面也没有打破这个结构,只是在Shell命令行下好像两个都输出了,好像打破了条件分支结构,实际上背后是两个进程。fork之后,父子进程的执行顺序和调度算法密切相关,多次执行有时可以看到父子进程的执行顺序并不是确定的。

通过以上那段fork代码程序,我们可以在用户态创建一个子进程,就是调用系统调用fork。

首先来回顾系统调用是怎样工作的,并讨论创建进程和其他常见的系统调用有哪些不同。

系统调用回顾

在正常触发系统调用时,对于X86 Linux系统来说用户态有一个int $0x80或syscall指令触发系统调用,CPU跳转到系统调用入口的汇编代码执行。int $0x80指令触发entry_INT80_32并以iret返回系统调用,syscall指令触发entry_SYSCALL_64并以sysret或iret返回系统调用。

对于ARM64 Linux系统来说,用户态程序会执行svc指令触发系统调用,CPU会跳转到异常向量表(vectors)中执行,然后进入异常处理入口,即svc指令之后跳转到el0_sync和el0_svc,执行完系统调用,以eret指令返回系统调用。

系统调用从用户态陷入内核态时,所使用的的函数调用堆栈也从用户态堆栈转换到内核态堆栈,然后把相应的CPU关键的现场栈顶寄存器、指令指针寄存器、标志寄存器等保存到内核堆栈,保存现场。系统调用入口的汇编代码还会通过系统调用号执行系统调用内核处理函数,最后恢复现场和系统调用返回,将CPU关键现场栈顶寄存器、指令指针寄存器、标志寄存器等从内核堆栈中恢复到对应寄存器中,并回到用户态int $0x80/syscall或svc指令之后的下一条指令的位置(系统调用返回地址)继续执行。

fork系统调用

fork也是一个系统调用,和前述一般的系统调用执行过程大致是一样的。尤其从父进程的角度来看,fork的执行过程与前述描述完全一致,但问题是:fork系统调用创建了一个子进程,子进程复制了父进程中所有的进程信息,包括内核堆栈、进程描述符等,子进程作为一个独立的进程也会被调度,当子进程获得CPU开始运行时,它是从哪里开始运行的呢?

从用户态空间来看,就是fork系统调用的下一条指令。但fork系统调用在子进程当中也是返回的,也就是说fork系统调用在内核里面变成了父子两个进程,父进程正常fork系统调用返回到用户态,fork出来的子进程也要从内核里返回到用户态。那么对于子进程来讲,fork系统调用在内核处理程序中是从何处开始执行的呢?一个新创建的子进程是从哪行代码开始执行的,这是一个关键问题。下面带着这个问题来仔细分析fork系统调用的内核处理过程,解决这个疑问相信会更深入地理解Linux内核源代码。

进程创建的主要过程

我们先来看如何正确建立一个进程的框架。我们前面了解了创建一个进程是复制当前进程的信息,就是通过_do_fork函数来创建了一个新进程。因为父进程和子进程的绝大部分信息是完全一样的,但是有些信息是不能一样的,比如 pid 的值和内核堆栈。还有将新进程链接到各种链表中,要保存进程执行到哪个位置,有一个thread数据结构记录进程执行上下文的关键信息也不能一样,否则会发生问题。

可以想象出来这样一个框架,父进程创建一个子进程,应该会有一个地方复制了父进程的进程描述符task_struct结构体变量,并有很多地方来修改复制出来的进程描述符task_struct结构体变量。因为父子进程各自都有很多自己独立之处,子进程应该有很多地方修改内核堆栈里的信息,因为内核堆栈里的很多数据是从父进程复制来的,而fork系统调用在父子进程中分别返回到用户态,父子进程的内核堆栈中可能某些信息也不完全一样。还有thread,根据子进程复制的父进程的内核堆栈的状况,肯定要设定好指令指针和栈顶寄存器,即设定好子进程开始执行的位置。

需要特别说明的是,fork一个子进程的过程中,复制父进程的资源时采用了Copy On Write(写时复制)技术,不需要修改的进程资源父子进程是共享内存存储空间的。

有了这个框架思路之后,就可以追踪具体代码执行过程,找到这个框架思路中需要了解的相关信息。为了避免重复,在此就不再赘述触发fork系统调用的过程,而直接从_do_fork函数来跟踪分析代码,具体代码如下见kernel/fork.c。

_do_fork函数

_do_fork函数主要完成了调用copy_process()复制父进程、获得pid、调用wake_up_new_task将子进程加入就绪队列等待调度执行等。

long _do_fork(struct kernel_clone_args *args)
{
    //复制进程描述符和执行时所需的其他数据结构   
    p = copy_process(NULL, trace, NUMA_NO_NODE, args);

    wake_up_new_task(p);//将子进程添加到就绪队列

    return nr;//返回子进程pid(父进程中fork返回值为子进程的pid)
}

copy_process()函数是如何复制父进程的

copy_process()是创建一个进程的主要的代码,如下的copy_process()函数代码做了删减并添加了一些中文注释,完整代码见kernel/fork.c。

static __latent_entropy struct task_struct *copy_process(
                    struct pid *pid,
                    int trace,
                    int node,
                    struct kernel_clone_args *args)
{
    //复制进程描述符task_struct、创建内核堆栈等
    p = dup_task_struct(current, node);

    /* copy all the process information */
    shm_init_task(p);
    …
    // 初始化子进程内核栈和thread
    retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
                 args->tls);
    …
    return p;//返回被创建的子进程描述符指针
}

copy_process函数主要完成了调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时子进程置为就绪态)、采用写时复制技术逐一复制所有其他进程资源、调用copy_thread_tls初始化子进程内核栈、设置子进程pid等。其中最关键的就是dup_task_struct复制当前进程(父进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。接下来具体看dup_task_struct和copy_thread_tls。

dup_task_struct复制当前进程(父进程)描述符task_struct

static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
…
        //实际完成进程描述符的拷贝,具体做法是*tsk = *orig
        err = arch_dup_task_struct(tsk, orig);
…
        tsk->stack = stack;
...
        //实际完成进程描述符的拷贝,具体做法是*tsk = *orig  
        setup_thread_stack(tsk, orig); 
        clear_user_return_notifier(tsk);
        clear_tsk_need_resched(tsk);
        set_task_stack_end_magic(tsk);
...  
        return ts
}

还有copy_thread_tls是一个关键,在早期版本3.18.6该函数叫copy_thread,它负责构造fork系统调用在子进程的内核堆栈,也就是fork系统调用在父子进程各返回一次,父进程中和其他系统调用的处理过程并无二致,而在子进程中的内核函数调用堆栈需要特殊构建,为子进程的运行准备好上下文环境。另外还有线程局部存储TLS(thread local storage) 则是为支持多线程编程引入的,我们不去深究。

在看copy_thread_tls之前我们需要重点看一下fork子进程的内核堆栈和进程描述符的最后一个成员struct thread_struct thread。 

task_struct数据结构的最后是保存进程上下文中CPU相关的一些状态信息的关键数据结构thread。struct thread_struct在进程描述符最后定义的结构体变量thread代码如下:

    /* CPU-specific state of this task: */
    struct thread_struct        thread;

这个struct thread_struct数据结构内部的东西还比较多,其中最关键的是sp和ip。在x86下32位Linux内核3.18.6中,sp用来保存进程上下文中的ESP寄存器状态,ip用来保存进程上下文中的EIP寄存器状态;数据结构中还有很多其他和CPU相关的状态。

需要特别说明的是在5.4.34代码中struct thread_struct数据结构中没有了ip,而是将ip通过内核堆栈来保存,比如fork创建的子进程内核堆栈中会有一个ret_addr。

了解了fork子进程的内核堆栈和进程描述符的最后一个成员struct thread_struct thread,我们需要重点看一下copy_thread_tls(以5.4.34为例)和copy_thread(以3.18.6为例)。

copy_thread_tls vs. copy_thread

int copy_thread_tls(unsigned long clone_flags, unsigned long sp,
        unsigned long arg, struct task_struct *p, unsigned long tls)
{

    frame->ret_addr = (unsigned long) ret_from_fork;
    p->thread.sp = (unsigned long) fork_frame;
  
    *childregs = *current_pt_regs();


    childregs->ax = 0;

...
    /*
     * Set a new TLS for the child thread?
     */
    if (clone_flags & CLONE_SETTLS) {
            err = do_arch_prctl_64(p, ARCH_SET_FS, tls);
            ......
int copy_thread(unsigned long clone_flags, unsigned long sp,
    unsigned long arg, struct task_struct *p)
{ 

    p->thread.sp = (unsigned long) childregs;

    //复制内核堆栈(复制父进程的寄存器信息,即系统调用int指令和SAVE_ALL压栈的那一部分内容)
    *childregs = *current_pt_regs();
    
    childregs->ax = 0; //将子进程的eax置0,所以fork的子进程返回值为0
    ...
    //ip指向 ret_from_fork,子进程从此处开始执行
    p->thread.ip = (unsigned long) ret_from_fork;
 
    ...

子进程创建好了进程描述符、内核堆栈等,就可以通过wake_up_new_task(p)将子进程添加到就绪队列,使之有机会被调度执行,进程的创建工作就完成了,子进程就可以等待调度执行,子进程的执行从这里设定的ret_from_fork开始。

值得注意的是进程关键上下文的ip和sp,linux-5.4.34与早期版本有所不同,主要是指令指针ip在3.18.6版本是存放在thread.ip中,而5.4.34中则是通过frame->ret_addr直接存储在内核堆栈中。

_do_fork总结

总结来说,进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的内核堆栈并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。


以上内容为中科大软件学院《Linux操作系统分析》课后总结,感谢孟宁老师的倾心教授,老师讲的太好啦(^_^)

参考资料:《庖丁解牛Linux内核分析》    孟宁  编著

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用户态--fork函数创建进程 的相关文章

  • 为什么在排序输入上插入到树中比随机输入更快?

    现在我一直听说从随机选择的数据构建二叉搜索树比有序数据更快 这仅仅是因为有序数据需要显式重新平衡以将树高度保持在最低限度 最近我实现了一个不可变的treap http en wikipedia org wiki Treap 一种特殊的二叉搜
  • 为什么模板类的静态成员不唯一

    看一下下面的代码 include
  • Xamarin 测试记录器选项有错误。无法记录自动化测试

    选项 gt Xamarin gt Xamarin Test Recorder 中的所有设置都有错误 我的桌面上安装了 Visual Studio 2015 企业版 以及 Xamarin 和 Xamarin Test Recorder 插件
  • 如何将动态数据写入 MVC 3 Razor 中的页面布局?

    我有带有 Razor 引擎的 MVC 3 C 项目 将动态数据写入 Layout cshtml 的方法和最佳实践是什么 例如 也许我想在网站的右上角显示用户名 该名称来自会话 数据库或基于用户登录的任何内容 更新 我也在寻找将某些数据渲染到
  • 如何在 Asp.Net Core 6 中向类型化 HttpClient 添加承载令牌身份验证

    我正在尝试使用 ASP Net Core 6 设置一个 Web api 以便用户可以到达我的端点 然后我使用特权帐户在幕后的 D365 中执行一些工作 我正在使用类型化的 HTTP 客户端 但我不确定如何插入承载身份验证 以便来自该客户端的
  • ASP.NET 如何在 Web API 中读取多部分表单数据?

    我将多部分表单数据发送到我的 Web API 如下所示 string example my string HttpContent stringContent new StringContent example HttpContent fil
  • 如何准备sql语句并绑定参数?

    不幸的是 文档 http www sqlite org完全缺乏示例 这真的很奇怪 就好像它假设所有读者都是优秀的程序员一样 然而 我对C 并且无法真正从文档中弄清楚如何真正准备和执行语句 我喜欢它的实施方式PDO for PHP 通常 我只
  • 这种对有效类型规则的使用是否严格遵守?

    C99和C11中的有效类型规则规定 没有声明类型的存储可以用任何类型写入 并且存储非字符类型的值将相应地设置存储的有效类型 抛开 INT MAX 可能小于 123456789 的事实不谈 以下代码对有效类型规则的使用是否严格符合 inclu
  • 为什么下面的重叠比较总是评估为 true

    我不明白为什么以下代码有警告 指出重叠比较始终评估为真 接下来的语句永远不会被执行 QVariant MainModel data const QModelIndex index int role const if index isVali
  • 在 T4 代码生成中,如何从引用的程序集中获取类型?

    由于 T4 在项目上下文之外运行 因此我无权访问当前程序集或其他程序集 如何注册对引用程序集的访问 然后从中获取类型 我猜您想访问项目中建筑物的程序集 我在下面的示例代码中所做的是将一个名为 TestLib 的项目添加到我的解决方案中 我将
  • 在关键服务器上对字符串进行内存受限的外部排序,并合并和计算重复项(数十亿个文件名)

    我们的服务器生成如下文件 c521c143 2a23 42ef 89d1 557915e2323a sign xml在其日志文件夹中 第一部分是GUID 第二部分是名称模板 我想计算具有同名模板的文件的数量 例如 我们有 c521c143
  • 公共基类打破了元组的空基类优化

    gcc 4 7 1 对元组进行空基类优化 我认为这是一个非常有用的功能 然而 这似乎有一个意想不到的限制 include
  • 如何检查给定调用站点的重载决策集

    如何检查重载解析集 我在多个调用站点中使用了 4 个相互竞争的函数 在一个调用站点中 我期望调用一个函数 但编译器会选择另一个函数 我不知道为什么 这不是微不足道的 为了了解发生了什么 我正在使用enable if disable if打开
  • Moq - 是否可以在不使用 It.IsAny 的情况下设置模拟

    我一直使用 Moq 进行单元测试 有时我会嘲笑有很多参数的方法 想象一下这样的方法 public class WorkClient public void DoSomething string itemName int itemCount
  • 代码块 - 使用大地址感知标志进行编译

    如何使用以下命令在 64 位系统上编译 32 位应用程序LARGE ADRESS AWARE使用代码块标记 我需要使用超过 2GB 的内存 应该是添加的情况 Wl large address aware到链接标志 我不使用 CodeBloc
  • 是否可以在对Where 的调用中调用命名方法?

    我试图从 RedGate 的这本免费电子书中了解 Linq 的一些性能影响ftp support red gate com ebooks under the hood of net memory management part1 pdf f
  • 为什么 getch 不可移植?

    是什么使得 getch 本质上无法作为标准 C 函数包含在内 对于控制台界面来说 它是如此直观和优雅 如果没有它 要求输入单个字符总是会产生误导 因为用户可以输入多个键 更糟糕的是 您经常需要确保在读取控制台输入后清除标准输入 这甚至不是作
  • 曲线/路径骨架二值图像处理

    我正在尝试开发一个可以处理图像骨架的路径 曲线的代码 我想要一个来自两点之间骨架的点向量 该代码在添加一些点后结束 我没有找到解决方案 include opencv2 highgui highgui hpp include opencv2
  • 查找文本文件中每行的行大小

    如何计算每行中的字符或数字数量 是否有类似 EOF 的东西更像是行尾 您可以遍历行中的每个字符并不断增加计数器直到行尾 n 遇到 确保以文本模式打开文件 r 而不是二进制模式 rb 否则流不会自动将不同平台的行结束序列转换为 n 人物 这是
  • 从最大到最小的3个整数

    我是 C 初学者 我使用 编程 使用 C 的原理与实践 第二版 问题如下 编写一个程序 提示用户输入三个整数值 然后以逗号分隔的数字顺序输出这些值 如果两个值相同 则应将它们排列在一起 include

随机推荐

  • Mysql进阶四:常见函数-流程控制函数

    进阶四 流程控制函数 作者 alicomon 寄语读者 博客为学习记录 目的有二 记录知识点 方便温故知新 为读者提供帮助 用于交流 共同提高 流程控制函数 可好玩了 1 if函数 if else 函数 SELECT IF 10 gt 5
  • k8s之三、pod 生命周期/ 探针/ 调度策略/ 副本ReplicaSet / 控制器Deployment

    pod 生命周期官方文档 https kubernetes io zh cn docs concepts workloads pods pod lifecycle 一 容器初始化 创建当前pod容器前 先创建 依赖的其他 container
  • python打包后打开闪退问题解决方法总结

    最近写了一个python项目 今天打包后却发现没有反应 查了一些资料说在程序最后加一个input输入语句可以停留在这里 查看看到报错原因 我加了input语句之后执行下面指令进行打包 pyinstaller F hidden import
  • 整型有哪几种形式?各种形式有什么区别?

    整型有哪几种形式 C 中提供的整数类型有三种 int long short 每种类型又分为有符号和无符号两种类型 有符号整数既可以表示非负整数 又可以表示负整数 但是 无符号整数不能表示负数 只能表示非负整数 一 无符号整数 在内存中 in
  • 素数环

    昨天晚上 突然想读刘汝佳老师书中的例题 素数环 但是突然自己就有了思路 于是便自己实现了一下 但是 由于昨晚时间比较晚 程序是写完了 但是没调试出来 今天一大早就开始调试 花了半小时终于调试出来了 好开心 中心思想是 回溯 上代码 incl
  • sqli-labs布尔盲注和时间盲注相结合的脚本实现

    import time import requests 定义一个变量 用于存放cookie值 HEADER cookie Idea 69a0360c 059291e0 9967 4fe4 bb5e 2524cdcf69d4 security
  • 多元共进|探索社区故事,助力开发者成长

    技术社区中的多元交流 带来丰富的灵感思维 为开发者的发展成长注入不竭的动力 谷歌凭借多种开发者生态项目和社区活动 辅助开发者在学习和成长的道路上稳步前行 中国的开发市场目前正处于全球视野中的重要位置 与各地的开发者交流互动也成为了中国开发者
  • 利用leapmotion实现抓取一个立方体

    1 基础设置 2 拖进来LeapHandController 显示为蓝色 其中 Edit Time Pose选择的是不同的模式 是桌面模式还是VR模式 3 拖进来HandModels 此部分为手势模型 为新建空物体 然后加上Hand Mod
  • 关于visual studio中的$(ConfigurationName)疑问

    关于visual studio中的 ConfigurationName 疑问 2012 12 02 16 09 15 转载 标签 it 分类 程序员之路 关于vs中的各种路径的值de查看方法 来源 http social msdn micr
  • Python---三大流程控制

    三大流程控制语句 1 顺序 按顺序去执行步骤 是最基本的代码执行规则 不做过多的解释 2 分支 单分支 if 条件 满足条件做什么 双分支 if 条件 满足条件做什么 else 不满足条件做什么 三元运算符 双份支的简化版 result 满
  • 关于scanf和printf的一些问题&&EOF和~的问题

    1 scanf和printf的返回值 在C语言中 scanf和printf这两个函数是标准输入输出库中的函数 它们在使用时不返回具体的值 而是通过输入输出参数来完成相应的功能 scanf函数 scanf函数用于从标准输入读取数据并根据提供的
  • KNN 原理及参数总结

    文章目录 前言 1 KNN 原理 2 KNN 优缺点 3 KNN 算法三要素 4 KNN 算法实现 5 sklearn实现KNN算法 前言 针对一个完整的机器学习框架目前还没有总结出来 所以目前只能总结每一个单独的算法 由于现在研究的重点是
  • 详解虚短、虚断以及在运算放大器中的应用

    详解虚短 虚断以及在运算放大器中的应用 一 运算放大器 运算放大器 后续简称运放 是一种集成电路 内部有很多三极管类晶体管的组合 外围接很少的电子元器件就能够实现放大信号的作用 并且信号干净 漂亮 1 1 开环 闭环 运算放大器电路 开环电
  • 2021年新版-编程基础训练32题-附提示和答案

    2021年新版 编程基础训练32题 附提示和答案 1 用级数法求圆周率 题目 圆周率十分重要 不仅仅是在数学理论上 即便在千年前的古代 工程上的需求 也迫切需要我们知道圆周率的尽量精确的数值 求圆周率 有很多种方法 级数法就是简便易行的方法
  • 牛客网Python篇入门编程习题

    目 录 一 输入输出 二 类型转换 三 字符类型 四 列表类型 五 运算符号 六 条件语句 七 循环语句 八 元组类型 九 字典类型 十 内置函数 十一 面向对象 十二 正则表达 本文题库非常适合刚刚接触Python编程的同学 有兴趣的同学
  • STlink V2 烧录器使用教学 【STM32篇】

    STlink V2 是一款支持STM32 STM8 烧录的常规工具 本帖主要讲解STM32 的烧录过程 STM32有2种烧录接口 分别为古老的Jtag接口和目前最常规的SWD接口 由于SWD只需要4条线就能烧录 目前STM32硬件工程师用S
  • Unity使用C#实现简单Scoket连接及服务端与客户端通讯

    简介 网络编程是个很有意思的事情 偶然翻出来很久之前刚开始看Socket的时候写的一个实例 贴出来吧 Unity中实现简单的Socket连接 c 中提供了丰富的API 直接上代码 服务端代码 Thread connectThread 当前服
  • idea 编码扫描插件_4款好用的IDEA插件

    刚开始安装使用的IDEA是没有灵魂的 所以我们要通过插件来给 它注入灵魂 Codota 这是一款代码提示工具 根据你敲击的代码进行提示 这样再敲一些长代码时会方便很多 安装方法 点击file gt settings 选择plugins 搜索
  • 悟空CRM9从零开始搭建详细步骤——肯定成功

    悟空CRM9从零开始搭建详细步骤 欢迎留言 欢迎各位一起加入开源 愿意共享分享学习经验 特别感谢打赏点赞的朋友 我们一起努力分享更多学习经验吧 可参考其他论坛 码云https gitee com wukongcrm 72crm java 悟
  • 用户态--fork函数创建进程

    我们一般使用Shell命令行来启动一个程序 其中首先是创建一个子进程 但是由于Shell命令行程序比较复杂 为了便于理解 我们简化了Shell命令行程序 用如下一小段代码来看怎样在用户态创建一个子进程 include