上一篇文章我们讲到fork的时候内存管理相关的内容,时间大概隔了快一周了,发布下篇文章,写文章确实费时费力,需要仔细推敲,原创不易,希望大家多多支持吧。本文讲解fork的时候进程管理相关的内容,主要讲解fork的时候进程如何组装调用相关的基础设施组件,以及如何加入运行队列的,调度执行的时候究竟会发生什么。

注:这里只讲解cfs调度类,主要关注用户任务

3.fork的进程管理

3.1 进程相关基础设施构建

我们移步到如下调用路径(当前处于sched_fork函数中):

kernel_clone   //kernel/fork.c
->copy_process/* Perform scheduler related setup. Assign this task to a CPU. */ ->sched_fork

正如源代码中的注释一样,在这里进程调度相关的设置,以及分配cpu给进程,但是请记住:分配完cpu后进程并没有参与调度执行。

首先需要说明的一点是,进程的task_struct是资源封装和管理的结构,如管理进程的虚拟内存mm_struct,进程的打开文件files_struct等,而进程参与调度使用的是调度实体去管理调度(对于普通的进程是sched_entity)。

所以在sched_fork函数中调用__sched_fork先来初始化,基本上都是一些清零操作:

sched_fork
->__sched_fork  p->on_rq                        = 0;                                           p->se.on_rq                     = 0;     p->se.exec_start                = 0;     p->se.sum_exec_runtime          = 0;     p->se.prev_sum_exec_runtime     = 0;     p->se.nr_migrations             = 0;     p->se.vruntime                  = 0;     INIT_LIST_HEAD(&p->se.group_node);       ...

然后设置了一些比较重要的一些属性:

sched_fork
-> p->state = TASK_NEW;   //设置进程初始化状态p->prio = current->normal_prio;  //进程的动态优先级设置/*                                                                            ¦* Revert to default priority/policy on fork if requested.                   ¦*/                                                                          if (unlikely(p->sched_reset_on_fork)) {                                      if (task_has_dl_policy(p) || task_has_rt_policy(p)) {                p->policy = SCHED_NORMAL;                //调度策略                    p->static_prio = NICE_TO_PRIO(0);                  //静态优先级设置              p->rt_priority = 0;                                          } else if (PRIO_TO_NICE(p->static_prio) < 0)                         p->static_prio = NICE_TO_PRIO(0);                            p->prio = p->normal_prio = __normal_prio(p);                         set_load_weight(p, false);    //设置进程权重                                        ...                             }                                                                            if (dl_prio(p->prio))                                                        return -EAGAIN;                                                      else if (rt_prio(p->prio))                                                   p->sched_class = &rt_sched_class;                                    else                                                                         p->sched_class = &fair_sched_class;     //设置调度类为cfs                             __set_task_cpu(p, smp_processor_id());       //设置 进程运行的cpu为当前cpuif (p->sched_class->task_fork)               p->sched_class->task_fork(p);        //执行调度类的task_fork方法即是task_fork_fair#if defined(CONFIG_SMP)                 p->on_cpu = 0;
#endif                                  init_task_preempt_count(p);     //初始化抢占计数器

可以看出这里主要设置了一些调度相关的属性:如调度优先级(一般设置为nice为0),调度策略为SCHED_NORMAL,调度类为公平调度类,进程权重信息等。

然后设置新的进程在当前cpu上。

接下来就调用了调度类的task_fork进行设置虚拟运行时间等(注意在task_fork_fair中会将设置的vruntime减去当前cpu运行cfs队列的最小min_vruntime,唤醒的时候会加上所在cpu运行队列的min_vruntime)。

3.2 修改异常上下文和调度上下文信息

上面构建好调度基础设施之后,接下来需要设置异常返回时的现场以及调度现场信息,使得进程能够返回正确的位置执行:

sched_fork
->copy_thread

copy_thread这个函数对于进程调度来说至关重要,决定进程第一次被调度的时候执行哪个代码,决定fork调用的返回值。写到这里不得不提到两个相关重要的两个结构体:pt_regs和cpu_context,他俩都是处理器架构相关的结构。

pt_regs描述的发生异常的时候保存的现场信息,主要是一些通用寄存器,我们这里称为异常现场:

struct pt_regs {                                                           union {                                                            struct user_pt_regs user_regs;                             struct {                                                   u64 regs[31];      //通用寄存器                                u64 sp;                                            u64 pc;                                            u64 pstate;                                        };                                                         };                                                                 u64 orig_x0;                                                       ....
};                                                                         

当异常发生时,异常的现场(通用寄存器的内容,如发生异常时的x0-x30,sp, pc, pstate)会被压到内核栈,通过pt_regs结构来描述,而当异常处理结束的时候,会需要恢复现场,将这些保存的值恢复到通用寄存器中。

cpu_context描述的是进程调度的时候需要保存的进程上下文,我们这里成为调度现场:

arch/arm64/include/asm/processor.htsk->thread.cpu_contextstruct cpu_context {unsigned long x19;unsigned long x20;unsigned long x21;unsigned long x22;unsigned long x23;unsigned long x24;unsigned long x25;unsigned long x26;unsigned long x27;unsigned long x28;     unsigned long fp;unsigned long sp;unsigned long pc;};

当进程切换的时候,会将处理器的当前需要保存的寄存器保存到前一个进程的tsk的thread.cpu_context中,并将后一个即将要调度的进程的上下文从tsk的thread.cpu_context中恢复到相应的寄存器,就完成了处理器状态的切换(如前一个进程的pc和sp的位置被保存起来,后一个进程的pc和sp的位置恢复到相关寄存器);

介绍完了这俩结构体,就可以在这两个结构体上做手脚,但是我们需要明确的是:

1.pt_regs和cpu_context都是处理器架构相关的结构。

2.pt_regs是发生异常时(当然包括中断)保存的处理器现场,用于异常处理完后来恢复现场,就好像没有发生异常一样,它保存在进程内核栈中

3.cpu_context是发生进程切换时,保存当前进程的上下文,保存在当前进程的进程描述符中。

4.pt_regs表征发生异常时处理器现场,cpu_context发生调度时当前进程的处理器现场。

ok,下面就可以在fork中做一些手脚:首先先将p->thread.cpu_context清零,然后对于用户进程和内核线程有不同的处理:

 if (likely(!(p->flags & PF_KTHREAD))) {          //对于用户进程*childregs = *current_pt_regs();        //拷贝父进程的pt_regs                            childregs->regs[0] = 0;             //        regs[0] 为异常返回用户空间时恢复到x0的值(fork的返回值),这里设置为0   表明是子进程返回!!!!/*                                                                   ¦* Read the current TLS pointer from tpidr_el0 as it may be          ¦* out-of-sync with the saved value.                                 ¦*/                                                                  *task_user_tls(p) = read_sysreg(tpidr_el0);                          if (stack_start) {                                                   if (is_compat_thread(task_thread_info(p)))                   childregs->compat_sp = stack_start;                  else                                                         childregs->sp = stack_start;   //创建线程时设置用户栈起始地址                      }                                                                    /*                                                                   ¦* If a TLS pointer was passed to clone, use it for the new          ¦* thread.                                                           ¦*/                                                                  if (clone_flags & CLONE_SETTLS)                                      p->thread.uw.tp_value = tls;                                 } else {                                     //对于内核线程/*                                                                   ¦* A kthread has no context to ERET to, so ensure any buggy          ¦* ERET is treated as an illegal exception return.                   ¦*                                                                   ¦* When a user task is created from a kthread, childregs will        ¦* be initialized by start_thread() or start_compat_thread().        ¦*/                                                                  memset(childregs, 0, sizeof(struct pt_regs));     //清0pt_regs                   childregs->pstate = PSR_MODE_EL1h | PSR_IL_BIT;       //设置子进程的处理器状态为   PSR_MODE_EL1h ,异常等级为el1使用sp_el1             p->thread.cpu_context.x19 = stack_start;    //设置内核线程执行函数地址                          p->thread.cpu_context.x20 = stk_sz;         //设置传递给函数的参数                }                                                                            p->thread.cpu_context.pc = (unsigned long)ret_from_fork;         //进程第一次被切换后的pc    p->thread.cpu_context.sp = (unsigned long)childregs;              //进程第一次被切换后的sp

上面以及做了注释,需要说明的是:

  1. 我们没有看到当创建用户任务的时候,异常返回后处理器的状态,实际上不需要设置,因为我们是通过fork系统调用的方式陷入内核,发生svc异常的时候,处理器的状态已经保存好了,已经是el0(PSR_MODE_EL0t)。

  2. childregs->regs[0] = 0;的设置保证了,子进程被调度返回用户空间的时候,fork的返回值为0,这就是为何fork返回值为0表示是子进程的原因。

  3. 如果创建的是子进程,那么就直接和父进程写时复制方式共享用户栈,而栈不需要在进行设置,直接使用父进程的。

  4. 最后两句,来设置的是进程切换时,子进程的pc和sp,当子进程第一次被调度的时候,从ret_from_fork开始执行指令,栈指针指向childregs,即为设置后pt_regs。

3.3 子进程被唤醒

前面已经为子进程的调度做好了一些数据结构的准备,但是子进程并没有被调度执行,那么何时开始被唤醒呢?我们回退到kernel_clone中,copy_process做了一些资源的复制之后,开始唤醒子进程:

kernel_clone
->copy_process->wake_up_new_task-> p->state = TASK_RUNNING;    //设置进程状态为TASK_RUNNING;__set_task_cpu(p, select_task_rq(p, task_cpu(p), WF_FORK))  //为子进程选择空闲的cpuactivate_task(rq, p, ENQUEUE_NOCLOCK)  //子进程加入到cpu的运行队列check_preempt_curr(rq, p, WF_FORK) //检查是否可以抢占所在cpu的当前进程

这里面做了几步非常重要的操作:

  1. 设置进程状态为TASK_RUNNING。

  2. 通过__set_task_cpu为子进程选择空闲的cpu,有可能不是当前的cpu(进程创建的时候是做负载均衡最好的时机,这个时候进程在cpu的cache还没有数据)。

  3. activate_task来将进程加入到选择的cpu的运行队列,这里加入到选择cpu的红黑树。

  4. check_preempt_curr就会检查是否能够抢占所在cpu的当前进程,这是创建进程时发生抢占的一个时机。

wake_up_new_task执行完之后,子进程就已经在所选择的cpu的运行队列了,也已经是TASK_RUNNING状态,等待调度器在合适的调度时机选择他。

其实,在这里我们也能看的,唤醒的实质是:将进程的状态设置TASK_RUNNING(调度器只选择TASK_RUNNING的进程),加入到cpu的运行队列(根据调度类加入到cpu的不同的调度队列,这里只是一种形象的说法,实际上不一定是队列,如:cfs类进程加入到红黑树),然后做唤醒抢占检查。

3.3  子进程被选择调度

走到这里,子进程已经被放置到了cpu的运行队列,已经具备调度条件,万事具备只欠东风,这个东风就是在何时的时候调度器选择这个子进程,几次上下文切换,子进程处在了红黑树最左边的那个节点上(这是有可能的,由于进程运行过程中,虚拟运行时间单调递增,向红黑树右侧移动,子进程就会逐渐移动到红黑树最左边),假如在某一时刻,子进程所在的cpu的运行队列上一个进程被tick中断打断,然后走到scheduler_tick中执行如下路径:

scheduler_tick   //kernel/sched/core.c->task_tick_fair->entity_tick->if (cfs_rq->nr_running > 1)               check_preempt_tick(cfs_rq, curr); ->ideal_runtime = sched_slice(cfs_rq, curr);    //获得当前进程的真实运行时间se = __pick_first_entity(cfs_rq);              //获得红黑树最左边的那个调度实体delta = curr->vruntime - se->vruntime;         //计算当前进程的虚拟运行时间 和 红黑树最左边的那个调度实体的虚拟运行时机的差值if (delta < 0)       //   差值小于0说明  当前进程的  vruntime 更小更需要调度return;                                if (delta > ideal_runtime)    //当差值  大于   当前进程的真实运行时间         resched_curr(rq_of(cfs_rq));       //设置重新调度标志

假如子进程刚好满足delta > ideal_runtime的条件,然后当前进程就被设置了重新调度标志,当tick中断返回的时候,发生抢占时调度:

tick中断发生
->vectors   //arch/arm64/kernel/entry.S->el0_irq->irq_handler  //处理中断->...->scheduler_tick  ->b       ret_to_user   //中断返回用户空间->work_pending->do_notify_resume-> if (thread_flags & _TIF_NEED_RESCHED) {schedule()   //发生调度

schedule的代码就不在分析,大致说明一下:

schedule实现中会选择一个合适的进程来调度,对于cfs调度类,选择红黑树最左边的那个调度实体所对应的进程,当前场景也就是渴望调度的子进程,然后进行进程的上下文切换,包括地址空间切换到子进程(见上篇),处理器状态切换,这里就切换了cpu_context到相应的寄存器。

这时,子进程就欢快的运行了。

3.4 子进程开始执行

进程上下文切换之后,子进程于是就获得了cpu,开始执行,那么最重要的两步就是pc和sp,当然上面我们知道fork的时候已经做了设置:

于是cpu就开始从ret_from_fork下面开始取指令执行,所处的上下文为子进程:

/** This is how we return from a fork.*/
SYM_CODE_START(ret_from_fork)   //arch/arm64/kernel/entry.Sbl      schedule_tailcbz     x19, 1f                         // not a kernel threadmov     x0, x20   //赋值内核线程函数的参数blr     x19      //执行内核线程函数
1:      get_current_task tskb       ret_to_user     //返回用户空间
SYM_CODE_END(ret_from_fork)

ret_from_fork首先跳转到schedule_tail(会raw_spin_unlock_irq打开中断和自旋锁以及一些对前一个进程做回收等操作)中执行,然后对于内核线程直接调用之前设置的内核执行的函数,对于用户任务通过 ret_to_user 返回用户空间。

3.5 父子进程返回用户空间

上面我们知道,当子进程被调度执行的时候从ret_from_fork开始执行,sp指向子进程内核栈的pt_regs, 最终执行 ret_to_user 来返回用户空间:

 ret_to_user   //arch/arm64/kernel/entry.S-> kernel_exit 0->  msr     elr_el1, x21                    // set up the return datamsr     spsr_el1, x22ldp     x0, x1, [sp, #16 * 0]ldp     x2, x3, [sp, #16 * 1]ldp     x4, x5, [sp, #16 * 2]ldp     x6, x7, [sp, #16 * 3]ldp     x8, x9, [sp, #16 * 4]ldp     x10, x11, [sp, #16 * 5]ldp     x12, x13, [sp, #16 * 6]ldp     x14, x15, [sp, #16 * 7]ldp     x16, x17, [sp, #16 * 8]ldp     x18, x19, [sp, #16 * 9]ldp     x20, x21, [sp, #16 * 10]ldp     x22, x23, [sp, #16 * 11]ldp     x24, x25, [sp, #16 * 12]ldp     x26, x27, [sp, #16 * 13]ldp     x28, x29, [sp, #16 * 14]ldr     lr, [sp, #S_LR]add     sp, sp, #PT_REGS_SIZE           // restore sp...eret

可以看的,子进程将自己内核栈中的pt_regs恢复到相应的寄存器中,完成了异常的恢复,最终调用eret,从异常中返回,这个时候硬件自动将 elr_el1设置到pc, spsr_el1设置到pstate, sp使用了sp_el0。

这里需要说明一下,以便更好的理解:

  1. elr_el1的值是原来父进程复制过来的,还记得copy_thread中的*childregs = *current_pt_regs()吗?,由于我们原来是fork系统调用,所以这里是执行svc系统调用的下一条指令。

  2. spsr_el1 是之前fork系统调用时保存的处理器的状态,现在恢复这个状态,当然原来在el0,现在也是el0。

  3. sp 改变为了sp_el0,共享父进程的用户栈(对于创建子进程来说)。

  4. 子进程返回的时候,由于负载均衡,不一定和父进程在一个cpu上,所以父子进程可以并发执行。

  5. 父进程创建完子进程,并唤醒子进程后,也会沿着原来的svc调用路径一路返回到 ret_to_user  ,然后恢复上下文,和子进程经历同样的过程,也会svc系统调用的下一条指令,继续使用原来的用户栈指针,好像什么都没发生一起,但是他却孕育了新的进程在当前cpu或者其他cpu上活跃着。

  6. 父子进程返回用户空间后都会从fork返回,fork函数调用一次却返回两次,这是由于是两个不同的进程参与调度,而且他们写实复制方式共享相同的地址空间,对于共享的私有数据,如堆栈会通过写实复制方式为写者分配新的页并作拷贝和映射操作(见上篇)。

写到这里来总结一下,发生fork的时候进程管理做的事情:

首先是调用sched_fork为新创建的进程构建调度相关的基础组件,如设置优先级、调度类计算虚拟运行时间等属性信息,为参与最终的调度做准备,然后调用copy_thread来设置异常返回的上下文和调度上下文这是为调度子进程后处理器状态做准备,最后通过wake_up_new_task来唤醒子进程将它放置到合适cpu的运行队列,来等待合适的调度时机参与进程调度,来获得cpu资源。

下面给出精心绘制的创建子进程后调度相关的图示:

4. 总结

写到这里,Linux内核进程创建也就讲完了,当然fork的实现涉及到很多内容,这里只是从内存管理和进程调度的两个维度来看进程的创建过程,阅读完这两篇文章希望能帮助大家理解fork的时候背后隐藏的一些技术细节,真正理解到fork的时候创建的页表如何被使用的,进程又是如何参与到调度的,从fork系统调用到最后的返回用户空间整个过程有所了解,感谢阅读。

5T技术资源大放送!包括但不限于:C/C++,Arm, Linux,Android,人工智能,单片机,树莓派,等等。在公众号内回复「peter」,即可免费获取!!

 记得点击分享在看,给我充点儿电吧

通过fork来剖析Linux内核的内存管理和进程管理(下)相关推荐

  1. 通过fork来剖析Linux内核的内存管理和进程管理(上)

    1.开场白 本文主要从内存管理和进程管理两个维度来窥探一下fork背后隐藏的技术细节,希望能够通过本文让大家站在一个高度去看进程创建. 全文分为两部分讲解:fork的内存管理部分和进程管理部分,内存管 ...

  2. Linux内核源码分析《进程管理》

    Linux内核源码分析<进程管理> 前言 1. Linux 内核源码分析架构 2. 进程原理分析 2.1 进程基础知识 2.2 Linux进程四要素 2.3 进程描述符 task_stru ...

  3. Linux内核设计与实现:进程管理

    1. 关于进程 在linux操作系统中,进程通过fork()函数调用,通过复制一个现有的进程来创建一个新的进程: 调用fork()的叫做父进程,而创建的进程叫子进程,fork()调用从内核返回两次,一 ...

  4. fork的黑科技,它到底做了个啥,源码级分析linux内核的内存管理

    最近一直在学习linux内核源码,总结一下 https://github.com/xiaozhang8tuo/linux-kernel-0.11 一份带注释的源码,学习用. fork的黑科技,它到底做 ...

  5. linux 内核 课程,Linux内核分析课程-全面剖析Linux内核技术 揭开Linux内核的面纱 Linux内核学习视频教 ......

    课程名称 Linux内核分析课程-全面剖析Linux内核技术 揭开Linux内核的面纱 Linux内核学习视频 课程目录 (1)\1, 计算机是如何工作的?:目录中文件数:0个 (2)\2, 操作系统 ...

  6. 从底层原理出发,了解Linux内核之内存管理

    本文讲解更加底层,基本都是从Linux内核出发,会更深入.所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉. 1.页 内核把物理页作为内存管理的基本单元. 尽管处理器的 ...

  7. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

  8. 【Linux内核】内存映射原理

    [Linux内核]内存映射原理 物理地址空间 物理地址是处理器在总线上能看到的地址,使用RISC(Reduced Instruction Set Computing精简指令集)的处理器通常只实现一个物 ...

  9. 深度剖析Linux内核地址映射机制

    深度剖析Linux内核地址映射机制 1.虚拟空间数据结构   2.进程虚拟空间  3.内存映射 视频讲解如下,点击观看: Linux内核开发系列第7讲--深度剖析Linux内核地址映射机制 C/C++ ...

最新文章

  1. 高精地图:为自动驾驶汽车提供动力的新时代地图
  2. oracle 登录rman,Oracle 学习之RMAN(二)由此开始
  3. 法国拟将雷诺与日产合并 代表团抵日进行谈判
  4. halcon的算子清点: Chapter 5 滤波
  5. wifi的基础知识及原理1
  6. Android之drawlayout使用和总结
  7. # 生成单色位二维码图_如何2个小时内学会ps抠图-纯干货
  8. Redis学习---(11)Redis 有序集合(sorted set)
  9. Redis脚本插件之————执行Lua脚本示例
  10. C-Free注册码,密钥,到期解决办法
  11. linux下cmake安装配置
  12. GPS之家论坛最新地图下载精选 汇集论坛精华 不断更新中(2013.3.30)
  13. ATMEGA128——初探
  14. linux ftp prompt off,FTP命令详解及使用技巧
  15. 云计算与大数据之间的关系
  16. 「Golang」sync.Once用法以及源码讲解
  17. 《艺多不压身 艺多不养家》反思
  18. Tableau长期免费使用的方法总结(包括Tableau Public,Tableau Desktop,Tableau Pre,Tableau eLearning)
  19. License Code of Jigloo序列号、注册码,可能是通用的呵呵
  20. 期末复习----习题

热门文章

  1. C++的atof()
  2. KVM详细介绍及搭建KVM虚拟化平台构建Centos7系统
  3. Outlook邮箱满了不用删除清理,快速转移保存到本地
  4. ventory做U盘启动,使用vmware进行测试U盘系统盘是否制作成功
  5. pip安装包下载地址
  6. localhost和127.0.0.1有什么区别?
  7. 洗地机和扫地机器人哪个实用?值得入手的洗地机十大品牌推荐
  8. 谷歌开源漏洞跟踪工具 Monorail 存在跨站点搜索漏洞
  9. 软件测试以bug数来考核,软件测试能力提升及其思考
  10. 网线线序及网线转RS232—— DB9线序