2019-2020-1 20199311《Linux内核原理与分析》第九周作业
1.问题描述
通过这一周的学习,我主要了解了linux中关于进程切换的部分,也就是进程调度时机来临时从就绪进程队列中挑选一个进程执行,占用CPU时间。下面通过介绍进行调度的时机和进程切换的过程这两部分知识,以及使用gdb跟踪分析schedule()函数来深入理解linux中的进程切换。
2.解决过程
1. 进程调度的时机
1.1 中断
进程调度的时机都与中断有关。除了主动让出CPU外,进程的调度都需要在内核进行,这就需要从进程的指令流里切换出来。中断能起到切出进程指令流的作用,中断处理程序是与进程无关的指令流。运行完内核代码后,CPU顺带检查一下是否需要进程调度。需要则切换进程(本质上是切换两个进程的内核堆栈),不需要则顺着函数调用堆栈正常中断返回iret。中断分为软中断和硬中断。
1.1.1 硬中断
硬中断就是CPU的两根引脚(可屏蔽中断和不可屏蔽中断)。CPU在执行每条指令后去检测这两根引脚的电平,如果是高电平,说明有中断请求,CPU就会中断当前程序的执行去处理。
1.1.2 软中断
软中断会导致程序无法继续执行,而跳转到CPU预设的处理函数
- 故障(Fault):发生问题,但可以恢复到当前指令。
- 退出(Abort):不可恢复的严重故障,程序退出。
- 陷阱(Trap):程序主动产生的异常,在执行当前指令后发生。简单说就是程序自己要借用中断这种机制进行转移。
1.2 进程调度时机
1.2.1 schedule函数
Linux内核通过schedule函数实现进程调度,schedule函数在运行队列中找到一个进程,把CPU分配给它。所以调用schedule函数的时候就是进程调度的时机。
1.2.2 上下文
一般来说,CPU在任何时刻都处于以下3种情况之一。
- 运行于用户空间,执行用户进程上下文
- 运行于内核空间,处于进程(一般是内核线程)上下文
- 运行于内核空间,处于中断上下文
1.2.3 进程调度
进程调度的时机就是内核调用schedule函数的时机。下面是对进程调度时机的总结
- 用户进程通过特定的系统调用主动让出CPU。
- 中断处理程序在内核返回用户态时进行调度。
- 内核线程主动调用schedule函数让出CPU。
- 中断处理程序主动调用schedule函数让出CPU。
2. 调度策略和算法
调度算法就是从就绪队列中选一个进程。调度策略是寻找满足需求的方法,而调度算法是如何实现这个调度策略。
2.1 进程的分类
按CPU占用率分类
- I/O消耗型进程。这种进程的特点是CPU负载不高,大量时间都在等待读写数据
- 处理器消耗型进程。这种进程的特点是CPU占用率为100%,但没有太多的硬件进行读写操作
按对系统响应时间要求分类 - 交互式进程。此类进程有大量人机交互,对系统响应时间要求比较高,否则用户会感觉系统反应迟缓。
- 批处理进程。此类进程不需要人机交互,在后台运行,需要占用大量的系统资源,但是能够忍受响应延迟。
- 实时进程。实时进程对调度延迟的要求最高,这些进程执行非常重要的操作,要求立即执行响应并执行。
当前Linux系统的解决方案是,对于实时进程,linux采用FIFO(先进先出)或者Round Robin(时间片轮转)的调度策略。对其它进程,则采用CFS调度器,核心是“完全公平”。
2.2 调度策略
Linux系统中常用的几种调度策略为SCHED_NORMAL、SCHED_FIFO、SCHED_RR。
其中SCHED_NORMAL是用于普通进程的调度类,而SCHED_FIFO和SCHED_RR是用于实时进程的调度类,优先级高于SCHED_NORMAL。内核根据进程的优先级来区分普通进程与实时进程,Linux内核进程优先级为0139,数值越高,优先级越低,0为最高优先级。实时进程的优先级取值为099,普通进程只具有nice值,nice值映射到优先级为100~139。
2.3 CFS调度算法
CFS即为完全公平调度算法,其基本原理是基于权重的动态优先级调度算法。每个进程使用CPU的顺序进程由已使用的CPU虚拟时间(vruntime)决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的概率也就越高。每个进程每次占用CPU后能够执行的时间(ideal_runtime)由进程的权重决定,并且保证在某个时间周期(_sched_period)内运行队列的所以进程都能够至少被调度执行一次。
3. 进程上下文切换
3.1 进程执行环境的切换
为了控制进程的执行,内核必须有能力挂起正在CPU中运行的进程,并恢复执行以前挂起的某个进程。这种行为被称为进程切换,任务切换或进程上下文切换。进程上下文包含了进程执行需要的所有信息。
- 用户空间地址:包括程序代码、数据、用户堆栈等。
- 控制信息:进程描述符、内核堆栈等。
- 硬件上下文,相关寄存器的值。
在实际的代码中,每个进程切换基本由两个步骤组成
- 切换页全局目录(CR3)以安装一个新的地址空间,这样不同进程的虚拟地址就会经过不同的页表转换为不同的物理地址。
- 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息。
3.2 核心代码分析
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行硬件上下文切换。
switch_to代码如下(内联汇编代码)
#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" /* 保存当前进程flags */
"pushl %%ebp\n\t" /* 当前进程堆栈基址压栈*/
"movl %%esp,%[prev_sp]\n\t" /*保存ESP,将当前堆栈栈顶保存起来*/
"movl %[next_sp],%%esp\n\t" /*更新ESP,将下一栈顶保存到ESP中*/
//完成内核堆栈的切换
"movl $1f,%[prev_ip]\n\t" /*保存当前进程EIP*/
"pushl %[next_ip]\n\t" /*将next进程起点压入堆栈,即next进程的栈顶为起点*/
//完成EIP的切换
__switch_canary
//next_ip一般是$1f,对于新创建的子进程时ret_from_fork
"jmp __switch_to\n" /*prev进程中,设置next进程堆栈*/
//jmp不同于call是通过寄存器传递参数
"1:\t" //next进程开始执行
"popl %%ebp\n\t"
"popfl\n"
/*输出变量定义*/
: [prev_sp] "=m" (prev->thread.sp), //[prev_sp]定义内核堆栈栈顶
[prev_ip] "=m" (prev->thread.ip), //[prev_ip]当前进程EIP
"=a" (last),
/* 要破坏的寄存器: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* 输入变量: */
: [next_sp] "m" (next->thread.sp), //[next_sp]下一个内核堆栈栈顶
[next_ip] "m" (next->thread.ip),
//[next_ip]下一个进程执行起点,,一般是$1f,对于新创建的子进程是ret_from_fork
/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
: /* 重新加载段寄存器 */
"memory");
} while (0)
切换过程如下图所示
其中要注意对函数堆栈的理解。其实堆栈存储了进程所有的函数调用历史,所以剩下的只要顺着堆栈返回上一级函数即可。由于_switch_to是被schedule()函数调用的,而schedule()函数又在其它系统调用函数中被调用,所以先返回到next进程上次切换让出CPU时的schedule()函数中,然后返回到调用schedule()的系统调用处理过程中。而系统调用又是在用户空间中触发的,所以通过中断上下文返回到系统调用被触发的地方,接着执行用户空间的代码。这样就回到了next进程的用户代码空间代码。
4. Linux系统构架与执行过程概览
Linux操作系统的整体架构如下图所示
中间为内核实现,内核向上为用户提供系统调用接口,向下调用硬件服务接口。其自身实现了如进程管理等功能。
下面用ls命令的执行过程分析整个系统的运行。当用户输入ls并回车后键后,发生如下图所示过程
5.使用gdb跟踪分析schedule()函数
本实验在实验楼环境下进行
首先将menu目录删除,利用git命令克隆一个新的menu目录
重新编译内核
返回LinuxKernel目录,shift+ctrl+o水平分割,执行如下命令
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
启动gdb,准备进行单步调试
gdb
file linux-3.18.6/vmlinux
target remote:1234
设置如下断点
b schedule
b context_switch
b switch_to
b pick_next_task
由于switch_to内部是内嵌汇编代码,无法跟踪调试,不过我们前面已经做了分析。
跟踪调试,首先停在schedule函数处
停在pick_next_task函数处
停在context_switch处
循环调用,进行进程切换
宏和函数调用顺序如下
schedule() --> context_switch() --> switch_to --> __switch_to()
3. 总结
通过这一周的学习,我对linux进程的切换,已经linux执行程序的过程进行了一个初步的了解,我将在未来的学习中,不断地深入学习和理解linux内核的工作机制。