哈工大os学习笔记五(内核级线程实现)
文章目录
某个中断开始(fork)
内核级线程要在代码层面实现
fork()是系统调用,会引起中断,fork()创建进程(所以会有资源分配和线程的创建),所以fork()是一个非常好的切入点。能够非常清晰地看清
1.线程是怎么切换的
2.切换的五段论代码具体怎么实现的
3.核心级线程切换要具体做哪些事
一、 中断入口、中断出口(前后两段)
首先还是要从上次我画的那个图说起,从那个图中的中断开始。
1. 从int中断进入内核(中断入口第一段)
int 0x80执行的时候没进入内核,执行完毕后,才进入内核。
由于int 0x80没有执行完毕,所以当前的PC指向的是当前的(int 0x80的下一条)
_NR_fork 是系统调用号,存入%eax寄存器,后面会算出调用哪个函数
压入EFLAGS标志寄存器的状态,保存寄存器的状态,在保存寄存器里的内容
int 0x80中断,之后就是OS为我们打开的系统调用system _call,system_call还要压栈,因为我们刚进入内核,CPU这些寄存器仍然是用户态的,要保存寄存器的内容,将一些内容压栈,保护用户态执行的现场。
调用system_call 这个表,具体处理sys_fork,实现sys_fork真正的效果,执行sys_fork 的时候会引起切换。I/O阻塞,发生调度
执行完sys_fork 后,不停,又往下执行。
call 下面的代码就是判断是否引发中断的代码
_systemZ_call:
push %d...%fs
push %edx...
call sys_fork //
push1 %eax
————————————————————————————————
mov1 _current,%eax //将当前的线程(当前线程一直是sys_fork)置给 eax
cmp1 $0,state(%eax) // state(%eax)是一段汇编代码,实际上是state加上eax
//current是PCB,就是看PCB中的state是不是0
//0是运行,非0表示阻塞
//如果不是0就要调度,也就是判断PCB队列的状态
jne reschedule //跳转调度,跳到schedule完成内核 栈的切换
//schedule五段论中间三段
——————————————————————————
cmp1 $0, counter(%eax) //第五段判断current的counter是不是等于0
je reschedule //等于0也进行调度,时间片是否为0
//时间片为0,时间片用完了为0,也要进行切换
ret_from_sys_call: //跳出去后,还要回来,这里是系统调用返回
//接下来就要执行中断返回的代码
——————————————————————
reschedule: //汇编的编号,实际上就是地址
//ret_from_sys_call的地址
push1 $ret_from_sys_call //将地址压栈
jmp _schedule /*调用_schedule,这个是C函数,他执行的时候
必然会遇到右大括号},执行}的时候就将弹栈,
返回的就上刚才压入的地址,
就会去ret_from_sys_call:执行*/
2.中断出口(最后一段)
会一个一个的弹栈,最后将iret一起pop弹出。
切换出去后,他的下一个核心线程,也就是下一个进程的执行起来的用户栈,完成切换了。
这里保存的都是用户态的内容
二、 其他三段
1.schedule()
schedule
next就是找下一个 线程,至于怎么找,后面会详细的讲。
next i进行调度,switch_to(next),next是下一个进程的TCB,
如果是线程那就是下一个核心级线程的TCB,switch_to 进行TCB切换就很简单,只要修改一下就ok
核心是根据TCB完成核心栈的切换
2.switch_to()
TCB切换图
switch_to 具体代码实现:
下面李老师讲的这个代码(Linux0.11)是TSS实现切换的,而真正的用内核栈切完成切换的是实验四。TSS的介绍实验四有,是基于TSS来实现Kernel stack
TSS:task struct segment 任务结构段,英特尔给做出来了只用一句常跳转指令(ljmp %0\n\t)就可以做到,但是效率特别低,所以进行了改造
TR是CPU的寄存器,相当于GDT表
相当于一个快照,保存恢复。
next就是要进行改变的TR,新的TR找到新TSS描述符,在指向新的段,新的段保存了现场。
这种方式慢,由于快照要扣好多东西,而且由于是一条常指令,不能进行指令流水,是硬件设计效果不好,不能充分利用向现代CPU硬件的加速方法。而用栈,不用照好多东西只要压栈弹栈,而且可以进行流水。
3.ThreadCreate(创建好TSS)
创建线程,就是要创建出能够切换的样子,能够切换了,线程也就创建好了。所以下面的核心就是把TSS做好。当然TSS要做好首先要有PCB,TCB,内核栈,然后就可以做TSS。
下面 是代码实现:
继续执行fork()
_copy_process 拷贝父进程
下面是一段汇编
上面什么参数也没写,是因为都在内核栈里面。
因为父进程执行fork的时候,压了一堆东西,这些东西主要就是父进程在用户态执行时候的样子。这些样子(参数)需要传递给_copy_process,C语言执行的时候他的参数都从栈中弹出,当然这里已经进入内核执行了所以这里是内核栈。所以内核栈里面的内容全都作为参数,有了这些参数就能知道父进程长什么样子了。在_copy_process 基本能做出和父进程一样的叉子(进程)父进程和子进程只有一个地方的小差别。
这些参数包括esp等。在C函数中,越在后面的参数就越早压入栈中,所以位置越靠前的参数,越靠近栈顶。参数最长的函数。
esp>ss:sp esp j就是标记栈的
eip>ret=??1 int 0x80 要执行的下一句代码
要知道这些参数和栈的序列是怎么对应的
看下图:内核态代码get_free_page();获得一页内存,不能用malloc,因为malloc 是用户态的代码,在系统初始化的时候涉及到mem_map,mem_map将内存打成4K,4K,4K的一段一段,那个就叫一页一页 ,从那里找空闲页
就是mem_map等于0的那一页,找到mem_map等于0的那一页,把地址返回给p,并进行强制转换,这一页内存就用来做PCB。
PCB就有了
下面四行代码就是设置TSS,esp0就是内核栈,esp就是用户栈,这都是硬性规定好的。PAGE_SIZE(4k)p申请好的地址。
0x10 是内核数据段 ,这里也是用的内核堆栈段是一个段。
内核段有了,段偏移4k+p,下面是PCB,上面是内核栈。(PCB内核栈都做好了)
设置用户栈(copy_process那个贼多参数那个)
父进程传递下来的东西,父进程正在执行int0x80那个时候用到的用户栈,所以用了和父进程一样的栈。用户级可以用一样的,但是在内核级是两个不同的东西
在把TSS初始化就OK了,eip int 指令完成后的下一句话
4.小结
fork()子进程已经做好了
-
父进程阻塞,然后进入schedule调度
-
schedule就要执行switch_to
-
switch_to就切换到子进程
-
子进程就会把刚才初始化好的TSS,直接扣到CPU。
5. 其中包括eip等于int 0x80,eax等于0
父进程进去int创建一个子进程,返回的时候执行int的下一条,所以父进程也执行这条指令,父进程执行的时候eax≠0,所以父进程执行下面的
exec怎么进去的,从exec系统调用开始,进去后会执行实sys_execve这个函数,
老师提出的问题:在进入子进程之前,父进程子进程执行同样的代码。
好了现在调用exec,进入到内核态,一顿折腾后,再返回的时候执行的就不再是和父进程一样的代码,而是ls。
那么以上这些东西是怎么做的???
怎么做的:
中断返回的时候要做iret,iret 找到ss sp ret 一些的东西,将这些栈存储的东西赋给真正的寄存器,然后让PC执行。
接下来执行 call_do_execve这个函数,压栈,压栈是因为exec也是要有参数的,实际上压进去的实际是参数,参数是esp,eip。
如果能把ls的地址找到并且赋给ret,将来在执行iret的时候弹栈。EIP也就是一个偏移
esp,eip加起来赋给eax,eax进行压栈,EIP=0x1c 十进制是28,正好跳到28,也就是ret=???(eip)最后会指向PC。
eip[0]=ex.a_entry入口地址置给eip,就okl了。
而eip[3] 指向SS:SP
ls有自己的执行代码了有自己的栈了。
ex.a_entry哪里来的?可以读文件 ls是一个可执行命令 ls hello ,hello 是一个可执行程序,这个程序在磁盘上有,从磁盘上把这个程序读进来,就有一个文件头,这个文件头就有a_entry,a_entry是编译的是时候作为可执行文件写进去的。实际上是连接做为可执行文件程序写入的,写进去就放给ex.a_entry。在读文件读出来置给内核栈中,置好内核栈后,在中断返回iret时候就弹回去了,就执行hello的第一句话。
小结
做一个操作系统要知道好多内容:读写文件,链接,文件的格式,内核栈结构,内核栈位置,文件头赋给内核栈的相应位置,iret等等。