进程=资源+执行序列。
执行序列=线程。
进程需要进入内核执行,所以进程里面的执行序列其实就是一个内核级线程。
而所谓对资源的管理,其实主要指的是对内存资源的管理。
因为要实现进程,首先需要实现一个内核级线程,然后再是对内存的管理。
先来回顾一下内核级线程切换的两套栈:
main函数执行,需要压栈,栈中保存当前函数执行结束后的返回地址,这里ret=exit ,表示main函数执行完毕后,程序就结束了。
执行A函数时,同样需要压栈,保存A函数执行结束后的返回地址,这里ret=B,表示A函数执行结束后,会去执行B函数
这里ret保存的实际上是cs和ip
A中调用fork函数,该函数首先将系统调用号保存到eax寄存器中,该中断号代表当前执行的是进程创建
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
这里eax中保存的系统调用号,会在system_call_table中发挥索引具体内核函数地址的作用
然后调用INT 0X80中断
执行INT 0x80中断的时候,还没有进入内核,在执行时,会将当前用户栈的SS和SP保存到内核栈中,还有cs和ip,当然还有相关寄存器,下面会将
因为ip会自动加一,所以这里ret= mov res,%eax
EFLAGS保存的是标志寄存器
标志寄存器主要是记录当前的程序状态
操作系统接口和调用–02
在执行sys_fork的时候,可能会引起切换,例如: 如果产生了阻塞或者时间片到期了
对于sys_read或者sys_write来说,会引起阻塞
引起切换,具体判断逻辑是什么,怎么进行切换的呢?
下面先来看看sys_fork执行完后的代码
将当前线程PCB赋值给eax
判断PCB的状态是否为0,在linux 0.11中,0是就绪状态,而非0是阻塞状态
如果调用了相关sys_read和sys_write方法后,只需要将当前PCB状态设置为非0,表示进入阻塞状态接口
如果当前PCB状态为非0,然后就调用reschedule函数进行调度
reschedule函数主要完成的是内核级线程的切换,即PCB切换,因为用户态状态在产生中断的时候,就已经保存到了内核栈中
然后再通过counter来判断时间片是否到期,如果到期了,也需要进行切换
当reschedule函数执行结束后,会去执行ret_from_sys_call函数,即iret返回
reschedule函数首先将ret_from_sys_call子程序标号入栈,然后去执行具体的_schedule函数,这里是一个c函数。
c函数执行结束后,会弹出栈顶元素,然后返回到栈顶元素地址处继续执行。
这里先来看一下中断返回,即执行完_schedule函数后,执行的ret_from_sys_call
恢复现场,将保存的相关寄存器状态从栈中弹出
此时esp指向的栈,已经不是原内核级线程对应的栈了,而是切换后的内核级线程对应的栈,所以这里弹栈,弹的也是切换后线程关联的栈,恢复的是切换后线程原先的状态
iret恢复用户栈和cp,ip相关状态大家注意思考: 此时eax等于多少?
再来看看执行调度的具体过程,即_schedule函数执行:
参考:
Linux0.11内核–进程的调度schedule和switch_to解析
任务状态段TSS及TSS描述符、局部描述符表LDT及LDT描述符
Linux 0.11用tss切换,但也可以 用栈切换,因为tss中的信息可以 写到内核栈中
下面讲解的是基于TSS完成进程切换的过程
在一个多任务环境中,当发生了任务切换,需保护现场,因此每个任务的应当用一个额外的内存区域保存相关信息,即任务状态段(TSS);TSS格式固定,104个字节,处理器固件能识别TSS中元素,并在任务切换时读取其中信息。
各部分关系图如下:
TSS描述符格式:
TYPE中'B':忙,刚创建时应为0,任务开始执行,挂起时为1,由硬件管理,防止切换任务切到自己;TSS描述符DPL必须为0,只有CPL为0能调用;
/****************************************************************************/
/* 功能:切换到任务号(即task[]数组下标)为n的任务 */
/* 参数:n 任务号 */
/* 返回:(无) */
/****************************************************************************/
// 整个宏定义利用ljmp指令跳转到TSS段选择符来实现任务切换
// __tmp用来构造ljmp的操作数。该操作数由4字节偏移和2字节选择符组成。
// 当选择符是TSS选择符时,指令忽略4字节偏移。
// __tmp.a存放的是偏移,__tmp.b的低2字节存放TSS选择符。高两字节为0。
// ljmp跳转到TSS段选择符会造成任务切换到TSS选择符对应的进程。
// ljmp指令格式是 ljmp 16位段选择符:32位偏移,但如果操作数在内存中,顺序正好相反。
// %0 内存地址 __tmp.a的地址,用来放偏移
// %1 内存地址 __tmp.b的地址,用来放TSS选择符
// %2 edx 任务号为n的TSS选择符
// %3 ecx task[n]
01 #define switch_to(n) { /
02 struct (long a,b;} __tmp; /
03 __asm__("cmpl %%ecx,current /n/t" /
04 "je 1f/n/t" /
05 "xchgl %%ecx, current/n/t" /
06 "movw %%dx, %1/n/t" /
07 "ljmp *%0/n/t" /
08 "cmpl %%ecx, %2/n/t" /
09 "jne 1f/n/t" /
10 "clts/n" /
11 "1:" /
12 ::"m" (*&__tmp.a), "m" (*&__tmp.b), /
13 "m" (last_task_used_math),"d" _TSS(n), "c" ((long) task[n])); /
14 }
注释:这是一个嵌入式汇编宏,作用是从当前任务切换到任务n,在进程调度程序中被调用。
首先,TR保存的值,还是先前线程的TSS选择符,因此CPU会首先会根据当前TR中保存的值,定位到先前线程的TSS,然后将当前相关寄存器状态,全部保存到该TSS中。
"d" _TSS(n)将新任务的TSS选择符放入到TR中,然后CPU根据TR中的值,去GDT表中找到对应的TSS描述符,然后根据描述符,定位到新任务的TSS,然后将对应TSS中保存的寄存器状态,全部恢复到当前CPU上
第2个难点是:在第7行执行后,完成任务切换(即切换到新的任务里执行);当任务切换回来后才会继续执行第8行!下面详解其原因。
既然任务切换时CPU会恢复寄存器现场,那么它当然也会保存寄存器现场了。这些寄存器现场都会被写入原任务的tss结构里,值得注意的是,EIP会指向引起任务切换指令(第7行)的下一条指令(第8行),所以,很明显,当原任务有朝一日再次被调度运行时,它将从EIP所指的地方(第8行)开始运行。
回到下面这幅图,我们上面已经讲完了,sys_fork创建线程前需要做啥,创建完线程后要做啥,但是就是没讲,具体内核级线程创建的过程,即sys_fork系统函数执行的过程,下面来具体聊聊:
下面进入sys_fork函数具体执行过程:
_sys_fork函数中具体会去调用copy_process函数完成内核线程的创建,而该函数中需要的所有参数值,都来源于栈中,已经压入栈中的参数是在创建线程前,放入的相关寄存器和用户栈状态
它会用当前进程的一个副本来创建新进程并分配pid,但不会实际启动这个新进程。它会复制寄存器中的值、所有与进程环境相关的部分,每个clone标志。新进程的实际启动由调用者来完成。
首先通过get_free_page()函数,申请一页内存,用来初始化当前线程对应的PCB
这里get_free_page()实际会去mem_map中获取一个空闲页,mem_map在mem_init…中被初始化好
不能使用malloc分配内存,是因为malloc是用户态函数,而这里需要调用内核态分配内存的函数
linux 0.11中线程的切换,是靠tss完成的,因此这里创建内核级线程时,最重要的就是,初始化该内核级线程对应tss的初始化值,这样一会切换到该线程执行时,只需要将该新创建线程的tss中保存的寄存器状态进行恢复即可。
首先,新创建的内核级线程的内核栈使用的是上面申请的PCB内存中的一部分:
对于申请的空闲页来说,下面是PCB内存,上面是内核栈
tss.ss0指向的是内核数据段
对于用户栈来说,其使用的就是父进程的用户栈空间,下面ss和esp参数,就是函数从栈中获取的实参值
最后还有一点需要说明,因为这里使用tss来完成内核级线程的切换,而不是内核栈的方式,因此不需要将eip压入两个栈中。因为tss中已经保存了相关寄存器的值
上面申请内存空间,创建TCB,创建内核栈和用户栈,关联栈和TCB后,下面会做什么事情呢?
然后eax设置为了0,这一点很重要
注意,此时子进程的eip和cs就是一开始父进程中断进入时压入栈中的
此时子进程拿到的eip等于mov res,%eax这条指令位置,而eax的值为0,所以此时res=0
那么fork函数返回0和非0的作用在哪里体现呢?
因此,父进程再创建完子进程,并且子进程第一次运行时,其实执行的就是父进程的代码,只不过,在进入exec后,此时子进程就会去执行和父进程不一样的代码了,相当于一把叉子,分界点就在上面的if判断处.
exec是会去进行系统调用,然后通过中断进入内核,再经过一通操作后,再返回到用户态执行hello.exe可执行文件
执行hello.exe可执行文件,会设计到对文件的操作,磁盘操作,因此必须要进入内核才行
进入内核态靠的是中断,中断返回靠的是iret,那么exec在进入内核前,需要压入栈中的eip设置为hello.exe程序的位置,这样中断返回后,才能直接去执行hello.exe程序
可以看到,在进行具体系统调用sys_execve时,首先会将EIP(%esp)内容压栈,这里EIP=0x1C,那么EIP+esp指向的就是+28的地址处,即ret=??1,这里ret就是存放的中断返回后,将会赋值给eip寄存器的值。
至于hello.ex可执行文件的入口地址是如何找到的,首先需要从磁盘读取出这个文件,然后通过其文件头中定义的信息,就可以找到该文件的入口地址处
然后,通过iret中断返回后,eip会被设置为hello.exe程序的地址,因此子进程就直接去执行该hello程序了
Linux 0.11的TSS方式:
通过内核栈完成切换
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/125707818
内容来源于网络,如有侵权,请联系作者删除!