操作系统 多进程图像 实验五-基于内核栈切换的进程切换

实验五 基于内核栈切换的进程切换

本文资源来自 哈工大李治军老师的 操作系统原理与实践 课程
实验地址:https://www.lanqiao.cn/courses/115
课程地址:https://www.bilibili.com/video/BV1d4411v7u7
部分图文来自 《操作系统真像还原》 郑刚,带我走进操作系统

前言

对初学者的建议

如果想真正学会操作系统,需要的远不仅仅是“看懂”CPU 内存管理 外设 IO 的 抽象原理,真正要做的是动手去写。而最易懂的方法是 在linux中操作。通过 bochs 自动生成img镜像 自己往镜像里面写一个不那么完整操作系统,并在此过程中调试,最终逐渐理解一切
~~
想要做实验,需要具有以下基础

  • intel汇编,AT&T汇编
  • 会拆解内联汇编
  • c语言,尤其指针和结构体部分
  • linux命令

image.png

上图展示了 linux 0.11版本 从系统加电起执行程序的顺序,其中 .s是汇编源代码文件,.c是c|c++文件。因此需要系统学习汇编和c语言基础,我们在内核部分还会碰到在c语言内嵌入汇编代码的情况,因此这两部分是硬实力,必须掌握。除此之外推荐“拿来主义”,到了再学。

TSS PCB/TCB LDT

TSS

TSS ,即 Task State Segment ,意为任务状态段,它是处理器在硬件上原生支持多任务的一种实现方式,下图是它的数据结构,TSS 是每个任务都有的结构 它用于任务的标识,相当于任务的身份证,程序拥有此结构才能运
行,这是处理器硬件上用于任务管理的系统结构。通过在GDT注册后使用

从28-100就是把寄存器压栈,存储该任务的状态,espX代表了对应特权级的栈地址。本实验中因为tss用于操作内核栈,故使用esp0。
image.png
image.png

PCB

操作系统为每个进程提供了 PCB ,Process Control Block,即程序控制块,它就是进程的身份证,用它来记录与此进程相关的信息,比如进程状态、 PID 、优先级等。又可称为进程表项。tcb就是thread CB,类比。

要注意的是,PCB格式并不唯一,大小以 页(4KB)为单位,而本实验中的内核栈指针位于满栈的栈顶。
image.png

LDT

LDT Local Descriptor Table 的缩写,即局部描述符表。Inter建议为每个程序单独赋予一个程序描述其私有资源(代码段,数据段,栈段)的数据结构,即LDT。在GDT注册使用

通过选择子找到对应的LDT描述符,再通过段基址:偏移+映射找到对应私有资源地址。
image.png

实验逻辑

本实验最终要求不完全依靠TSS,用PCB和LDT实现堆栈形式的基于内核栈切换的进程切换。
其中内核级进程使用的是fork()陷入,因此简易了创建新进程的步骤。

进程切换最关键的函数在 switch_to,通过TCB的内核栈指针,ret到内核程序,用CS:IP切到用户段。
image.png

步骤

修改schedule调度函数

1
2
3
4
5
6
7
8
kernal/sched.c schedule() 函数:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;


switch_to(pnext, LDT(next));
}
  • pnext——下一进程的指针,与current对应。
  • LDT(next)——下一进程的LDT地址
  • current——在 <asm/current.h> 中定义, 它产生一个指针指向结构 task_struct,在此指向当前运行进程

next在没学选择子前,可以理解为GDT数组标号。

补充 c语言函数调用栈

在schedule()中,switch是被调函数,所以ebp位置如图(EBP of callee function)
所以在schedule的函数调用栈中,
switch_to运行时,2个参数分别位于8(ebp)和12(ebp)。

实现不主要依靠TSS的switch_to()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
已修改版本,在代码块里逐行讲。老师写的太漂亮了~
switch_to:
pushl %ebp
movl %esp,%ebp
!这里栈帧初始化,然后把子进程PCB指针地址pnext传给ebx,和全局变量current对比
pushl %ecx
pushl %ebx
pushl %eax
movl 8(%ebp),%ebx
cmpl %ebx,current
je 1f
! 切换PCB
movl %ebx,%eax
xchgl %eax,current ! 交换current和eax,实现PCB指针切换


! TSS中的内核栈指针的重写
! 只是充当“保留”功能,因为原linux用的tss切换(数据结构繁琐,相对耗时),这里作保留的把0号tss
! 的esp0(位于4偏移),对应内核栈指针重写为下一进程PCB顶部的nei
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
! 切换内核栈
KERNEL_STACK = 12
movl %esp,KERNEL_STACK(%eax)
! 再取一下 ebx,因为前面修改过 ebx 的值
movl 8(%ebp),%ebx
movl KERNEL_STACK(%ebx),%esp
! 切换LDT,本质是切换用户态映射。
movl 12(%ebp),%ecx
mov %cx,%fs
movl $0x17,%ecx
mov %cx,%fs
! 和后面的 clts 配合来处理协处理器,由于和主题关系不大,此处不做论述
cmpl %eax,last_task_used_math
jne 1f
clts

1: popl %eax
popl %ebx
popl %ecx
popl %ebp
ret
1
2
3
4
5
6
7
8
// 在 include/linux/sched.h 中
//原本PCB中是没有内核栈指针的属性的,但可以人为添加,看一下新PCB数据结构
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;
//......

修改fork.c

fork直译是叉子,可以形象理解为最上面的尖头是父进程,下面的是子进程,是通过拷贝父进程+修改生成的
这是fork.c修改的部分代码,是看懂switch必须会的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
! sched -> sys_fork ->call copy_process
copy_process(){
long * krnstack;
struct task_struct *p
p= (struct task_struct *)get_free_page();//获取一个空闲页用作存放自定义内核栈+PCB

krnstack=(long)(PAGE_SIZE+(long)p);//把krnstack内容放在页末(对应后面问题)

!PCB初始化
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority


*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
// 这里的 0 最有意思,细致观察会发现弹栈这里对应的是%eax,原因放在“问题”做解释
*(--krnstack) = 0;

p->kernelstack = krnstack;

思考问题

针对下面的代码片段:

1
2
3
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)

回答问题:

  • (1)为什么要加4096;

在copy_process()中,子进程的PCB大小设置为一页4KB,而内核栈基址设在了页末所以是 PCB指针+4096

  • (2)为什么没有设置tss中的ss0。

对于linux0.11来说,内核栈段的选择子永远都是0x10,故ss0不用初始化


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!