• Linux进程调度与源码分析(三)——do_fork()的实现原理


            用户层的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位也就设置了这样的属性,将当前物理页面设置为可读不可写。

  • 相关阅读:
    Windows 7安装 OneDrive
    MySQL8.0降级为MySQL5.7
    Windows和Linux下安装Rsync
    Jenkins持续集成工具安装
    Pure-Ftpd安装配置
    redis安装配置
    Tcp粘包处理
    .Net Core Socket 压力测试
    使用RpcLite构建SOA/Web服务(Full .Net Framework)
    使用RpcLite构建SOA/Web服务
  • 原文地址:https://www.cnblogs.com/scu-cjx/p/8057298.html
Copyright © 2020-2023  润新知