用户层的fork(),vfork(),clone()API函数在执行时,会触发系统调用完成从用户态陷入到内核态的过程,而上述函数的系统调用,最终实现都是通过内核函数do_fork()完成,本篇着重分析do_forkI()函数的实现过程。

Linux操作系统中,产生一个新的进程和产生一个新的线程对于内核来说,最为本质的区别在于资源是否共享。这里的资源包括进程地址空间,文件描述符,信号,命名空间等。由于笔者没有分析过Windows操作系统,故不能说出在上述两种操作系统环境下对于进程和线程的描述。

Linux操作系统中,通过fork系统调用将会创建一个子进程,根据子进程是否存在用户虚拟内存空间,可以将“进程”这个概念分为:

  • 用户进程,子进程完全独立于父进程,是拥有不同的资源的两个实体;
  • 用户线程,子进程与父进程共享资源,子进程与父进程共享用户虚拟地址空间;
  • 内核线程,由内核产生,内核线程不属于任意用户,不具有用户虚拟地址空间,负责执行内核指定的“特殊任务”,比如kswapd内核线程用于在内存空间不足时根据页面置换算法换出物理页面到swap分区。

接下来深入分析do_fork内核函数的相关实现:

1、代码树

do_fork()

——copy_process()

————dup_task_struct()

————检查是否超过最大进程数目

————初始新进程task_struct部分变量

————sched_fork()

——————__sched_fork()

——————设置新进程状态为TASK_RUNNING

——————设置新进程动态优先级为父进程普通优先级

——————加入新进程到一个调度器类,并更新调度器时钟

————copy_xxx

————为新进程分配pid

————设置进程组与父子进程关系

——wake_up_new_task()

————activate_task()

————check_preempt_curr()

上述列举了部分较为关键的执行过程,其中本文主要讨论进程调度的主要流程,对具体的进程调度策略将在后续进行介绍。

2、do_fork()的实现

整个do_fork()所完成的功能还是很明确的,即生成一个子进程,然后把它加入到CPU就绪队列,等待CPU调度,然后系统调用就返回了。

copy_process():从函数名可以看出,将父进程的相关资源复制到子进程,执行生成子进程的工作;

wake_up_new_task():将子进程加入到CPU就绪队列。

2.1、copy_process()的实现

2.1.1 dup_task_struct()的实现

dup_task_struct()为子进程分配了一个新的task_struct内存空间,子进程的task_struct可以在内核虚拟地址空间的任何空闲位置进行分配。与之相对应的,会为子进程分配两个内存页(32位操作系统中为8KB),用于存放thread_union联合。关于这个联合的结构,可以详细介绍一下。

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

上述thread_union包含两个成员。一个是thread_info结构,内核通过该结构能够快速获得进程结构体task_struct。一个是stack结构,用于保存进程内核栈。具体的图示如下:

之所以说利用thread_info结构能够快速找到进程task_struct,是有原因的:

内核堆栈和用户堆栈作用类似,只不过保存的是当前进程在进入内核后的函数栈帧。其中ebp指向函数栈帧底部,esp指向当前活动记录的位置,也即栈顶。esp随着内核函数的入栈或出栈指向的内存地址不断改变。将esp指向的内存地址屏蔽后13位,即可以得到thread_info结构的首地址,在图中对应为esp & 0xffffe000 = 0x0155a000,而thread_info结构的首地址保存的是进程结构体task_struct的指针,所以能够很容易通过取指针操作得到当前进程的进程结构体。这一点上Linux内核设计的非常巧妙,内核中的current宏即是通过这样的方式获取到当前进程信息。

由于内核栈中保存着函数栈帧数据,且最大大小不超过两个内存页(8KB),故在内核驱动编程中,切忌出现int a[4096]这样的变量声明。

上述关于内核栈进行了简单介绍,后续关于内核函数栈帧结构可以进行详细描述,包括通过ebp栈回溯方式,获取当前内核函数执行路径。

在dup_task_struct后,内核检查当前系统中的进程数目是否超过了最大限额。

2.1.2 sched_fork()的实现

这一部分主要是根据当前进程类型(实时进程或是普通进程),为进程设置调度器类。关于调度器类的分析,后文在讲到调度器的时候再展开介绍。这里只用先知道,Linux对不同类型的进程会有不同的调度算法:实时进程具有最高优先级,实时进程是实时调度类的一个实例;具有次要优先级的是非实时进程,非实时进程是CFS完全公平调度类的一个实例;具有最低优先级的是idle进程,其在CPU就绪队列中完全没有任务的时候调用,作用是让CPU不被闲置。

2.1.3 copy_xxx()的实现

之后有许多copy_xxx()函数,主要用于复制或共享特定的内核子系统资源,是否需要与父进程共享系统资源主要根据参数传入的条件进行判断。这里也是Linux系统中进程与线程区别的一个重要表现。这里主要分析两个重要函数。

copy_files(): 复制父进程文件信息,则所属文件的文件引用计数加1,表示当前文件所属进程数目增加1。这里扯一点Linux网络编程的东西能更好理解copy_files(),一个简单的多进程框架是:服务端父进程监听套接字端口,客户端请求连接,服务端fork()一个子进程与客户端建立连接套接字,如下图:

然后父进程关闭连接套接字connfd,子进程关闭监听套接字listenfd,后续父进程继续监听,子进程完成数据传输。这里父进程关闭连接套接字或子进程关闭监听套接字并不会关闭TCP连接的原因就在于,子进程复制了父进程套接字,对于套接字来说,现被两个进程使用,其引用计数为2,故在关闭后,其引用计数仍不为0,内核就不会释放套接字的系统资源,连接仍然能够进行。

copy_mm():复制父进程用户虚拟地址空间。前面提到过,对于内核线程是没有用户虚拟地址空间的,所以如果这里的父进程是内核线程,直接返回0。如果是普通用户进程,子进程复制整个用户虚拟地址空间,执行流程为:copy_mm()->dup_mmap()->copy_page_range()

在copy_page_range()中复制页目录,中间目录以及页面表。在x86 32位Linux操作系统下,主要采用二级页表分页机制,中间目录项pmd名存实亡。但由于分页方式是基于内存管理单元mmu硬件实现的,在Linux内核中,软件实现上通过将pgd指针强制类型转换为pmd指针用以绕过mmu内存管理单元的三级页表机制,这一点不再进行赘述。具体的二级页表映射方式如图:

利用虚拟地址前20位索引到物理页框,利用后12位索引物理地址。采用多级页表映射机制,极大程度上节省了内存空间。

继续分析copy_page_range()对父进程虚拟地址空间的拷贝。这里有一个非常重要的设计思想,叫做写时复制(copy on write)技术。大多数情况下,父进程在fork生成子进程后,子进程大多调用execve执行新的程序,通过从父进程拷贝过来的内容在新的执行程序加载后被抹去,实属浪费时间和空间。而高效的解决方案是,子进程只需要在建立好的虚拟地址空间中,与父进程映射同一块物理内存页面,即在子进程的页面表项中,填入与父进程页面表项一样的内容。32位的页面表项,前20位与父进程完全一样,用于指向相同的物理页框。这样仅仅需要复制页面表项,也就是32位,4个字节,而不用完全复制整个物理页表,4KB。

但仅仅这样显然是不够的,如果子进程和父进程(不是线程)分别写物理页面,两者之间又没有什么方法互相通知,那岂不是乱套了。显然,将物理页表设置为不可写,当一个进程写入物理页面,产生缺页异常,操作系统再为其复制一个新的物理页完成写操作就能够解决上述问题。32位的页面表项,后12位也就设置了这样的属性,将当前物理页面设置为可读不可写。

Linux进程调度与源码分析(三)——do_fork()的实现原理的更多相关文章

  1. Linux进程调度与源码分析(二)——进程生命周期与task_struct进程结构体

    1.进程生命周期 Linux操作系统属于多任务操作系统,系统中的每个进程能够分时复用CPU时间片,通过有效的进程调度策略实现多任务并行执行.而进程在被CPU调度运行,等待CPU资源分配以及等待外部事件 ...

  2. Spring5深度源码分析(三)之AnnotationConfigApplicationContext启动原理分析

    代码地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian AnnotationCon ...

  3. Linux进程调度与源码分析(一)——简介

    本系列文章主要是近期针对Linux进程调度源码进行阅读与分析后的经验总结,分析过程中可能结合部分Linux网络编程的相关知识以便于理解,加深对Linux进程调度的理解和知识分享. 本系列文章主要结合L ...

  4. linux调度器源码分析 - 运行(四)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 之前的文章已经将调度器的数据结构.初始化.加入进程都进行了分析,这篇文章将主要说明调度器是如何在程序稳定运 ...

  5. linux调度器源码分析 - 初始化(二)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 上期文章linux调度器源码分析 - 概述(一)已经把调度器相关的数据结构介绍了一遍,本篇着重通过代码说明 ...

  6. tomcat源码分析(三)一次http请求的旅行-从Socket说起

    p { margin-bottom: 0.25cm; line-height: 120% } tomcat源码分析(三)一次http请求的旅行 在http请求旅行之前,我们先来准备下我们所需要的工具. ...

  7. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  8. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  9. Guava 源码分析之Cache的实现原理

    Guava 源码分析之Cache的实现原理 前言 Google 出的 Guava 是 Java 核心增强的库,应用非常广泛. 我平时用的也挺频繁,这次就借助日常使用的 Cache 组件来看看 Goog ...

随机推荐

  1. 【Python】python基础_代码编写注意事项

    1. 说明使用的编译方式 1 #!/usr/bin/python 2. 说明字符编码方式 1 #coding=utf-8 3. print 默认输出是换行的,如果要实现不换行需要在变量末尾加上逗号 # ...

  2. BZOJ 1055 玩具取名(区间DP)

    很显然的区间DP,定义dp[i][j][k], 如果dp[i][j][k]=1表示字符串[i,j]可以组成k字符. # include <cstdio> # include <cst ...

  3. 【bzoj1858】[Scoi2010]序列操作 线段树区间合并

    题目描述 lxhgww最近收到了一个01序列,序列里面包含了n个数,这些数要么是0,要么是1,现在对于这个序列有五种变换操作和询问操作: 0 a b 把[a, b]区间内的所有数全变成0 1 a b ...

  4. BZOJ4152 AMPPZ2014 The Captain(最短路)

    事实上每次走到横坐标或纵坐标最接近的点一定可以取得最优方案.于是这样连边跑最短路就可以了. #include<iostream> #include<cstdio> #inclu ...

  5. [洛谷P5169]xtq的异或和

    题目大意:给你一张$n(n\leqslant10^5)$个点$m(m\leqslant3\times10^5)$条边的无向图,每条边有一个权值,$q(q\leqslant2^{18})$次询问,每次询 ...

  6. 详解利用ELK搭建Docker容器化应用日志中心

    概述 应用一旦容器化以后,需要考虑的就是如何采集位于Docker容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 收集.本文即将阐述如何利用ELK日志中心来收集容器化应用 ...

  7. BZOJ2738:矩阵乘法——题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=2738 Description 给你一个N*N的矩阵,不用算矩阵乘法,但是每次询问一个子矩形的第K小数 ...

  8. LOJ2537:[PKUWC2018]Minimax——题解

    https://loj.ac/problem/2537 参考了本题在网上能找到的为数不多的题解. 以及我眼睛瞎没看到需要离散化,还有不开longlong见祖宗. ——————————————————— ...

  9. HDU 5651 逆元

    xiaoxin juju needs help Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/ ...

  10. Codeforces Round #508 (Div. 2) E. Maximum Matching(欧拉路径)

     E. Maximum Matching 题目链接:https://codeforces.com/contest/1038/problem/E 题意: 给出n个项链,每条项链左边和右边都有一种颜色(范 ...