热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

x86linux的进程调度,x86体系结构下Linux2.6.26的进程调度和切换

进程调度相关数据结构task_structtask_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的

进程调度相关数据结构

task_struct

task_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的变量thread,记录了CPU相关的进程状态信息,如内核控制的断点和栈指针等。

在内核中获得当前进程task_struct结构使用宏current,该宏读取变量current_task得到指针。

thread_union和thread_info

thread_union用于表示一个进程的内核态堆栈,当进程进入内核态时就会使用该进程对应的内核态堆栈。

thread_union是一个联合体,由stack和thread_info两项组成。其中的stack用于直接访问内核态堆栈的各项,而thread_info表示了堆栈顶部(低地址部分)用于特殊用途的部分。

thread_info结构定义在include/asm-x86/thread_info_32.h,定义了本进程task_struct结构的指针等在进入内核初期马上要访问的辅助数据。

在内核中得到当前thread_info用current_thread_info函数得到,它通过对当前栈指针进行计算得到thread_info的指针。

sched_class

sched_class是Linux2.6中调度算法对外的统一界面。Linux使用这个概念将进程调度的具体策略和进程切换的过程隔离开,使得组织有序且可以实现对不同类进程采用不同的调度策略。

在Linux-2.6.26中sched_class有fair_sched_class,rt_sched_class和idle_sched_class三个实例,分别组织在kernel下的sched_fair.c、sched_rt.c、sched_idletask.c中。这三个实例在初始化时被串成了一个链表,依次为:rt,fair,idle。

sched_entity和sched_rt_entity

内核中对sched_entity的解释为“CFS stats for a schedulable entity (task, task-group etc)”,sched_rt_entity可类似解释。从这里可以看出这个结构的作用是存储一些调度算法相关的进程的状态。

rq

rq是当前CPU上就绪进程所组织成的队列。这个结构体记录了每个队列的状态。rq结构体中有cfs_rq和rt_rq两个子结构,分别描述了该CPU上fair类型和rt类型进程的信息。

schedule函数分析

schedule函数是进程调度的入口,在kernel/sched.c中。除去繁琐的检查、统计、上锁等操作,仔细观察,其主流程是4187行到4205行:

prev->sched_class->put_prev_task(rq, prev);

next = pick_next_task(rq, prev);

if (likely(prev != next)) {

sched_info_switch(prev, next);

rq->nr_switches++;

rq->curr = next;

++*switch_count;

context_switch(rq, prev, next); /*unlocks the rq*/

/** the context switch might have flipped the stack from under

* us, hence refresh the local variables.*/

cpu = smp_processor_id();

rq = cpu_rq(cpu);

} else

spin_unlock_irq(&rq->lock);

第一句中的prev在之前被赋值为rq->curr,因此是当前运行队列正在运行的进程。从字面看是将当前进程放回队列。第二句是从队列中取出下一个可运行的进程,叫next。

接下来是进程的上下文切换工作。首先判断prev和next是否是同一个进程,若是,则不必切换。否则统计信息,接着设置rq->curr为next,然后调用context_switch来进行实际的上下文切换。

schedule函数的简要分析结束。可见,理解进程的调度,核心是put_prev_task和pick_next_task;而理解进程的切换,核心是context_switch。下面就分两条线索,分别说明进程的切换和调度的流程。

进程的切换

接上,先忽略调度策略,假设prev和next指向需要换走和换入的进程,然后进入了context_switch函数。

context_switch在kernel/sched.c中的第2447行。

忽略prepare、finish、lazy等字样的函数,忽略SMP同步操作,主流程上只有switch_mm和switch_to。

switch_mm

switch_mm在x86体系结构下在include/asm-x86/mmu_context_32.h中第35行定义,其中核心语句只有一句:

load_cr3(next->pgd); 从名字和内容可见switch_mm是切换页表。这里切换页表,即地址空间,不会引起控制流程改变,因为每个进程地址空间的内核部分(3G以上)的映射是一样的。然而,当从内核空间返回用户空间时,这里的改变就会起到本质的作用:跑到next的用户代码中继续运行。

switch_to

switch_to是一个宏,定义在include/asm-x86/system.h中第30行,如下:

#define switch_to(prev, next, last) \

do { \

/*\

* Context-switching clobbers all registers, so we clobber \

* them explicitly, via unused output variables. \

* (EAX and EBP is not listed because EBP is saved/restored \

* explicitly for wchan access and EAX is the return value of \

* __switch_to()) \*/ \

unsigned long ebx, ecx, edx, esi, edi; \

\

asm volatile("pushfl\n\t" /*save flags*/ \

"pushl %%ebp\n\t" /*save EBP*/ \

"movl %%esp,%[prev_sp]\n\t" /*save ESP*/ \

"movl %[next_sp],%%esp\n\t" /*restore ESP*/ \

"movl $1f,%[prev_ip]\n\t" /*save EIP*/ \

"pushl %[next_ip]\n\t" /*restore EIP*/ \

"jmp __switch_to\n" /*regparm call*/ \

"1:\t" \

"popl %%ebp\n\t" /*restore EBP*/ \

"popfl\n" /*restore flags*/ \

\

/*output parameters*/ \

: [prev_sp] "=m" (prev->thread.sp), \

[prev_ip] "=m" (prev->thread.ip), \

"=a" (last), \

\

/*clobbered output registers:*/ \

"=b" (ebx), "=c" (ecx), "=d" (edx), \

"=S" (esi), "=D" (edi) \

\

/*input parameters:*/ \

: [next_sp] "m" (next->thread.sp), \

[next_ip] "m" (next->thread.ip), \

\

/*regparm parameters for __switch_to():*/ \

[prev] "a" (prev), \

[next] "d" (next)); \

} while (0)

这个宏通过切换堆栈和控制流程来达到上下文切换的目的。=

切换内核堆栈

这个宏首先在保存了ebx,ecx,edx,esi,edi,flags在当前内核堆栈上,然后进行关键的堆栈切换:

pushl %ebp

movl %esp, %[prev_sp]

mvol %[prev_sp], %esp 首先将栈帧ebp压栈,然后从prev->thread.sp中取出上个进程在上一次切换时的栈顶,放到esp中。此句过后,内核态的堆栈已经切换到了下一个进程的内核堆栈。由前面介绍的current_thread_info的特点,此时调用此函数,将得到next的thread_info。

切换内核控制流程

接着开始切换控制流程:

movl $1f, %[prev_ip]

pushl %[next_ip]

jmp __switch_to

1:

popl %ebp 首先将标号1,即该段最后一句的地址放入prev->thread.ip。可见下次prev运行时,将从pop %ebp开始。然后,将next->thread.ip压栈,并跳转到__switch_to执行。

__switch_to返回时,会从堆栈弹出一项作为返回地址,由于调用__switch_to时不是通过call指令,而是手工压栈加跳转,所以不会返回到“这里”的标号1处,而是返回到next->thread.ip处。如果next进程不是新创建出来的,那么原来也是通过switch_to切换走的,则断点必定是“那里”的标号1,且此时next的内核堆栈上保存有那一次的ebp和flags,以及ebx等。

因此通过这一次函数调用,内核的控制流程也被成功转移到next,在next上次切换的标号1处,对上次保存的ebp和flags等内容进行恢复,这就完成了整个切换过程。

切换总结

下面是switch_to整个切换过程的图示。

0818b9ca8b590ca3270a3433284dd417.png

switch_to之前

0818b9ca8b590ca3270a3433284dd417.png

切换堆栈之前

0818b9ca8b590ca3270a3433284dd417.png

切换堆栈之后

0818b9ca8b590ca3270a3433284dd417.png

push和jmp

0818b9ca8b590ca3270a3433284dd417.png

__switch_to返回

0818b9ca8b590ca3270a3433284dd417.png

switch_to完成

__switch_to

搞清楚switch_to的作用后,下面来看__switch_to的工作。函数在arch/x86/kernel/process_32.c中,该函数做的关键动作如下。

切换浮点部件寄存器和状态

__unlazy_fpu(prev_p);

/*we're going to use this soon, after a few expensive things*/

if (next_p->fpu_counter > 5)

prefetch(next->xstate);

/*If the task has used fpu the last 5 timeslices, just do a full

* restore of the math state immediately to avoid the trap; the

* chances of needing FPU soon are obviously high now

*

* tsk_used_math() checks prevent calling math_state_restore(),

* which can sleep in the case of !tsk_used_math()*/

if (tsk_used_math(next_p) && next_p->fpu_counter > 5)

math_state_restore();

浮点部件保存与恢复用了一些lazy策略。但是当进程连续使用FPU超过5个时间片时,取消使用lazy策略。

重设TSS的esp0

/** Load the per-thread Thread-Local Storage descriptor.*/

load_TLS(next, cpu);

设TSS的原因是,让在next中从用户态转移到内核态时,切换到next的内核堆栈,以保证正确地保存用户态上下文。

设置current_task

x86_write_percpu(current_task, next_p);

在这一句以后,用current宏得到的当前进程就是next了。

其他

函数中还切换了TLS、IOPL等内容。这些切换并非不重要,但属于细枝末节的问题或较新的功能,因此不仔细分析了。

相关问题

最后遗留的问题是__switch_to的参数传递和返回值问题。从process_32.c中可见,__switch_to有两个参数和一个返回值。但是在调用前并没有按照常规对参数入栈,这是什么原因呢?jmp一句旁边的注释说“regparm call”,提示是通过寄存器传参。而__switch_to函数定义处并没有加入任何选项,应该采用默认的堆栈传参啊!这个问题曾经困扰我很久,后来终于在Makefile中看到了原因。在arch/x86/Makefile中有下面一句:

KBUILD_CFLAGS += -msoft-float -mregparm=3 -freg-struct-return 其中-mregparm=3表示默认使用3个寄存器传参,问题解决。

至于返回值,是按照常规通过eax传递。由代码,__switch_to会将prev返回。而eax最终被绑定到switch_to的last参数上。那么这里last的意义就值得探讨了。根据上面的分析,__switch_to是从prev到next的一座“桥”。调用前,代码是在prev的控制中;调用后,代码就跑到next的控制中了。然而,寄存器信息在调用后却从prev传递到了next。因此,last就是之前一直讲的prev。需要注意的是,现在控制和上下文都跑到了next中,next中的prev并不是之前的prev,因此要凭空生出一个last的概念来维护真实的进程切换关系:对next而言,last是物理时间上的上一个运行的进程,而next中的prev则是next自己,next中的next是next上次切换走时切换到的进程。

从context_switch对switch_to的使用看,last参数又被绑定到了prev。因此,对某一个进程p,它被切换走后,途径各种进程兜一圈回来,prev被赋值为last。从而“保持了”prev的直观意义。

这里描述起来非常绕,原因是prev和next的含义是随着进程的改变而改变的,理解时必须自己在不同的“世界”中来回切换。关键点是,prev和next是和一个进程相绑定的“状态量”,last则是描述两个进程的切换时刻的“过程量”。

总结

总结整个进程切换过程,其中做的关键操作是:切换地址空间、切换内核堆栈、切换内核控制流程,加上一些必要的寄存器保存和恢复。这里,除去地址空间的切换,其他操作要强调“内核”一词。这是因为,这些操作并非针对用户代码,切换完成后,也没有立即跑到next的用户空间中执行。用户上下文的保存和恢复是通过中断和异常机制,在内核态和用户态相互切换时才发生的。从这个意义上讲,切换地址空间才是本质上想要达到的“用户代码和数据的切换”,其余的切换不过是内核中不同的控制流程在“交接棒”而已。

进程切换这里当初领会起来比较难,但是一旦理解,就会深深佩服这一系列过程的巧妙。特别是switch_to宏,几乎就是多一句嫌多,少一句嫌少。

进程的调度

现在回过头来看选择next的过程。

不同类型的进程之间的调度选择

schedule函数中对不同类型的进程之间的调度选择是由pick_next_task函数确定的。这个函数在kernel/sched.c的第4106~4136行:

/** Pick up the highest-prio task:*/

static inline struct task_struct *

pick_next_task(struct rq *rq, struct task_struct *prev)

{

const struct sched_class *class;

struct task_struct *p;

/** Optimization: we know that if all tasks are in

* the fair class we can call that function directly:*/

if (likely(rq->nr_running == rq->cfs.nr_running)) {

p = fair_sched_class.pick_next_task(rq);

if (likely(p))

return p;

}

class = sched_class_highest;

for ( ; ; ) {

p = class->pick_next_task(rq);

if (p)

return p;

/** Will never be NULL as the idle class always

* returns a non-NULL p:*/

class = class->next;

}

}

这个函数的中心环节是一个for循环,它遍历sched_class的每一个实例,并依次调用各个实例的pick_next_task函数。若返回非空,则将下一个进程设为它。

由此可见,Linux调度系统采用的是操作系统理论中的多级队列调度,且上一个队列中进程的优先级恒比下一个队列中进程高。本文第一部分已述,sched_class链表依次为rt、fair、idle。因此,只要有rt类型进程就绪,调度时就一定会被选择,从而保证了rt类型进程的实时性。

注释中还提到一点,idle队列中一定非空,因此在前两个类型的进程都没有就绪时,idle中的idle进程一定会被选中并调度,这保证了循环一定能终止。这里可以看到系统idle进程的重要性。

同类型的进程之间的调度选择

kernel/sched.c中的pick_next_task函数依次对每个sched_class实例调用的pick_next_task,成为同类型的进程之间的调度选择机制的关键。另外schedule函数在pick_next_task之前,还对prev所在sched_class实例做了put_prev_task操作。

由于内核对调度算法做了一层抽象,因此直接从源码分析下去得到的只能是一些函数指针。因此不另加说明时,不妨假定,进程的task_struct结构中sched_class为fair_sched_class。

put_prev_task

在fair类中,此函数在kernel/sched_fair.c中第1246~1258行:

/** Account for a descheduled task:*/

static void put_prev_task_fair(struct rq *rq, struct task_struct *prev)

{

struct sched_entity *se = &prev->se;

struct cfs_rq *cfs_rq;

for_each_sched_entity(se) {

cfs_rq = cfs_rq_of(se);

put_prev_entity(cfs_rq, se);

}

}

此函数意义很明确:"Account for a descheduled task"。其具体实现是找到prev进程对应sched_entity,并调用put_prev_entity。

put_prev_entity在757~773行:

static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)

{

/** If still on the runqueue then deactivate_task()

* was not called and update_curr() has to be done:*/

if (prev->on_rq)

update_curr(cfs_rq);

check_spread(cfs_rq, prev);

if (prev->on_rq) {

update_stats_wait_start(cfs_rq, prev);

/*Put 'current' back into the tree.*/

__enqueue_entity(cfs_rq, prev);

}

cfs_rq->curr = NULL;

} 此函数实现涉及具体的公平调度算法,无需再仔细分析。不过由于公平调度是用红黑树实现的,可以隐约看到它调用__enqueue_entity把进程重新放回了红黑树中。

从这里的分析可以看到,put_prev_task实质上是在进程被调出时,留给具体调度算法更新内部状态的一次机会。

pick_next_task

在fair类中,此函数在kernel/sched_fair.c中第1226~1244行。类似put_prev_task,它调用pick_next_entity。

pick_next_entity在744~755行:

static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)

{

struct sched_entity *se = NULL;

if (first_fair(cfs_rq)) {

se = __pick_next_entity(cfs_rq);

se = pick_next(cfs_rq, se);

set_next_entity(cfs_rq, se);

}

return se;

} 而first_fair是一个内联函数,内容只有一行:

return cfs_rq->rb_leftmost;

if中前两句的工作是更新并摘取红黑树的leftmost区域的sched_entity;后一句最终调用__dequeue_entity,可见是将leftmost从红黑树中取出。

pick_next_entity返回后,pick_next_task再将entity转为task_struct返回。这样,schedule就能得到下一个将要运行的进程了。



推荐阅读
  • 扫描线三巨头 hdu1928hdu 1255  hdu 1542 [POJ 1151]
    学习链接:http:blog.csdn.netlwt36articledetails48908031学习扫描线主要学习的是一种扫描的思想,后期可以求解很 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 题目Link题目学习link1题目学习link2题目学习link3%%%受益匪浅!-----&# ... [详细]
  • CentOS7源码编译安装MySQL5.6
    2019独角兽企业重金招聘Python工程师标准一、先在cmake官网下个最新的cmake源码包cmake官网:https:www.cmake.org如此时最新 ... [详细]
  • 在金融和会计领域,准确无误地填写票据和结算凭证至关重要。这些文件不仅是支付结算和现金收付的重要依据,还直接关系到交易的安全性和准确性。本文介绍了一种使用C语言实现小写金额转换为大写金额的方法,确保数据的标准化和规范化。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • ImmutableX Poised to Pioneer Web3 Gaming Revolution
    ImmutableX is set to spearhead the evolution of Web3 gaming, with its innovative technologies and strategic partnerships driving significant advancements in the industry. ... [详细]
  • This document outlines the recommended naming conventions for HTML attributes in Fast Components, focusing on readability and consistency with existing standards. ... [详细]
  • 机器学习中的相似度度量与模型优化
    本文探讨了机器学习中常见的相似度度量方法,包括余弦相似度、欧氏距离和马氏距离,并详细介绍了如何通过选择合适的模型复杂度和正则化来提高模型的泛化能力。此外,文章还涵盖了模型评估的各种方法和指标,以及不同分类器的工作原理和应用场景。 ... [详细]
  • 本文探讨了 C++ 中普通数组和标准库类型 vector 的初始化方法。普通数组具有固定长度,而 vector 是一种可扩展的容器,允许动态调整大小。文章详细介绍了不同初始化方式及其应用场景,并提供了代码示例以加深理解。 ... [详细]
  • 本实验主要探讨了二叉排序树(BST)的基本操作,包括创建、查找和删除节点。通过具体实例和代码实现,详细介绍了如何使用递归和非递归方法进行关键字查找,并展示了删除特定节点后的树结构变化。 ... [详细]
  • 本文详细介绍了VMware的多种认证选项,帮助你根据职业需求和个人技能选择最合适的认证路径,涵盖从基础到高级的不同层次认证。 ... [详细]
  • 本题涉及一棵由N个节点组成的树(共有N-1条边),初始时所有节点均为白色。题目要求处理两种操作:一是改变某个节点的颜色(从白变黑或从黑变白);二是查询从根节点到指定节点路径上的第一个黑色节点,若无则输出-1。 ... [详细]
  • Linux设备驱动程序:异步时间操作与调度机制
    本文介绍了Linux内核中的几种异步延迟操作方法,包括内核定时器、tasklet机制和工作队列。这些机制允许在未来的某个时间点执行任务,而无需阻塞当前线程,从而提高系统的响应性和效率。 ... [详细]
  • 深入探讨CPU虚拟化与KVM内存管理
    本文详细介绍了现代服务器架构中的CPU虚拟化技术,包括SMP、NUMA和MPP三种多处理器结构,并深入探讨了KVM的内存虚拟化机制。通过对比不同架构的特点和应用场景,帮助读者理解如何选择最适合的架构以优化性能。 ... [详细]
author-avatar
望奇迹般地神话
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有