进程系统调用——fork函数深入理解

article/2025/10/1 17:54:38

转载 进程系统调用——fork函数深入理解

当我们在一个现代系统上运行一个程序的时候,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一程序。我们的程序好像是独占的使用处理器和存储器。处理器就是无间断的一条一条地执行我们程序中的指令。最后我们程序中的代码和数据显得好像是系统存储器中唯一的对象。这些假象都是通过进程的概念提供给我们的。下面我们来看一下一段程序:

#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 */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid == 0)  //放在ax寄存器里
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else  //父进程里返回的是真实的 child pid
    {  
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

运行结果如下: 

è¿éåå¾çæè¿°


在没有知道进程这个概念的时候,我们看到代码,可能会认为整个代码中的if_else语句只有一个执行,要么if,要么else。但是当我们看完结果我们会感到惊讶。为什么else if和else语句都被执行呢?是不是if_else的结构被破坏了。其实不是的,这就fork的作用。fork是干什么用的呢?如何理解父进程和子进程呢?下面我们就来看看了解一下fork的一些基本知识。

fork()知识总览
fork()函数又叫计算机程序设计中的分叉函数,fork是一个很有意思的函数,它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。还有一个很奇妙的是:fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

新创建的子进程几乎但是不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。 
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。 
下面我们再以一个最简单的代码来更简单说明fork()函数:

#include <stdio.h>
#include <unistd.h>
int mian(void){
   fork();
   printf("hello");
   return 0;
}

他的运行结果我们以一个图的形式展现出来,如下: 

è¿éåå¾çæè¿°


这个例子简单明了的把fork的作用表现出来了。在这里父进程和子进程都执行printf(),所以会输出两个hello。在这里没有用到fork函数返回值得问题。其实fork函数返回值刚才上面已经说过了。如果fork出现错误,则fork返回一个负值。当然在用户态的时候的时候我们调用的是fork,但是他在内核中是如何工作的,我们可以以到内核代码去瞧一瞧,稍微的分析一下。这也是我们这一篇博客写作的重点。

fork()内核处理过程
在这里我们会提很多疑问,比如说,当在用户态调用fork()函数的时候,系统的内核是如何执行这个函数的,子进程在内核是从哪里执行的?他的堆栈有哪些变化呢?当然说道进程,那么进程控制块PCB我们肯定是要了解的。进程控制块PCB是干什么用的呢?为了描述和控制进程的运行,系统为每一个进程定义了一个数据结构——进程控制块。它是进程实体的一部分,是操作系统中最重要的记录型数据结构。或者说,OS是根据PCB来对并发程序的进程进行控制和管理的。总而言之,PCB是进程存在的唯一标志。进程控制块中的信息包括进程标识符、处理机状态、进程调度信息、进程控制信息。然而PCB在linux中具体实现是 task_struct数据结构,由于这个数据结构是相当庞大的,我们给出把一个链接(task_struct数据结构),可以到该链接下去看看。

Linux下用于创建进程的API有三个fork,vfork和clone,这三个函数分别是通过系统调用sys_fork,sys_vfork以及sys_clone实现的 
(这里目前讨论的都是基于x86架构的)。而且这三个系统调用,都是通过do_fork来实现的,只是传入了不同的参数。所以我们可以得出结论:所有的子进程是在do_fork实现创建和调用的。下面我们就来整理一下整个进程的在用户态到内核态的过程是怎么样的。fork系统调用如下: 

è¿éåå¾çæè¿°
下面我们来重点看看do_fork的代码。http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c#do_fork

long do_fork(unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
    * Determine whether and which event to report to ptracer.  When
    * called from kernel_thread or CLONE_UNTRACED is explicitly
    * requested, no event is reported; otherwise, report if the event
    * for the type of forking is enabled.
    */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    p = copy_process(clone_flags, stack_start, stack_size,
        child_tidptr, NULL, trace);
    /*
    * Do this prior waking up the new thread - the thread pointer
    * might get invalid after that point, if the thread exits quickly.
    */
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

整段代码挺长,涉及到很多工作的处理,但是整个创建新进程是在上述代码的第29行copy_process()z这个函数实现的。我们前面已经说过,子进程是通过复制实现的。为了探个究竟,我们进入到copy_process()这个函数体里可以看到几个很重要的函数,列举如下:

复制一个PCB——task_struct

p = dup_task_struct(current);
1
复制当前进程的PCB描述符task_struct。我们在进入到该函数dup_task_struct体内就可以看到这个pcb是如何复制的。主要的赋值函数是

err = arch_dup_task_struct(tsk, orig);//这一句是赋值操作
1
当然在dup_task_struct函数体内还有其他的一次辅助操作例如:

tsk = alloc_task_struct_node(node);
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig);//这里只是复制thread_info,而非复制内核堆栈

然而我们再 往dup_task_struct(current)函数往下看,后面是大量的修改进程的内容,也就是对复制过来的东西修改为子进程所拥有的数据。也就是初始化一个子进程。我们再往下看,在copy_process()函数http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c#copy_process的第1396行有一个非常重要的函数copy_thread,他是干什么的呢?我们点该函数,然后选择/linux-3.18.6/arch/x86/kernel/,然后点击进入copy_thread()函数体内瞧一瞧。进去之后我们可以看到,一部分重要代码如下:

struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;

p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

if (unlikely(p->flags & PF_KTHREAD)) {
    /* kernel thread */
    memset(childregs, 0, sizeof(struct pt_regs));
    p->thread.ip = (unsigned long) ret_from_kernel_thread;
    task_user_gs(p) = __KERNEL_STACK_CANARY;
    childregs->ds = __USER_DS;
    childregs->es = __USER_DS;
    childregs->fs = __KERNEL_PERCPU;
    childregs->bx = sp; /* function */
    childregs->bp = arg;
    childregs->orig_ax = -1;
    childregs->cs = __KERNEL_CS | get_kernel_rpl();
    childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
    p->thread.io_bitmap_ptr = NULL;
    return 0;
}
*childregs = *current_pt_regs();//拷贝父进程的内核堆栈栈底,也就是已有的内核堆栈数据的拷贝
childregs->ax = 0;//给eax赋值为0,因为子进程返回的是0,系统调用是通过eax返回的,
if (sp)
    childregs->sp = sp;//修改栈顶

p->thread.ip = (unsigned long) ret_from_fork;//给ip赋值,这就是子进程执行的起点

从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process

struct pt_regs *childregs = task_pt_regs(p);
*childregs = *current_pt_regs(); //复制内核堆栈栈底
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!

p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

上面赋值复制的内核堆栈并不是父进程的所有内核堆栈的内容,那复制的是哪些部分呢?我们可以看上面代码的第一句,其中复制的内容就是pt_regs里面的内容。里面的代码如下:

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    int  xfs;
    int  xgs;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};

父进程堆栈复制给子进程的就是上面那些参数。从copy_thread中我们就已经得出堆栈复制和子进程开始执行的起始地方。综上所述,我们对整个do_fork的分析到此就可以告一段落了。我们在回过头来总结一下,do_fork()的实现,主要是靠copy_process()完成的,这就是一环套一环。整个过程实现如下:

p = dup_task_struct(current); 为新进程创建一个内核栈、thread_iofo和task_struct,这里完全copy父进程的内容,所以到目前为止,父进程和子进程是没有任何区别的。

为新进程在其内存上建立内核堆栈

对子进程task_struct任务结构体中部分变量进行初始化设置,检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描诉符中的初始值,从这开始,父进程和子进程就开始区别开了。

把父进程的有关信息复制给子进程,建立共享关系

设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置

复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志

调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID

return ret_from_fork;返回一个指向子进程的指针,开始执行

总结
linux创建一个新的进程是从复制开始的,在系统内核里首先是将父进程的进程控制块PCB进行拷贝,然后再根据自己的情况修改相应的参数,获取自己的进程号,再开始执行。我觉得整个过程重点就是理解子进程如何创建,在内核调用的几个重要的内核函数,以及子进程怎么返回开始执行的。把握这些点就OK了


http://chatgpt.dhexx.cn/article/gV4OanMw.shtml

相关文章

进程调度实验

一、实验目的 通过编写程序实现进程或作业先来先服务、高优先权、按时间片轮转调度算法&#xff0c;进一步掌握进程调度的概念和算法&#xff0c;加深对处理机分配的理解。了解进程&#xff08;线程&#xff09;的调度机制。学习使用进程&#xff08;线程&#xff09;调度算法…

基于C++实现的进程调度算法

资源下载地址&#xff1a;https://download.csdn.net/download/sheziqiong/85650672 一、问题描述与分析 1.1 设计构想 程序能够完成以下操作:选择调度算法;查看历史记录;创建进程;进程调度:进程创建完成后就选择进程调度算法&#xff0c;每次执行的结果都在屏幕上输出。 1…

短进程优先调度算法c语言spf,短进程优先的调度算法详解

短进程优先的调度算法详解 发布时间:2020-05-17 04:52:01 来源:51CTO 阅读:293 作者:张立达 一、SPF算法简介 SJF算法SJF(shortest job first)是以进程的运行时间长度作为优先级,进程运行时间越短,优先级越高。 SJF算法的缺点必须预知进程的运行时间。即使是程序员也很难…

进程调度算法(c语言)

对一个非抢占式多道批处理系统采用以下算法的任意两种&#xff0c;实现进程调度,并计算进程的开始执行时间,周转时间,带权周转时间,平均周转时间,平均带权周转时间 1.先来先服务算法 2.短进程优先算法 *3.高响应比优先算法 一、设计思想 每个进程有一个进程控制块&#xff08;…

进程的创建——fork函数

1. 进程的信息 进程的结构 在Linux中&#xff0c;一切皆文件&#xff0c;进程也是保存在内存中的一个实例&#xff0c;下图描述了进程的结构: 堆栈:保存局部变量数据段:一般存放全局变量和静态变量代码段:存储进程的代码文件TSS状态段:进程做切换时&#xff0c;需要保存进程现场…

C语言结构体

本节主要讲解下结构体的一些易错点和重要内容 结构体变量定义 &#xff08;使用typedef起别名&#xff09; 一般的结构体定义&#xff1a;定义类型变量 struct student {long stuID;char stuName[10];char stuSex;char birthYear;int mathScore; }stu1;可以用typedef取别…

深入探索 Linux 进程信号的奥秘

Linux 进程信号 0 查看IPC(进程间通信)资源的指令1 学习进程信号的过程2 Linux 进程信号的基本概念2.1 对信号的基本认知 3 Linux 进程信号的产生方式4 Linux 进程信号的保存和处理5 Linux 进程信号递达6 volatile关键字 0 查看IPC(进程间通信)资源的指令 ipcs -m : 查看共享内…

Linux 进程信号深剖

目录 传统艺能&#x1f60e;概念&#x1f914;信号发送&#x1f914;信号记录&#x1f914;信号产生&#x1f914;常见信号处理方式&#x1f914;终端按键产生信号&#x1f914;核心转储&#x1f60b;如何调试&#x1f914; 系统函数发送信号&#x1f914;raise函数&#x1f91…

Linux进程信号

文章目录 一.信号入门二. 产生信号(1). 通过键盘按键产生信号(2). 硬件异常产生信号(3).通过系统函数发送信号(4). 由软件条件产生信号 三.阻塞信号(1). 阻塞/递达/未决概念 :(2). 信号在内核中的表示(3). sigset_t(4). 信号集操作函数(5). 处理信号 四. 可重入函数/不可重入函…

[培训-DSP快速入门-7]:C54x DSP开发环境与第一个汇编语言程序

作者主页(文火冰糖的硅基工坊)&#xff1a;https://blog.csdn.net/HiWangWenBing 本文网址&#xff1a;https://blog.csdn.net/HiWangWenBing/article/details/119011489 目录 引言&#xff1a; 第1章 DSP汇编语言编程的流程概述 第2章 汇编语言程序建立过程 2.1 建立工程…

[培训-DSP快速入门-6]:C54x DSP开发中C语言库函数的使用

作者主页(文火冰糖的硅基工坊)&#xff1a;https://blog.csdn.net/HiWangWenBing 本文网址&#xff1a;https://blog.csdn.net/HiWangWenBing/article/details/119010855 目录 第1章 DSP库函数概述 第2章 运行时支持库 2.1 如何加入运行时支持库 2.2 为什么需要运行时的库…

【DSP开发】帮您快速入门 TI 的 Codec Engine

德州仪器半导体技术&#xff08;上海&#xff09;有限公司 通用DSP 技术应用工程师 崔晶 德州仪器&#xff08;TI&#xff09;的第一颗达芬奇&#xff08;DaVinci&#xff09;芯片&#xff08;处理器&#xff09;DM6446已经问世快三年了。继DM644x之后&#xff0c;TI又陆续推出…

DSP开发,使用CCS软件建立工程以及烧录

DSP开发&#xff0c;使用CCS软件建立工程以及烧录 1 概述1.1 资源概述1.2 DSP介绍 2 工程建立步骤4 烧录到flash中4.1 通过增减文件实现4.2 增加预编译宏 5 独立下载工具5.1 Uniflash5.2 C2prog 6 程序源码6.1main()函数6.2 leds.c文件6.3 leds.h文件 1 概述 实验的代码已经上…

浅谈DSP开发创建第一个工程Hello World

浅谈DSP开发创建第一个工程Hello World 本教程以TI公司的TMS320F2812芯片为例进行演示开发环境搭建(CCS)CMD文件概述编写第一个工程Hello World概述 本教程以TI公司的TMS320F2812芯片为例进行演示 开发环境搭建(CCS) 首先开发环境问题&#xff1a;目前最新TI官方发布的开发环…

DSP(数字信号处理器)技术概要

数字信号处理器(digital signal processor,DSP)是一种用于数字信号处理的可编程微处理器&#xff0c;它的诞生与快速发展&#xff0c;使各种数字信号处理算送得以实时实现&#xff0c;为数字信号处理的研究和应用打开了新局面&#xff0c;提供了低成本的实际工作环境和应用平台…

DSP_1 环境搭建

1、打开ccs6.0&#xff0c;将DSP281x_common、DSP281x_headers两个库文件导入到根目录当中。 2、在project的Properties当中添加库文件路径&#xff0c;使编译器能够找到这些文件。 3、exclude那些重定义的文件&#xff0c;即可。 4、编译的过程分为compile与link&#xff0c…

从BES蓝牙耳机开发中谈DSP开发与嵌入式ARM的区别

对比下DSP开发与嵌入式ARM的区别&#xff0c;DSP开发&#xff0c;发开算法&#xff0c;注意链接文件的使用。 一先看BES的DSP开发 1 lds链接文件之代码段text 存放可执行代码和浮点数常量 2 data数据段 3 .bss段 存放没有初始化的全局变量和静态变量。 二 ARM开发 主要是配置…

用于多核DSP开发的核间通信

TI的多核DSP以TMS320C6678为例&#xff0c;它是多核同构的处理器&#xff0c;内部是8个相同的C66x CorePac。对其中任意一个核的开发就和单核DSP开发的流程是类似的。   但是如果仅仅只是每个核独立开发&#xff0c;那么很难体现出多核的优势。要充分发挥多核处理器的性能&am…

近期C6000 DSP开发小结

使用C开发DSP 如果你也跟我一样刚开始接触C6000系列的DSP&#xff0c;我觉得可以尝试一下用C来开发&#xff0c;虽然说这么做代码的执行效率可能会比C或者纯汇编的开发来得低&#xff0c;但它胜在能够让整个工程的脉络更加清晰。 面向C6000开发的cl6x编译器对C有比较好的支持。…

【DSP开发】CCS5.5测试代码运行时间

1、进入CCS环境&#xff0c;load已有工程.out文件&#xff0c;找到要查看的代码执行周期的地方。 2、选择CCS菜单中的Run——Clock——Enable 3、选择Run——Clock——Setup 4、在左下角观察时钟周期 5、 至此就可以解决在代码中引入<time.h>后输出为0的情况&#xff…