fork()是如何创建新进程的

本文深入解析Linux中进程创建的过程,包括使用clone、fork和vfork系统调用创建新进程的具体步骤,以及进程描述符task_struct的复制和初始化细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       进程,我们一般都把它看做是程序执行的一个实例,是进程实体的运行过程,是系统进行资源分配和调度的一个独立的单位。有产生、有消亡。当一个进程创建时,子进程可以继承父进程所有资源,撤销时,也要归还所有资源。

一、进程描述符

       我们一般所说的进程控制块PCB对应到linux内核里面是一个task_struct的类型结构,它的字段包含了与一个进程相关的所有信息。下图即是一个linux进程描述符的概况:



二、创建进程

       之前的博客中我们大致分析了在start_kernel中的0号和1号进程,也分析过进程的切换。在用户看来,每当他输入了一条指令,shell进程就创建了一个新进程,那么进程到底是怎么创建的呢?现在,我们来看看进程的创建。
       linux系统中提供了三个系统调用可以创建新进程:clone()、fork()、vfork()。实际上,不管是我们比较熟悉的fork()还是剩下的两个在linux中都是通过clone()实现的。clone()是在c语言库中定义的一个封装函数,它负责建立进程堆栈并且调用对程序员隐藏的clone()系统调用。
       进一步观察发现,linux内核中又是用do_fork()来处理这三个系统调用的。(参考博客:do_fork

三、fork()系统调用分析

        先来看看fork()函数在系统中执行过程,也就是进程是怎样创建的。
       直观来看,fork产生新进程时是复制父进程资源,并给新进程分配相应的内核堆栈,然后更改一些数据。那么实际是不是这样呢?我们结合代码来分析。
       通过fork()系统调用的实例,用gdb跟踪调试。体会创建新进程的过程。

#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) 
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else 
    {  
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

       执行这段代码会发现输出了两条信息,“This is Parent Process!” 和“This is Child Process!”、“Child Complete!”。为什么会出现这种情况呢?执行了else语句后又执行了else if语句。这就和fork这个函数有关了。因为fork在创建了子进程后,子进程执行了else if语句。那么fork创建子进程是从哪开始返回的呢?
       我们在sys_clone()、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork处设置断点。





       单步执行,一条条语句的查看,就会发现和之前描述的基本符合。具体到内核代码来分析:当调用do_fork()时,这里的关键代码是copy_process()复制进程描述符。copy_process()创建进程描述符以及进程执行所需的所有其他数据结构。而这个函数里面有dup_task_struct()操作,为子进程获取进程描述符。还会获取一块内存区,存放新进程thread_info结构和内核栈。还有一个关键函数就是copy_thread()。用发出clone()系统调用时cpu寄存器的值来初始化进程的内核栈。
       执行完do_fork()后,系统有了处于可运行态的完整子进程,但是这个子进程并没有实际运行,调度程序决定了何时运行它。比如会把esp值装入寄存器,把函数ret_from_fork()的地址放入eip中,然后返回用户态等等。在fork()系统调用结束时,新进程开始执行。返回给子进程的值是0,返回给父进程的值是子进程的PID号。

四、总结

       新的进程通过复制父进程而建立。为了创建新进程,首先在系统的物理内存中为新进程创建一个 task_struct 结构,将旧进程的 task_struct 结构内容复制到其中,再修改部分数据。接着,为新进程分配新的堆栈,分配新的进程标识符 pid。然后,将这个新 task_struct 结构的地址填到 task 数组中,并调整进程链关系,插入运行队列中。于是,这个新进程便可以在下次调度时被选择执行。此时,由于父进程的进程上下文 TSS 结构复制到了子进程的 TSS 结构中,通过改变其中的部分数据,便可以使子进程的执行效果与父进程一致,都是从系统调用中退出,而且子进程将得到与父进程不同的返回值(返回父进程的是子进程的 pid,而返回子进程的是 0)。
       

       卢鹏  原创作品转载请注明出处  
      (部分内容引用网上资源,如有不当之处,可与本人联系)



<think>嗯,用户想了解fork函数如何用于创建新进程,特别是原理和示例,在Linux环境下。首先,我需要回忆fork函数的基本机制。根据引用内容,fork函数会复制出一个子进程,之后父子进程都处于就绪状态,由内核调度执行。fork函数返回两次,父进程返回子进程的PID,子进程返回0。这点很重要,用户可能需要明确返回值的不同来区分父子进程。 接下来,用户可能需要一个具体的例子来理解。比如,代码示例应该展示fork的使用,包括如何通过返回值判断当前是父进程还是子进程。记得引用中提到,子进程的PID与父进程不同,所以示例代码中应该包含获取进程ID的部分,比如getpid()函数。 然后,原理部分需要解释写时拷贝(Copy-On-Write)机制,引用[1]提到写时拷贝发生在何时,可能是在父进程或子进程尝试修改内存时才会复制资源,这样可以提高效率。用户可能想知道为什么fork在复制进程时不会立即复制所有资源,这时候写时拷贝的优化就很重要了。 另外,用户可能对fork的实际应用场景感兴趣,比如在服务器编程中创建多个子进程处理客户端请求,或者在shell中执行命令。需要结合引用中的例子,比如Linux进程编程中的情况,说明父进程和子进程的执行顺序可能由调度决定,但通常父进程先执行还是子进程先执行是不确定的,这也需要提到。 在回答结构上,应该先讲原理,再给示例,最后说明应用场景。确保使用正确的LaTeX格式,比如行内公式用$...$,独立公式用$$...$$。同时,生成相关问题时要围绕fork函数展开,比如写时拷贝的作用、父子进程执行顺序、实际应用场景等。 还需要检查引用是否正确标注,每个引用的内容要对应到正确的来源。例如,引用[3]提到通过fork返回值判断父子进程,而引用[4]则详细说明了返回值的不同。在回答中需要正确引用这些内容,确保信息的准确性。 最后,确保语言简洁,用中文回答,代码示例用Python或C?根据引用中的例子,用户提供的引用中有C语言的示例,比如获取pid,所以可能需要用C语言来展示fork的使用。但用户的问题中没有指定语言,可能需要确认,但根据引用内容,应该用C语言更合适。</think>### fork函数创建进程的原理与示例 #### 1. **fork函数的基本原理** fork函数是Linux系统调用中用于创建新进程的核心函数,其核心机制如下: - **写时拷贝(Copy-On-Write)**:调用fork时,操作系统不会立即复制父进程的物理内存,而是共享同一份内存。只有当父进程或子进程尝试修改内存时,才会触发实际的拷贝操作[^1]。 - **进程状态**:调用fork后,子进程与父进程处于就绪状态,由内核调度决定执行顺序[^2]。 - **返回值差异**:父进程返回子进程的PID,子进程返回0,通过返回值可区分父子进程[^4]。 #### 2. **示例代码(Linux环境)** 以下代码展示了fork函数的使用及父子进程的行为: ```c #include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork failed\n"); return 1; } else if (pid == 0) { // 子进程代码 printf("Child PID: %d\n", getpid()); } else { // 父进程代码 printf("Parent PID: %d, Child PID: %d\n", getpid(), pid); } return 0; } ``` 输出可能为: ``` Parent PID: 4563, Child PID: 4564 Child PID: 4564 ``` #### 3. **关键机制解析** - **写时拷贝的优势**:避免了不必要的内存复制,提升性能。 - **调度不确定性**:父子进程执行顺序由内核调度决定,可能交替执行[^3]。 - **资源继承**:子进程继承父进程的文件描述符、信号处理函数等。 #### 4. **典型应用场景** - **服务器并发**:Web服务器通过fork为每个客户端连接创建进程。 - **Shell命令执行**:Shell通过fork创建进程执行`ls`、`grep`等命令。 - **进程池管理**:预创建多个子进程处理任务,减少频繁创建的开销。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值