linux调度器源码分析 - 新进程加入(三)

简介: 本文为原创,转载请注明:http://blog.chinaunix.net/uid/26772321.html  引言   之前的文章已经介绍了调度器已经初始化完成,现在只需要加入一个周期定时器tick驱动它进行周期调度即可,而加入定时器tick在下一篇文章进行简单说明(主要这部分涉及调度器比较少,更多的是时钟、定时器相关知识)。
本文为原创,转载请注明: http://blog.chinaunix.net/uid/26772321.html  

引言

  之前的文章已经介绍了调度器已经初始化完成,现在只需要加入一个周期定时器tick驱动它进行周期调度即可,而加入定时器tick在下一篇文章进行简单说明(主要这部分涉及调度器比较少,更多的是时钟、定时器相关知识)。这篇文章主要说明系统如何把一个进程加入到队列中。

 

加入时机

  之前的文章也有提到过,只有处于TASK_RUNNING状态下的进程才能够加入到调度器,其他状态都不行,也就说明了,当一个进程处于睡眠、挂起状态的时候是不存在于调度器中的,而进程加入调度器的时机如下:

  • 当进程创建完成时,进程刚创建完成时,即使它运行起来立即调用sleep()进程睡眠,它也必定先会加入到调度器,因为实际上它加入调度器后自己还需要进行一定的初始化和操作,才会调用到我们的“立即”sleep()。
  • 当进程被唤醒时,也使用sleep的例子说明,我们平常写程序使用的sleep()函数实现原理就是通过系统调用将进程状态改为TASK_INTERRUPTIBLE,然后移出运行队列,并且启动一个定时器,在定时器到期后唤醒进程,再重新放入运行队列。

sched_fork

  在我的博文关于linux系统如何实现fork的研究(二)中专门描述了copy_process()这个创建函数,而里面有一个函数专门用于进程调度的初始化,就是sched_fork(),其代码如下

  1. int sched_fork(unsigned long clone_flags, struct task_struct *p)
  2. {
  3.     unsigned long flags;
  4.     /* 获取当前CPU,并且禁止抢占 */
  5.     int cpu = get_cpu();
  6.     
  7.     /* 初始化跟调度相关的值,比如调度实体,运行时间等 */
  8.     __sched_fork(clone_flags, p);
  9.     /*
  10.      * 标记为运行状态,表明此进程正在运行或准备好运行,实际上没有真正在CPU上运行,这里只是导致了外部信号和事件不能够唤醒此进程,之后将它插入到运行队列中
  11.      */
  12.     p->state = TASK_RUNNING;

  13.     /*
  14.      * 根据父进程的运行优先级设置设置进程的优先级
  15.      */
  16.     p->prio = current->normal_prio;

  17.     /*
  18.      * 更新该进程优先级
  19.      */
  20.     /* 如果需要重新设置优先级 */
  21.     if (unlikely(p->sched_reset_on_fork)) {
  22.         /* 如果是dl调度或者实时调度 */
  23.         if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
  24.             /* 调度策略为SCHED_NORMAL,这个选项将使用CFS调度 */
  25.             p->policy = SCHED_NORMAL;
  26.             /* 根据默认nice值设置静态优先级 */
  27.             p->static_prio = NICE_TO_PRIO(0);
  28.             /* 实时优先级为0 */
  29.             p->rt_priority = 0;
  30.         } else if (PRIO_TO_NICE(p->static_prio) 0)
  31.             /* 根据默认nice值设置静态优先级 */
  32.             p->static_prio = NICE_TO_PRIO(0);

  33.         /* p->prio = p->normal_prio = p->static_prio */
  34.         p->prio = p->normal_prio = __normal_prio(p);
  35.         /* 设置进程权重 */
  36.         set_load_weight(p);

  37.          /* sched_reset_on_fork成员在之后已经不需要使用了,直接设为0 */
  38.         p->sched_reset_on_fork = 0;
  39.     }

  40.     if (dl_prio(p->prio)) {
  41.         /* 使能抢占 */
  42.         put_cpu();
  43.         /* 返回错误 */
  44.         return -EAGAIN;
  45.     } else if (rt_prio(p->prio)) {
  46.         /* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
  47.         p->sched_class = &rt_sched_class;
  48.     } else {
  49.         /* 如果是普通进程,设置其调度类为fair_sched_class */
  50.         p->sched_class = &fair_sched_class;
  51.     }
  52.     /* 调用调用类的task_fork函数 */
  53.     if (p->sched_class->task_fork)
  54.         p->sched_class->task_fork(p);

  55.     /*
  56.      * The child is not yet in the pid-hash so no cgroup attach races,
  57.      * and the cgroup is pinned to this child due to cgroup_fork()
  58.      * is ran before sched_fork().
  59.      *
  60.      * Silence PROVE_RCU.
  61.      */
  62.     raw_spin_lock_irqsave(&p->pi_lock, flags);
  63.     /* 设置新进程的CPU为当前CPU */
  64.     set_task_cpu(p, cpu);
  65.     raw_spin_unlock_irqrestore(&p->pi_lock, flags);

  66. #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
  67.     if (likely(sched_info_on()))
  68.         memset(&p->sched_info, 0, sizeof(p->sched_info));
  69. #endif
  70. #if defined(CONFIG_SMP)
  71.     p->on_cpu = 0;
  72. #endif
  73.     /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
  74.     /* 初始化该进程为内核禁止抢占 */
  75.     init_task_preempt_count(p);
  76. #ifdef CONFIG_SMP
  77.     plist_node_init(&p->pushable_tasks, MAX_PRIO);
  78.     RB_CLEAR_NODE(&p->pushable_dl_tasks);
  79. #endif
  80.     /* 使能抢占 */
  81.     put_cpu();
  82.     return 0;
  83. }

   在sched_fork()函数中,主要工作如下:

  • 获取当前CPU号
  • 禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)
  • 初始化进程p的一些变量(实时进程和普通进程通用的那些变量)
  • 设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)
  • 根据父进程和clone_flags参数设置进程p的优先级和权重。
  • 根据进程p的优先级设置其调度类(实时进程优先级:0~99  普通进程优先级:100~139)
  • 根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)
  • 设置进程p的当前CPU为此CPU。
  • 初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)
  • 使能内核抢占

  可以看出sched_fork()进行的初始化也比较简单,需要注意的是不同类型的进程会使用不同的调度类,并且也会调用调度类中的初始化函数。在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数,我们具体看看实现:

  1. static void task_fork_fair(struct task_struct *p)
  2. {
  3.     struct cfs_rq *cfs_rq;
  4.     
  5.     /* 进程p的调度实体se */
  6.     struct sched_entity *se = &p->se, *curr;
  7.     
  8.     /* 获取当前CPU */
  9.     int this_cpu = smp_processor_id();
  10.     
  11.     /* 获取此CPU的运行队列 */
  12.     struct rq *rq = this_rq();
  13.     unsigned long flags;
  14.     
  15.     /* 上锁并保存中断记录 */
  16.     raw_spin_lock_irqsave(&rq->lock, flags);
  17.     
  18.     /* 更新rq运行时间 */
  19.     update_rq_clock(rq);
  20.     
  21.     /* cfs_rq = current->se.cfs_rq; */
  22.     cfs_rq = task_cfs_rq(current);
  23.     
  24.     /* 设置当前进程所在队列为父进程所在队列 */
  25.     curr = cfs_rq->curr;

  26.     /*
  27.      * Not only the cpu but also the task_group of the parent might have
  28.      * been changed after parent->se.parent,cfs_rq were copied to
  29.      * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
  30.      * of child point to valid ones.
  31.      */
  32.     rcu_read_lock();
  33.     /* 设置此进程所属CPU */
  34.     __set_task_cpu(p, this_cpu);
  35.     rcu_read_unlock();

  36.     /* 更新当前进程运行时间 */
  37.     update_curr(cfs_rq);

  38.     if (curr)
  39.         /* 将父进程的虚拟运行时间赋给了新进程的虚拟运行时间 */
  40.         se->vruntime = curr->vruntime;
  41.     /* 调整了se的虚拟运行时间 */
  42.     place_entity(cfs_rq, se, 1);

  43.     if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
  44.         /*
  45.          * Upon rescheduling, sched_class::put_prev_task() will place
  46.          * 'current' within the tree based on its new key value.
  47.          */
  48.         swap(curr->vruntime, se->vruntime);
  49.         resched_curr(rq);
  50.     }

  51.     /* 保证了进程p的vruntime是运行队列中最小的(这里占时不确定是不是这个用法,不过确实是最小的了) */
  52.     se->vruntime -= cfs_rq->min_vruntime;
  53.     
  54.     /* 解锁,还原中断记录 */
  55.     raw_spin_unlock_irqrestore(&rq->lock, flags);
  56. }

  在task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列,值得我们注意的是 cfs_rq = task_cfs_rq(current); 这一行,在注释中已经表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的并不是根cfs队列,而是所处的cfs_rq,也就是如果父进程处于一个进程组的cfs_rq中,新创建的进程也会处于这个进程组的cfs_rq中。

 

wake_up_new_task()

  到这里新进程关于调度的初始化已经完成,但是还没有被调度器加入到队列中,其是在do_fork()中的wake_up_new_task(p);中加入到队列中的,我们具体看看wake_up_new_task()的实现:


  1. void wake_up_new_task(struct task_struct *p)
  2. {
  3.     unsigned long flags;
  4.     struct rq *rq;

  5.     raw_spin_lock_irqsave(&p->pi_lock, flags);
  6. #ifdef CONFIG_SMP
  7.     /*
  8.      * Fork balancing, do it here and not earlier because:
  9.      * - cpus_allowed can change in the fork path
  10.      * - any previously selected cpu might disappear through hotplug
  11.      */
  12.      /* 为进程选择一个合适的CPU */
  13.     set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
  14. #endif

  15.     /* Initialize new task's runnable average */
  16.     /* 这里是跟多核负载均衡有关 */
  17.     init_task_runnable_average(p);
  18.     /* 上锁 */
  19.     rq = __task_rq_lock(p);
  20.     /* 将进程加入到CPU的运行队列 */
  21.     activate_task(rq, p, 0);
  22.     /* 标记进程p处于队列中 */
  23.     p->on_rq = TASK_ON_RQ_QUEUED;
  24.     /* 跟调试有关 */
  25.     trace_sched_wakeup_new(p, true);
  26.     /* 检查是否需要切换当前进程 */
  27.     check_preempt_curr(rq, p, WF_FORK);
  28. #ifdef CONFIG_SMP
  29.     if (p->sched_class->task_woken)
  30.         p->sched_class->task_woken(rq, p);
  31. #endif
  32.     task_rq_unlock(rq, p, &flags);
  33. }
  在wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task(),而activate_task()函数最后会调用到新进程调度类中的enqueue_task指针所指函数,这里我们具体看一下cfs调度类的enqueue_task指针所指函数enqueue_task_fair():
  1. static void
  2. enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
  3. {
  4.     struct cfs_rq *cfs_rq;
  5.     struct sched_entity *se = &p->se;

  6.     /* 这里是一个迭代,我们知道,进程有可能是处于一个进程组中的,所以当这个处于进程组中的进程加入到该进程组的队列中时,要对此队列向上迭代 */
  7.     for_each_sched_entity(se) {
  8.         if (se->on_rq)
  9.             break;
  10.         /* 如果不是CONFIG_FAIR_GROUP_SCHED,获取其所在CPU的rq运行队列的cfs_rq运行队列
  11.          * 如果是CONFIG_FAIR_GROUP_SCHED,获取其所在的cfs_rq运行队列
  12.          */
  13.         cfs_rq = cfs_rq_of(se);
  14.         /* 加入到队列中 */
  15.         enqueue_entity(cfs_rq, se, flags);

  16.         /*
  17.          * end evaluation on encountering a throttled cfs_rq
  18.          *
  19.          * note: in the case of encountering a throttled cfs_rq we will
  20.          * post the final h_nr_running increment below.
  21.         */
  22.         if (cfs_rq_throttled(cfs_rq))
  23.             break;
  24.         cfs_rq->h_nr_running++;

  25.         flags = ENQUEUE_WAKEUP;
  26.     }

  27.     /* 只有se不处于队列中或者cfs_rq_throttled(cfs_rq)返回真才会运行这个循环 */
  28.     for_each_sched_entity(se) {
  29.         cfs_rq = cfs_rq_of(se);
  30.         cfs_rq->h_nr_running++;

  31.         if (cfs_rq_throttled(cfs_rq))
  32.             break;

  33.         update_cfs_shares(cfs_rq);
  34.         update_entity_load_avg(se, 1);
  35.     }

  36.     if (!se) {
  37.         update_rq_runnable_avg(rq, rq->nr_running);
  38.         /* 当前CPU运行队列活动进程数 + 1 */
  39.         add_nr_running(rq, 1);
  40.     }
  41.     /* 设置下次调度中断发生时间 */
  42.     hrtick_update(rq);
  43. }
  在enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作,如下:
  1. static void
  2. enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
  3. {
  4.     /*
  5.      * Update the normalized vruntime before updating min_vruntime
  6.      * through calling update_curr().
  7.      */
  8.     if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
  9.         se->vruntime += cfs_rq->min_vruntime;

  10.     /*
  11.      * Update run-time statistics of the 'current'.
  12.      */
  13.     /* 更新当前进程运行时间和虚拟运行时间 */
  14.     update_curr(cfs_rq);
  15.     enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
  16.     /* 更新cfs_rq队列总权重(就是在原有基础上加上se的权重) */
  17.     account_entity_enqueue(cfs_rq, se);
  18.     update_cfs_shares(cfs_rq);

  19.     /* 新建的进程flags为0,不会执行这里 */
  20.     if (flags & ENQUEUE_WAKEUP) {
  21.         place_entity(cfs_rq, se, 0);
  22.         enqueue_sleeper(cfs_rq, se);
  23.     }

  24.     update_stats_enqueue(cfs_rq, se);
  25.     check_spread(cfs_rq, se);
  26.     
  27.     /* 将se插入到运行队列cfs_rq的红黑树中 */
  28.     if (se != cfs_rq->curr)
  29.         __enqueue_entity(cfs_rq, se);
  30.     /* 将se的on_rq标记为1 */
  31.     se->on_rq = 1;

  32.     /* 如果cfs_rq的队列中只有一个进程,这里做处理 */
  33.     if (cfs_rq->nr_running == 1) {
  34.         list_add_leaf_cfs_rq(cfs_rq);
  35.         check_enqueue_throttle(cfs_rq);
  36.     }
  37. }

 

总结

  需要注意的几点:

  • 新创建的进程先会进行调度相关的结构体和变量初始化,其中会根据不同的类型进行不同的调度类操作,此时并没有加入到队列中。
  • 当新进程创建完毕后,它的父进程会将其运行状态置为TASK_RUNNING,并加入到运行队列中。
  • 加入运行队列时系统会根据CPU的负载情况放入不同的CPU队列中。






目录
相关文章
|
29天前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
29 0
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
595 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
64 2
|
4天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
26 4
linux进程管理万字详解!!!
|
4天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
24 4
|
5天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
6天前
|
消息中间件 存储 Linux
|
13天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
12 1
|
24天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
18 1
|
29天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
26 0
Linux c/c++之IPC进程间通信