从零到一手写操作系统(八、进程 2)linux 进程)
如果追忆会荡起涟漪,那么今天的秋红落叶和晴空万里都归你
https://aeneag.xyz
微信公众号:技术乱舞
艾恩凝
手写操作系统目录
8.2)Linux进程与调度
8.2.1)进程结构task_struct
1struct task_struct {
2 struct thread_info thread_info;//处理器特有数据
3 volatile long state; //进程状态
4 void *stack; //进程内核栈地址
5 refcount_t usage; //进程使用计数
6 int on_rq; //进程是否在运行队列上
7 int prio; //动态优先级
8 int static_prio; //静态优先级
9 int normal_prio; //取决于静态优先级和调度策略
10 unsigned int rt_priority; //实时优先级
11 const struct sched_class *sched_class;//指向其所在的调度类
12 struct sched_entity se;//普通进程的调度实体
13 struct sched_rt_entity rt;//实时进程的调度实体
14 struct sched_dl_entity dl;//采用EDF算法调度实时进程的调度实体
15 struct sched_info sched_info;//用于调度器统计进程的运行信息
16 struct list_head tasks;//所有进程的链表
17 struct mm_struct *mm; //指向进程内存结构
18 struct mm_struct *active_mm;
19 pid_t pid; //进程id
20 struct task_struct __rcu *parent;//指向其父进程
21 struct list_head children; //链表中的所有元素都是它的子进程
22 struct list_head sibling; //用于把当前进程插入到兄弟链表中
23 struct task_struct *group_leader;//指向其所在进程组的领头进程
24 u64 utime; //用于记录进程在用户态下所经过的节拍数
25 u64 stime; //用于记录进程在内核态下所经过的节拍数
26 u64 gtime; //用于记录作为虚拟机进程所经过的节拍数
27 unsigned long min_flt;//缺页统计
28 unsigned long maj_flt;
29 struct fs_struct *fs; //进程相关的文件系统信息
30 struct files_struct *files;//进程打开的所有文件
31 struct vm_struct *stack_vm_area;//内核栈的内存区
32 };
在内存中,一个 task_struct 结构体的实例变量代表一个 Linux 进程。
创建 task_struct 结构
首先来看一张图
图中说明的是早期的linux结构,Linux 早期是这样创建 task_struct 结构体的实例变量的:找伙伴内存管理系统,分配两个连续的页面(即 8KB),作为进程的内核栈,再把 task_struct 结构体的实例变量,放在这 8KB 内存空间的开始地址处。内核栈则是从上向下伸长的,task_struct 数据结构是从下向上伸长的。
从图中不难发现,Linux 把 task_struct 结构和内核栈放在了一起 ,所以我们只要把 RSP 寄存器的值读取出来,然后将其低 13 位清零,就得到了当前 task_struct 结构体的地址。由于内核栈比较大,而且会向下伸长,覆盖掉 task_struct 结构体内容的概率就很小。
进程的创建
1static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node)
2{
3 struct page *page = alloc_pages_node(node, THREADINFO_GFP,
4 THREAD_SIZE_ORDER);//分配两个页面
5 if (likely(page)) {
6 tsk->stack = kasan_reset_tag(page_address(page));
7 return tsk->stack;//让task_struct结构的stack字段指向page的地址
8 }
9 return NULL;
10}
11static inline struct task_struct *alloc_task_struct_node(int node)
12{
13 return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);//在task_struct_cachep内存对象中分配一个task_struct结构休对象
14}
15static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
16{
17 struct task_struct *tsk; unsigned long *stack;
18 tsk = alloc_task_struct_node(node);//分配task_struct结构体
19 if (!tsk)
20 return NULL;
21 stack = alloc_thread_stack_node(tsk, node);//分配内核栈
22 tsk->stack = stack;
23 return tsk;
24}
25static __latent_entropy struct task_struct *copy_process(
26 struct pid *pid, int trace, int node,
27 struct kernel_clone_args *args)
28{
29 int pidfd = -1, retval;
30 struct task_struct *p;
31 //……
32 retval = -ENOMEM;
33 p = dup_task_struct(current, node);//分配task_struct和内核栈
34 //……
35 return ERR_PTR(retval);
36}
37
38pid_t kernel_clone(struct kernel_clone_args *args)
39{
40 u64 clone_flags = args->flags;
41 struct task_struct *p;
42 pid_t nr;
43 //……
44 //复制进程
45 p = copy_process(NULL, trace, NUMA_NO_NODE, args);
46 //……
47 return nr;
48}
49//建立进程接口
50SYSCALL_DEFINE0(fork)
51{
52 struct kernel_clone_args args = {
53 .exit_signal = SIGCHLD,
54 };
55 return kernel_clone(&args);
56}
fork()函数就是建立一个与父进程相同的进程,也就是复制了父进程的一系列数据。代码展示了从 SLAB 中分配 task_struct 结构,以及从伙伴内存系统分配内核栈的过程。
8.2.2)进程地址空间
Linux 也是支持虚拟内存的操作系统内核,现在我们来看看 Linux 用于描述一个进程的地址空间的数据结构,它就是 mm_struct 结构。
1struct mm_struct {
2 struct vm_area_struct *mmap; //虚拟地址区间链表VMAs
3 struct rb_root mm_rb; //组织vm_area_struct结构的红黑树的根
4 unsigned long task_size; //进程虚拟地址空间大小
5 pgd_t * pgd; //指向MMU页表
6 atomic_t mm_users; //多个进程共享这个mm_struct
7 atomic_t mm_count; //mm_struct结构本身计数
8 atomic_long_t pgtables_bytes;//页表占用了多个页
9 int map_count; //多少个VMA
10 spinlock_t page_table_lock; //保护页表的自旋锁
11 struct list_head mmlist; //挂入mm_struct结构的链表
12 //进程应用程序代码开始、结束地址,应用程序数据的开始、结束地址
13 unsigned long start_code, end_code, start_data, end_data;
14 //进程应用程序堆区的开始、当前地址、栈开始地址
15 unsigned long start_brk, brk, start_stack;
16 //进程应用程序参数区开始、结束地址
17 unsigned long arg_start, arg_end, env_start, env_end;
18};
创建mm_struct 结构
1//在mm_cachep内存对象中分配一个mm_struct结构休对象
2#define allocate_mm() (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
3static struct mm_struct *dup_mm(struct task_struct *tsk,
4 struct mm_struct *oldmm)
5{
6 struct mm_struct *mm;
7 //分配mm_struct结构
8 mm = allocate_mm();
9 if (!mm)
10 goto fail_nomem;
11 //复制mm_struct结构
12 memcpy(mm, oldmm, sizeof(*mm));
13 //……
14 return mm;
15}
16static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
17{
18 struct mm_struct *mm, *oldmm;
19 int retval;
20 tsk->min_flt = tsk->maj_flt = 0;
21 tsk->nvcsw = tsk->nivcsw = 0;
22 retval = -ENOMEM;
23 mm = dup_mm(tsk, current->mm);//分配mm_struct结构的实例变量
24 if (!mm)
25 goto fail_nomem;
26good_mm:
27 tsk->mm = mm;
28 tsk->active_mm = mm;
29 return 0;
30fail_nomem:
31 return retval;
32}
8.2.3)进程文件表
Linux 系统中,可以说万物皆为文件,比如文件、设备文件、管道文件等。一个进程对一个文件进行读写操作之前,必须先打开文件,这个打开的文件就记录在进程的文件表中,它由 task_struct 结构中的 files 字段指向。这里指向的其实是个 files_struct 结构,代码如下所示。
1struct files_struct {
2 atomic_t count;//自动计数
3 struct fdtable __rcu *fdt;
4 struct fdtable fdtab;
5 spinlock_t file_lock; //自旋锁
6 unsigned int next_fd;//下一个文件句柄
7 unsigned long close_on_exec_init[1];//执行exec()时要关闭的文件句柄
8 unsigned long open_fds_init[1];
9 unsigned long full_fds_bits_init[1];
10 struct file __rcu * fd_array[NR_OPEN_DEFAULT];//默认情况下打开文件的指针数组
11};
文件表的创建
1static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
2{
3 struct files_struct *oldf, *newf;
4 int error = 0;
5 oldf = current->files;//获取当前进程的files_struct的指针
6 if (!oldf)
7 goto out;
8
9
10 if (clone_flags & CLONE_FILES) {
11 atomic_inc(&oldf->count);
12 goto out;
13 }
14 //分配新files_struct结构的实例变量,并复制当前的files_struct结构
15 newf = dup_fd(oldf, NR_OPEN_MAX, &error);
16 if (!newf)
17 goto out;
18
19
20 tsk->files = newf;//新进程的files_struct结构指针指向新的files_struct结构
21 error = 0;
22out:
23 return error;
8.2.4)进程调度
Linux 进程调度支持多种调度算法,有基于优先级的调度算法,有实时调度算法,有完全公平调度算法(CFQ)。
进程调度实体
它其实是 Linux 进程调度系统的一部分,被嵌入到了 Linux 进程数据结构中,与调度器进行关联,能间接地访问进程,这种高内聚低耦合的方式,保证了进程数据结构和调度数据结构相互独立,我们后面可以分别做改进、优化,这是一种高明的软件设计思想。
1struct sched_entity {
2 struct load_weight load;//表示当前调度实体的权重
3 struct rb_node run_node;//红黑树的数据节点
4 struct list_head group_node;// 链表节点,被链接到 percpu 的 rq->cfs_tasks
5 unsigned int on_rq; //当前调度实体是否在就绪队列上
6 u64 exec_start;//当前实体上次被调度执行的时间
7 u64 sum_exec_runtime;//当前实体总执行时间
8 u64 prev_sum_exec_runtime;//截止到上次统计,进程执行的时间
9 u64 vruntime;//当前实体的虚拟时间
10 u64 nr_migrations;//实体执行迁移的次数
11 struct sched_statistics statistics;//统计信息包含进程的睡眠统计、等待延迟统计、CPU迁移统计、唤醒统计等。
12#ifdef CONFIG_FAIR_GROUP_SCHED
13 int depth;// 表示当前实体处于调度组中的深度
14 struct sched_entity *parent;//指向父级调度实体
15 struct cfs_rq *cfs_rq;//当前调度实体属于的 cfs_rq.
16 struct cfs_rq *my_q;
17#endif
18#ifdef CONFIG_SMP
19 struct sched_avg avg ;// 记录当前实体对于CPU的负载
20#endif
21};
我们只要通过 sched_entity 结构变量的地址,减去它在 task_struct 结构中的偏移(由编译器自动计算),就能获取到 task_struct 结构的地址。这样就能达到通过 sched_entity 结构,访问 task_struct 结构的目的。
进程运行队列
结构的出现一定是为了方便管理
Linux 定义了一个进程运行队列结构,每个 CPU 分配一个这样的进程运行队列结构实例变量
1struct rq {
2 raw_spinlock_t lock;//自旋锁
3 unsigned int nr_running;//多个就绪运行进程
4 struct cfs_rq cfs; //作用于完全公平调度算法的运行队列
5 struct rt_rq rt;//作用于实时调度算法的运行队列
6 struct dl_rq dl;//作用于EDF调度算法的运行队列
7 struct task_struct __rcu *curr;//这个运行队列当前正在运行的进程
8 struct task_struct *idle;//这个运行队列的空转进程
9 struct task_struct *stop;//这个运行队列的停止进程
10 struct mm_struct *prev_mm;//这个运行队列上一次运行进程的mm_struct
11 unsigned int clock_update_flags;//时钟更新标志
12 u64 clock; //运行队列的时间
13 //后面的代码省略
14};
重点关注cfs_rq
1struct rb_root_cached {
2 struct rb_root rb_root; //红黑树的根
3 struct rb_node *rb_leftmost;//红黑树最左子节点
4};
5struct cfs_rq {
6 struct load_weight load;//cfs_rq上所有调度实体的负载总和
7 unsigned int nr_running;//cfs_rq上所有的调度实体不含调度组中的调度实体
8 unsigned int h_nr_running;//cfs_rq上所有的调度实体包含调度组中所有调度实体
9 u64 exec_clock;//当前 cfs_rq 上执行的时间
10 u64 min_vruntime;//最小虚拟运行时间
11 struct rb_root_cached tasks_timeline;//所有调度实体的根
12 struct sched_entity *curr;//当前调度实体
13 struct sched_entity *next;//下一个调度实体
14 struct sched_entity *last;//上次执行过的调度实体
15 //省略不关注的代码
16};
再多的文字也抵不过一张图
task_struct 结构中包含了 sched_entity 结构。sched_entity 结构是通过红黑树组织起来的,红黑树的根在 cfs_rq 结构中,cfs_rq 结构又被包含在 rq 结构,每个 CPU 对应一个 rq 结构。这样,我们就把所有运行的进程组织起来。
调度器类
简单说就是,linux中有多个调度器,为了支持不同的调度器,定义了调度器数据结构
1struct sched_class {
2 //向运行队列中添加一个进程,入队
3 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
4 //向运行队列中删除一个进程,出队
5 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
6 //检查当前进程是否可抢占
7 void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
8 //从运行队列中返回可以投入运行的一个进程
9 struct task_struct *(*pick_next_task)(struct rq *rq);
10} ;
这样就可以根据具体调度器申请不同的调度函数,上面类似面向对象过程。
8.2.5)CFS调度器
Linux 支持多种不同的进程调度器,比如 RT 调度器、Deadline 调度器、CFS 调度器以及 Idle 调度器。
CFS 调度器下不叫优先级,而是叫权重,权重表示进程的优先级,各个进程按权重的比例分配 CPU 时间。
举个例子,现在有 A、B 两个进程。进程 A 的权重是 1024,进程 B 的权重是 2048。那么进程 A 获得 CPU 的时间比例是 1024/(1024+2048) = 33.3%。进程 B 获得的 CPU 时间比例是 2048/(1024+2048)=66.7%。
因此,权重越大,分配的时间比例越大,就相当于进程的优先级越高。
有了权重之后,分配给进程的时间计算公式如下:
进程的时间 = CPU 总时间 * 进程的权重 / 就绪队列所有进程权重之和
但是进程对外的编程接口中使用的是一个 nice 值,大小范围是(-20~19),数值越小优先级越大,意味着权重值越大,nice 值和权重之间可以转换的。Linux 提供了后面这个数组,用于转换 nice 值和权重。
1const int sched_prio_to_weight[40] = {
2 /* -20 */ 88761, 71755, 56483, 46273, 36291,
3 /* -15 */ 29154, 23254, 18705, 14949, 11916,
4 /* -10 */ 9548, 7620, 6100, 4904, 3906,
5 /* -5 */ 3121, 2501, 1991, 1586, 1277,
6 /* 0 */ 1024, 820, 655, 526, 423,
7 /* 5 */ 335, 272, 215, 172, 137,
8 /* 10 */ 110, 87, 70, 56, 45,
9 /* 15 */ 36, 29, 23, 18, 15,
10};
一个进程每降低一个 nice 值,就能多获得 10% 的 CPU 时间。1024 权重对应 nice 值为 0,被称为 NICE_0_LOAD。默认情况下,大多数进程的权重都是 NICE_0_LOAD。
进程调度延迟
什么是调度延迟?其实就是保证每一个可运行的进程,都至少运行一次的时间间隔。
我们结合实例理解,系统中有 3 个可运行进程,每个进程都运行 10ms,那么调度延迟就是 30ms;如果有 10 个进程,那么调度延迟就是 100ms;如果现在保证调度延迟不变,固定是 30ms;如果系统中有 3 个进程,则每个进程可运行 10ms;如果有 10 个进程,则每个进程可运行 3ms。
随着进程的增加,每个进程分配的时间在减少,进程调度次数会增加,调度器占用的时间就会增加。因此,CFS 调度器的调度延迟时间的设定并不是固定的。
当运行进程少于 8 个的时候,调度延迟是固定的 6ms 不变。当运行进程个数超过 8 个时,就要保证每个进程至少运行一段时间,才被调度。这个“至少一段时间”叫作最小调度粒度时间。
虚拟时间
例如,调度延迟是 10ms,系统一共 2 个相同优先级的进程,那么各进程都将在 10ms 的时间内各运行 5ms。
现在进程 A 和进程 B 他们的权重分别是 1024 和 820(nice 值分别是 0 和 1)。进程 A 获得的运行时间是 10x1024/(1024+820)=5.6ms,进程 B 获得的执行时间是 10x820/(1024+820)=4.4ms。进程 A 的 cpu 使用比例是 5.6/10x100%=56%,进程 B 的 cpu 使用比例是 4.4/10x100%=44%。
这两个进程的实际执行时间是不等的,但 CFS 调度器想保证每个进程的运行时间相等。因此 CFS 调度器引入了虚拟时间,也就是说,上面的 5.6ms 和 4.4ms 经过一个公式,转换成相同的值,这个转换后的值就叫虚拟时间。这样的话,CFS 只需要保证每个进程运行的虚拟时间是相等的。
虚拟时间 vruntime 和实际时间(wtime)转换公式如下:vruntime = wtime( NICE_0_LOAD/weight)*
根据上面的公式,可以发现 nice 值为 0 的进程,这种进程的虚拟时间和实际时间是相等的,那么进程 A 的虚拟时间为:5.6 *(1024/1024)=5.6,进程 B 的虚拟时间为:4.4 *(1024/820)=5.6。虽然进程 A 和进程 B 的权重不一样,但是计算得到的虚拟时间是一样的。
在运行队列中用红黑树结构组织进程的调度实体,这里进程虚拟时间正是红黑树的 key,这样进程就以进程的虚拟时间被红黑树组织起来了。红黑树的最左子节点,就是虚拟时间最小的进程,随着时间的推移进程会从红黑树的左边跑到右,然后从右边跑到左边,就像舞蹈一样优美。
8.2.6)CFS调度进程
CFS 调度器就是要维持各个可运行进程的虚拟时间相等,不相等就需要被调度运行。如果一个进程比其它进程的虚拟时间小,它就应该运行达到和其它进程的虚拟时间持平,直到它的虚拟时间超过其它进程,这时就要停下来,这样其它进程才能被调度运行。
定时周期调度
虚拟时间就是一个数据,如果没有任何机制对它进行更新,就会导致一个进程永远运行下去,因为那个进程的虚拟时间没有更新,虚拟时间永远最小,这当然不行
因此定时周期调度机制应运而生。Linux 启动会启动定时器,这个定时器每 1/1000、1/250、1/100 秒(根据配置不同选取其一),产生一个时钟中断,在中断处理函数中最终会调用一个 scheduler_tick 函数
调度器入口
1static void __sched notrace __schedule(bool preempt)
2{
3 struct task_struct *prev, *next;
4 unsigned long *switch_count;
5 unsigned long prev_state;
6 struct rq_flags rf;
7 struct rq *rq;
8 int cpu;
9 cpu = smp_processor_id();
10 rq = cpu_rq(cpu);//获取当前CPU的运行队列
11 prev = rq->curr; //获取当前进程
12 rq_lock(rq, &rf);//运行队列加锁
13 update_rq_clock(rq);//更新运行队列时钟
14 switch_count = &prev->nivcsw;
15 next = pick_next_task(rq, prev, &rf);//获取下一个投入运行的进程
16 clear_tsk_need_resched(prev); //清除抢占标志
17 clear_preempt_need_resched();
18 if (likely(prev != next)) {//当前运行进程和下一个运行进程不同,就要进程切换
19 rq->nr_switches++; //切换计数统计
20 ++*switch_count;
21 rq = context_switch(rq, prev, next, &rf);//进程机器上下文切换
22 } else {
23 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
24 rq_unlock_irq(rq, &rf);//解锁运行队列
25 }
26}
27void schedule(void)
28{
29 struct task_struct *tsk = current;//获取当前进程
30 do {
31 preempt_disable();//关闭内核抢占
32 __schedule(false);//进程调用
33 sched_preempt_enable_no_resched();//开启内核抢占
34 } while (need_resched());//是否需要再次重新调用
35}
挑选下一个进程
下一个进程,流程为:首先按照优先级调用具体调度器类的函数,CFS调用pick_next_task_fair函数,然后该函数调用pick_next_entity 函数选择虚拟时间最小的调度实体,然后调用 set_next_entity 函数,对选择的调度实体进行一些必要的处理
pick_next_entity函数做的就是它调用了相关函数,从运行队列上的红黑树中查找虚拟时间最少的调度实体,然后处理要跳过调度的情况,最后决定挑选的调度实体是否可以抢占并返回它。
挑选完成后,调用 context_switch 函数,完成两个进程的地址空间和机器上下文的切换,一次进程调度工作结束。
pick_next_task
1static inline struct task_struct *pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
2{
3 const struct sched_class *class;
4 struct task_struct *p;
5 //这是对CFS的一种优化处理,因为大部分进程属于CFS管理
6 if (likely(prev->sched_class <= &fair_sched_class &&
7 rq->nr_running == rq->cfs.h_nr_running)) {
8 p = pick_next_task_fair(rq, prev, rf);//调用CFS的对应的函数
9 if (unlikely(p == RETRY_TASK))
10 goto restart;
11 if (!p) {//如果没有获取到运行进程
12 put_prev_task(rq, prev);//将上一个进程放回运行队列中
13 p = pick_next_task_idle(rq);//获取空转进程
14 }
15 return p;
16 }
17restart:
18 for_each_class(class) {//依次从最高优先级的调度类开始遍历
19 p = class->pick_next_task(rq);
20 if (p)//如果在一个调度类所管理的运行队列中挑选到一个进程,立即返回
21 return p;
22 }
23 BUG();//出错
24}
pick_next_task_fair
1struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
2{
3 struct cfs_rq *cfs_rq = &rq->cfs;
4 struct sched_entity *se;
5 struct task_struct *p;
6 if (prev)
7 put_prev_task(rq, prev);//把上一个进程放回运行队列
8 do {
9 se = pick_next_entity(cfs_rq, NULL);//选择最适合运行的调度实体
10 set_next_entity(cfs_rq, se);//对选择的调度实体进行一些处理
11 cfs_rq = group_cfs_rq(se);
12 } while (cfs_rq);//在没有调度组的情况下,循环一次就结束了
13 p = task_of(se);//通过se获取包含se的进程task_struct
14 return p;
15}
pick_next_entity
1struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
2{
3 struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);//先读取在tasks_timeline中rb_node指针
4 if (!left)
5 return NULL;//如果为空直接返回NULL
6 //通过红黑树结点指针取得包含它的调度实体结构地址
7 return rb_entry(left, struct sched_entity, run_node);
8}
9static struct sched_entity *__pick_next_entity(struct sched_entity *se)
10{ //获取当前红黑树节点的下一个结点
11 struct rb_node *next = rb_next(&se->run_node);
12 if (!next)
13 return NULL;//如果为空直接返回NULL
14 return rb_entry(next, struct sched_entity, run_node);
15}
16static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
17{
18 //获取Cfs_rq中的红黑树上最左节点上调度实体,虚拟时间最小
19 struct sched_entity *left = __pick_first_entity(cfs_rq);
20 struct sched_entity *se;
21 if (!left || (curr && entity_before(curr, left)))
22 left = curr;//可能当前进程主动放弃CPU,它的虚拟时间比红黑树上的还小,所以left指向当前进程调度实体
23 se = left;
24 if (cfs_rq->skip == se) { //如果选择的调度实体是要跳过的调度实体
25 struct sched_entity *second;
26 if (se == curr) {//如果是当前调度实体
27 second = __pick_first_entity(cfs_rq);//选择运行队列中虚拟时间最小的调度实体
28 } else {//否则选择红黑树上第二左的进程节点
29 second = __pick_next_entity(se);
30 //如果次优的调度实体的虚拟时间,还是比当前的调度实体的虚拟时间大
31 if (!second || (curr && entity_before(curr, second)))
32 second = curr;//让次优的调度实体也指向当前调度实体
33 }
34 //判断left和second的虚拟时间的差距是否小于sysctl_sched_wakeup_granularity
35 if (second && wakeup_preempt_entity(second, left) < 1)
36 se = second;
37 }
38 if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) {
39 se = cfs_rq->next;
40 } else if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) {
41 se = cfs_rq->last;
42 }
43 clear_buddies(cfs_rq, se);//需要清除掉last、next、skip指针
44 return se;
45}
8.3)流程图
注:进程于2022年5月6日结束
手写操作系统目录
则移山填海之难,
终有成功之日!
——孙文