哈工大操作系统实验4---基于内核栈切换的进程切换

前置知识

关于栈桢

关于栈栈帧详解https://blog.csdn.net/ylyuanlu/article/details/18947951

进程切换流程

首先先了解一下进程切换的流程。开始的时候一个进程(or线程,在这里不加以区分,简单的认为二者的区别只是在是否产生内存映射表的切换,其他方面相同)A正在运行,CPU通过取指执行的方式一点点的去执行它的代码,突然遇到一个中断(系统调用、外部中断、异常),然后就要进入到A进程的内核态去执行相应的中断服务例程,在这期间突然不小心发生一个意外,程序的执行由于缺乏某中资源而不得不中止,而调用了schedule()函数,即产生了进程的切换。而这块知识的重点并非是schedule()函数如何去选择下一个要被执行的进程,我们跳过这个黑箱,假设现在已经取得下一个进程的PCB和PID,那么剩下的重点就落在了switch_to()这个函数的身上。在切换到了新的进程B的内核栈之后,再利用B的内核栈中保存的用户栈的信息跳转到用户态去执行程序,从而完成了进程的切换。

tss的进程切换

TSS就是(task state segment)任务状态。他记录了CPU执行某个进程的上下文,就是比如这个进程开始执行了5秒钟后cpu中各个寄存器的内容,概括的说是记录从cpu来看整个进程执行到了一个什么样的地步,可以形象的理解为TSS就是个相机,将cpu某一时刻的状态给“咔嚓”地拍了下来。由于TSS的本质是一个,所以关于他的寻找遵从保护模式下的段机制,由cpu中的tr寄存器记录当前进程tss所在的位置,然后统一去GDT里面找。
Intel架构不仅提供了TSS来实现任务切换,而且只要一条指令就能完成这样的切换,即图中的ljmp指令。具体的工作过程是:
  (1)通过tr寄存器在GDT中找到当前进程的TSS表,然后将当前cpu的状态记录到找到的TSS表中。
  (2)通过ljmp指令的参数找到下一个要切换进程的TSS表的位置,并将其内容“扣”到cpu中。
  (3)将cpu中的tr寄存器置为当前的TSS位置
  (4)由于当前cpu中已经存放了新进程的cs和ip寄存器,所以直接开始执行就OK
在这里插入图片描述上面给出的这些工作都是一句长跳转指令 ljmp 段选择子:段内偏移,在段选择子指向的段描述符是 TSS 段时 CPU 解释执行的结果,所以基于 TSS 进行进程/线程切换的 switch_to 实际上就是一句 ljmp 指令:

#define switch_to(n) {
    struct{long a,b;} tmp;
    __asm__(
        "movw %%dx,%1"
        "ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
    )
 }
 
#define FIRST_TSS_ENTRY 4
 
#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))

内核栈的机制

每次使用fork()创建一个进程,都会申请一页的空间(4kb),低地址空间base用来存放进程的PCB,而base+PAGE_SIZE则是作为该进程的内核栈的栈底。
该栈用于存放父进程的各种寄存器值,毕竟fork出来的子进程其实跟父进程是完全一样的(除非调用了exec类函数等)。子进程的寄存器基本都是用父进程相应寄存器来赋值(eax除外,其为fork的返回值,子进程的为0)。
父进程在调用系统调用创建子进程时,会把自己的ss、esp、EFLAGS、cs、eip压入栈,这对于调用copy_process函数就相当于传参(其实函数中的参数都是从堆栈中获取的),而在中断处理函数中还会压入父进程的其他寄存器。这时,copy_process(int nr, long ebp, long edi, long esi, long gs, long none,long ebx, long ecx, long edx,long fs, long es, long ds,long eip, long cs, long eflags, long esp, long ss)函数就获得了所有需要的参数(父进程的寄存器)。
压入cs:eip指向的是fork函数的下一个指令的地址,所以子进程被调用的话,第一条指令是调用fork函数完后下面的第一条指令。如if(fork() == 0),这里的所谓的“第一条指令”就是用fork函数的返回值与0做比较。
然后就是switch_to函数用于切换进程,该函数传入两个参数:子进程的PCB地址和其ldt描述符的索引。该函数由汇编代码组成,首先是保存父进程一系列的寄存器到父进程内核栈,还有保存esp在自身PCB相应字段,把子进程的内核栈指针写到全局TSS中esp0字段,然后就是再次把子进程的内核栈指针(在PCB中)写到现在的esp寄存器,这样就可以用ss:esp进行压栈和出栈了。接着就是切换LDT了,使用lldt指令就可以了。然后设置fs寄存器,使其保存指向用户数据空间的局部描述符(0x17 = 0001 0111b)。最后就是连续的出栈指令,把之前创建子进程时保存到子进程内核栈的都出栈到相应的寄存器(这时的内核栈已经切换为子进程的了)。

switch_to 五段论

最重要的就是一套栈到两套栈的转变。老师讲解的很好,这里理一下思路。
例如有段代码是在用户态执行的,有时需要系统调用进入内核。通过中断(INT)可从用户态进入内核态,此时需要从用户栈切换到内核栈,为了从内核顺利返回到用户态,需要在用户栈和内核栈之间建立联系,在进入内核之前需要把用户态下的SS/SP/EFLAGS/IP/CS等信息压入内核栈,等系统调用完成后(中断返回IRET),从内核栈中弹出信息,就可以顺利从之前的用户态执行到的地方继续执行,并且使用的是用户栈。
在这里插入图片描述内核级线程的切换:

S线程,从100处执行用户程序,A()函数调用B()函数,A函数中下一条指令地址104入栈,B()函数调用read()函数,B函数下一条指令地址204入栈,read()函数执行int 0x80中断,内核栈压入此时用户态的SS/SP/EFLAGS/IP/CS等(此时用户态的IP=304)建立内核栈和用户栈的联系,然后进入内核程序,执行system_call处代码,调sys_read函数,把1000压入内核栈,进入sys_read函数内执行。
注意:在调用int,int 后硬件自动做好了,将ss:sp压入了内核栈(也就是内核栈与用户栈的链就关联好了,)至于把CS压栈,是将CS段基址进行关联。
在这里插入图片描述

开始在内核中的切换:switch_to

在这里插入图片描述

S线程在内核执行sys_read函数的时候,启动磁盘读,进行I/O操作(会进入阻塞态), 于是内核就进行调度switch_to(cur, next);(cur是S线程的TCB,next是T线程的TCB),把S线程使用的核分配给T线程,所以就要完成S线程切换到T线程,需要把S线程应该执行的下一个指令地址压入S线程的内核栈,并把S线程的现场保存到S线程的TCB中,然后使用T线程的TCB恢复T线程的现场,当然此时esp指向T线程的栈顶,而遇到switc_to函数的右大括号}时就会弹栈,弹出的就是T线程之前执行到的地址,于是T线程接着之前执行到的地方继续执行。而此时T线程在内核态执行一些代码后,势必还是会回到T线程的用户态的,那么会遇到IRET,返回到T线程的用户态,怎么返回到T线程的用户态呢?T线程内核栈中弹出之前压入的T线程的用户态对应的CS/IP/SP/SS等信息,恢复到T线程的用户态执行。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述- 中断进入内核;

  • 在内核态中,由于启动磁盘或者时钟中断,引发线程切换;
  • 通过TCB对内核栈进行切换;
  • 使用IRET退出中断,对用户栈进行切换。至此,内核栈+用户栈都完成了切换;
  • 如果两个相互切换的线程不是同一个进程,还需要对内存映射表进行切换

实验目的

  • 深入理解进程和进程切换的概念;
  • 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题;
  • 开始建立系统认识。

实验内容

现在的 Linux 0.11 采用 TSS(后面会有详细论述)和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。

而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。

本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。
本次实验包括如下内容:

实验步骤

1、改写switch_to

目前 Linux 0.11 中工作的schedule()函数是首先找到下一个进程的数组位置 next,而这个next就是 GDT中的 n,所以这个 next 是用来找到切换后目标 TSS段的段描述符的,一旦获得了这个 next值,直接调用上面剖析的那个宏展开 switch_to(next);就能完成 TSS 切换所示的切换了。我们不用 TSS进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的 switch_to 中将用到当前进程的PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。
switch_to原来在include/linux/sched.h的宏定义中,我们将那部分注释掉并在kernel/system_call.s中添加以下switch_to汇编代码
switch_to主要是用来切换进程的,包括切换内核栈、LDT、PCB等。

switch_to:(内核及线程的切换)
    pushl %ebp   
    movl %esp,%ebp
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl 8(%ebp),%ebx
    cmpl %ebx,current
    je 1f
#切换PCB
    movl %ebx,%eax
	xchgl %eax,current
#重写TSS指针
    movl tss,%ecx
    addl $4096,%ebx
    movl %ebx,ESP0(%ecx)
#切换内核栈
    movl %esp,KERNEL_STACK(%eax)
    movl 8(%ebp),%ebx
    movl KERNEL_STACK(%ebx),%esp
#切换LDT
	movl 12(%ebp), %ecx
    lldt %cx
    movl $0x17,%ecx
	mov %cx,%fs
#这一段先不用管
    cmpl %eax,last_task_used_math 
    jne 1f
    clts

1: 
	popl %eax
    popl %ebx
    popl %ecx
    popl %ebp
	ret

首先要理解栈桢结构,在解析上述代码之前看一下运行switch_to之前内核栈的具体样子:
在这里插入图片描述

switch_to:(内核及线程的切换)
pushl %ebp /*将栈帧指针ebp压入内核栈中,*/ 这个就是把栈的位置保存,这样在弹出的时候才能知道栈的位置在哪

上面图中的内核栈继续增长变为:
在这里插入图片描述

movl %esp,%ebp //将esp栈指针传递给ebp,ebp指针就指向刚刚压入的ebp位置,设置好新的栈低
pushl %ecx
pushl %ebx
pushl %eax

在这里插入图片描述

movl 8(%ebp),%ebx

linux 0.11的内核栈的地址顺序从上往下看,是由高到低的。也就是说,这句代码是将ebp指针+8指向的数据传递给了ebx寄存器,也就是将pnext(下一个进程的PCB)放在ebx寄存器中:
在这里插入图片描述

cmpl %ebx,current //ebx寄存器中保存的是下一个进程的PCB,current是当前进程的PCB。如果两个进程相同,跳转到1f位置处,后面的代                      码不用执行了,什么都不会发生。
je 1f
movl %ebx,%eax  #切换PCB  把ebx的数据置给eax
xchgl %eax,current  #交换ebx和current中的内容
这两句代码执行完后,ebx和current都指向下一个进程的PCB,eax指向当前进程的PCB
movl tss,%ecx /*明白这一点,有一点需要注意的是,整个切换代码并没有取操作TR寄存器,也即TR寄存器一直都是指向0号进程的TSS,我们				  需要做的是在切换程序中改变TSS中的esp0,使之指向不同进程的PCB所在页的顶端,即进程的内核栈最高点,这个很关键,				 因为每次中断触发时,不论是80中断还是别的时钟中断,芯片会自动的从TR中找到TSS.esp0,将当前用户态的ESP SS 					ELAGS CS EIP存入esp0,然后再将esp0赋值给ESP从而切换到了内核栈
				ecx里面存的是tss段的首地址,在后面我们会知道,tss段的首地址就是进程0的tss的首地址,
	           根据这个tss段里面的内核栈指针找到内核栈,所以在切换时就要更新这个内核栈指针。也就是说,
	            任何正在运行的进程内核栈都被进程0的tss段里的某个指针指向,我们把该指针叫做内核栈指针。*/

addl $4096,%ebx
movl %ebx,ESP0(%ecx)(放入栈指针)
		/*为什么偏移量是4096?4096 = 4KB。在linux0.11中,一个进程的内核栈和该进程的PCB段是放在一块大小为4KB的内存段中的,其中该内存段的高地址开始是内核栈,低地址开始是PCB段。之前提过ebx存放的是新进程PCB指针的地址,之所以要给他加上4096即4K空间是指向了新进程的内存栈指针,由于每个进程的内核栈是由tss中的ss0和esp0共同标记的,所以每次进入内核栈时,任务的内核栈总是空的,而它初始的地址就是PCB首地址+4096。*/
# 切换内核栈
	# KERNEL_STACK代表kernel_stack在PCB表的偏移量,意思是说kernel_stack位于PCB表的第KERNEL_STACK个字节处,注意:PCB表就是task_struct
	movl %esp,KERNEL_STACK(%eax)	/* eax就是上个进程的PCB首地址,这句话是将当前的esp压入旧PCB的kernel_stack。所以该										句就是保存旧进程内核栈的操作。*/
	movl 8(%ebp),%ebx		/*%ebp+8就是从左往右数起第一个参数,也就是ebx=*pnext ,pnext就是下一个进程的PCB首地址。看上								面的图*/
	movl KERNEL_STACK(%ebx),%esp	/*将下一个进程的内核栈指针加载到esp*/
#切换LDT
	movl 12(%ebp),%ecx         /* %ebp+12就是从左往右数起第二个参数,对应_LDT(next) */
	lldt %cx                /*用新任务的LDT修改LDTR寄存器*/
	/*下一个进程在执行用户态程序时使用的映射表就是自己的 LDT 表了,地址空间实现了分离*/

#重置一下用户态内存空间指针的选择符fs
	movl $0x17,%ecx
	mov %cx,%fs
	/*通过 fs 访问进程的用户态内存,LDT 切换完成就意味着切换了分配给进程的用户态内存地址空间,
	所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,
	所以就需要用这两条指令来重取 fs。*/
	

    cmpl %eax,last_task_used_math
    jne 1f
    clts


1:  popl %eax
	popl %ebx
	popl %ecx
	popl %ebp
ret

2、配合switch_to的修改

1、在system_call.s的头部附近添加以下代码

.globl switch_to/*即可使switch_to被外面访问到*/

2、改写task_struct

原来的进程切换是基于TSS切换和长跳转指令的,没有内核栈,也就是说PCB表内没有记录内核栈信息%esp的字段。所以,我们要为PCB增加这么一个字段kernelstack,里面记录了内核栈的栈顶指针,也就是%esp里会放置的内容。在(/oslab/linux0.11/include/linux/sched.h)中找到结构体task_struct的定义,对其进行如下修改:

* linux/sched.h */
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;/*添加*/
/* ...... */
}

3、由于这里将PCB结构体的定义改变了,所以在产生0号进程的PCB初始化时也要跟着一起变化,需要在(sched.h)中做如下修改:

/* linux/sched.h */
#define INIT_TASK \
/* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task,\
/* signals */   0,{{},},0, \
/*也就是增加了PAGE_SIZE+(long)&init_task*/
}

同时在(system_call.s)中定义KERNEL_STACK = 12 并且修改汇编编码,修改代码如下

/* kernel/system_call.s */
ESP0        = 4/*定义 ESP0 = 4 是因为 内核栈指针 esp0 就放在进程0的TSS 中偏移为 4 的地方*/
KERNEL_STACK    = 12 /*KERNEL_STACK = 12的意思非常明显了,我们上面刚刚设过kernelstack放在task_struct第4个位置。*/
/* ...... */
state   = 0     # these are offsets into the task-struct.
counter = 4
priority = 8
kernelstack = 12
signal  = 16
sigaction = 20      # MUST be 16 (=len of sigaction)
blocked = (37*16)

4、由于我们将要在schedule()调用switch_to,所以需要在sche.c声明一下这个外部函数。

/*添加在文件头部附近*/
extern long switch_to(struct task_struct *p, unsigned long address);

3、改写schedule

Linux 0.11 工作的 schedule() 函数是首先找到下一个进程的数组位置 next,而这个 next 就是 GDT 中的 n,所以这个 next 是用来找到切换后目标 TSS 段的段描述符的,一旦获得了这个 next 值,直接调用 switch_to(next);就能完成如图 TSS 切换所示的切换了。

现在,我们不用 TSS 进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的 switch_to 中将用到当前进程的 PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。由于 Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址;另外,由于当前进程的 PCB 是用一个全局变量 current 指向的,所以只要告诉新 switch_to()函数一个指向目标进程 PCB 的指针就可以了。同时还要将 next 也传递进去,虽然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的,也就是说,现在每个进程不用有自己的 TSS 了,因为已经不采用 TSS 进程切换了,但是每个进程需要有自己的 LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到 LDT 的切换。

综上所述,需要将目前的 schedule() 函数(在 kernal/sched.c 中)做稍许修改

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

switch_to(next);

修改为

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

//.......

switch_to(pnext, LDT(next));
/*这样,pnext就指向下个进程的PCB。在schedule()函数中,当调用函数switch_to(pnext, _LDT(next))时,会依次将参数2 _LDT(next)、参数1 pnext、返回地址 }压栈。当执行switch_to的返回指令ret时,就回弹出schedule()函数的}执行schedule()函数的返回指令}。*/

4、修改fork

1、为什么修改fork()

fork()是用来创建子进程的,而创建子进程意味着必须要该线程的一切都配置好了,以后直接切换就可以执行。而由于我们上面修改了进程切换方式,所以原来的配置不管用了。所以我们需要根据切换方式的改变,来改变这些配置。

2、由于fork()的时候要配置子进程内核栈,所以我们先要明确内核栈里面应该有什么。具体可以看一下上面的图解。
再来看一下五段论:

  • 第一段中断进入,是switch_to的第一阶段,核心的工作就是记录当前程序在用户态执行的信息。例如程序指令用户栈等。
    在这里插入图片描述

  • 第二阶段:当在schedule()执行switch_to前会进行参数压栈以及下一条指令压栈内核栈会压入参数_LDT(next)、pnext,然后压入它的下一条指令的位置,也就是 “}”,进入switch_to后,将一些调用者寄存器压栈,以便后续使用这些寄存器。
    此时内核栈如下:
    在这里插入图片描述

  • 第三段切换内核栈
    这个阶段内核栈变化了。一个进程被挂起,也意味着其内核栈被挂起,这时切换到一个之前被挂起的进程,并将其内核栈的栈顶地址加载到esp。
    但是,我们要注意一点:任何进程被挂起,都要经过上面的过程,也就意味着内核栈的结构没变,但是里面的内容变了。

  • 第四段:切换内核栈后,后续经过一些和内核栈无关的操作后,最后在switch_to弹出eax、ebx、ecx、ebp切换内核栈这个阶段内核栈变化了。一个进程被挂起,也意味着其内核栈被挂起,这时切换到一个之前被挂起的进程,并将其内核栈的栈顶地址加载到esp。但是,我们要注意一点:任何进程被挂起,都要经过上面的过程,也就意味着内核栈的结构没变,但是里面的内容变了。切换内核栈后,后续经过一些和内核栈无关的操作后,最后在switch_to弹出eax、ebx、ecx、ebp变成
    在这里插入图片描述switch_to的ret
    1、执行到switch_to 的ret会弹出 “}”并返回到schedule()的 “}”处执行。
    2、在C函数的“}” 处,会自动弹出参数pnext 和_LDT(next),并弹出ret_from_sys_call的地址作为EIP开始执行ret_from_sys_call。
    内核栈情况如下

在这里插入图片描述ret_from_sys_call的最后弹出了一系列用户态寄存器信息

在这里插入图片描述
第五段:回到用户栈

具体修改的代码如下:

1、 schedule() 函数(在 kernal/sched.c 中)

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

改为:

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

由于在seched.c中调用switch_to因此还需要加上函数声明

extern long switch_to(struct task_struct *p, unsigned long address);

还需要在sched.c

还需在初始化pnext时,一定要赋值初始化任务的指针哈,不然,系统是无法跑起来的。
linux-0.11/kernel/schd.c 在schedule()函数前面定义

struct tss_struct * tss = &(init_task.task.tss);

2、system_call.s中实现switch_to

! 汇编语言中定义的方法可以被其他调用需要
.globl switch_to
.globl first_return_kernel
ESP0 = 4
KERNEL_STACK = 12
state	= 0		# these are offsets into the task-struct.
counter	= 4
priority = 8
kernelstack = 12
signal	= 16
sigaction = 20		# MUST be 16 (=len of sigaction)
blocked = (37*16)
.align 2
switch_to:
    	pushl %ebp
    	movl %esp,%ebp
    	pushl %ecx
    	pushl %ebx
    	pushl %eax
    	movl 8(%ebp),%ebx
    	cmpl %ebx,current
    	je 1f
    /* 切换PCB*/
    	movl %ebx,%eax
    	xchgl %eax,current
/* TSS中的内核栈指针的重写*/
    	movl tss,%ecx
    	addl $4096,%ebx
    	movl %ebx,ESP0(%ecx)
		/* 切换内核栈*/
		movl %esp,KERNEL_STACK(%eax)
		/*再取一下 ebx,因为前面修改过 ebx 的值*/
		movl 8(%ebp),%ebx
		movl KERNEL_STACK(%ebx),%esp
		/* 切换LDT*/
        movl 12(%ebp),%ecx
		lldt %cx 
    	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
.align 2
first_return_kernel:
        popl %edx
        popl %edi
        popl %esi
        pop %gs
        pop %fs
        pop %es
        pop %ds
        iret

3、Linux 0.11的PCB定义中没有保存内核栈指针这个域(kernelstack),所以需要我们额外添加。并且将原来的switch_to那个宏注释掉。(/oslab/linux0.11/include/linux/sched.h)中找到结构体task_struct的定义,对其进行如下修改:

/* linux/sched.h */
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;//添加
/* ...... */
}

4、这里将PCB结构体的定义改变了,所以在产生0号进程的PCB初始化时也要跟着一起变化,需要在(sched.h)中做如下修改:

/* linux/sched.h */
#define INIT_TASK \
/* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task,\
/* signals */   0,{{},},0, \
......
}

6、修改fork.c中的copy_process函数

extern long first_return_from_kernel(void);
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;
	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
 	long *kernelStack = (long *)(PAGE_SIZE + (long)p); 
	/* 
	 * 把内核栈做成进程切换之前的样子,即
	 * INT0x80 -> system_call -> schedule() -> switch_to()
	 * 这个过程中形成的堆栈写一遍
	 */
	/* INT0x80 */
 	*(--kernelStack) = ss & 0xffff; 
	*(--kernelStack) = esp;
	*(--kernelStack) = eflags;
	*(--kernelStack) = cs & 0xffff;
	*(--kernelStack) = eip; 
	/* system_call */
 	*(--kernelStack) = ds & 0xffff; 
	*(--kernelStack) = es & 0xffff; 
	*(--kernelStack) = fs & 0xffff; 
	*(--kernelStack) = gs & 0xffff; 
	*(--kernelStack) = esi;
	*(--kernelStack) = edi;
	*(--kernelStack) = edx; 
	/* schedule() */
	/* switch_to 弹栈后执行 ret 
	 * ret会继续弹一次栈给 EIP 执行
	 * 所以这次弹出来的要求是函数地址
	 * 如果是经历过切换的老进程,不是新fork出来的,
	 * 那这里的地址会是 schedule() 中 switch_to 的下一条指令
	 * 即 }
	 * 但由于这里新 fork 出来的,这里的地址就改变到另一个函数 
	 * 即 first_return_from_kernel 
	 * 在 first_return_from_kernel 中执行中断返回 iret
	 * 所以这里 schedule() 就不用入栈一些值,因为跳过了       
	 */
 	*(--kernelStack) = (long)first_return_from_kernel;
	/* switch_to() */
 	*(--kernelStack) = ebp;
	*(--kernelStack) = ecx;
	*(--kernelStack) = ebx;
	*(--kernelStack) = 0; 
	/* 进程 PCB 的 内核栈指针 指向 内核栈*/
	p->kernelStack = kernelStack; 
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;
}

7、编译运行,中间出了好多错误,最终按上面的代码

在这里插入图片描述

最后回答几个问题

1、针对下面的代码片段:

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

回答问题:

  • (1)为什么要加 4096;

    答:4096=4kB,在linux0.11中,一个进程的内核栈和该进程的PCB段是放在一块大小为4KB的内存段中的,其中该内存段的高地址开始是内核栈,低地址开始是PCB段。ebx是指向一个进程的PCB,偏移4096后便指向了另一个进程的PCB.栈结构如下:

    在这里插入图片描述

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

SS0、SS1和SS2分别是0、1和2特权级的栈段选择子。这里用不着特权级为0的内核段。此时唯一的tss的目的就是:在中断处理时,能够找到当前进程的内核栈的位置。

2、针对下面代码片段

*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;

回答问题

1、子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让 eax 等于这样一个数?

eax = 0;其实就是将内核栈用用于返回给eax寄存器的内容置为0,最后eax的内容会返回给fork函数

2、这段代码中的 ebx 和 ecx 来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?

答:ebx和ecx来自copy_process()的形参,形参的来源是各个段寄存器。对于fork函数而言,子进程是父进程的拷贝,就是要让父子进程共用同一个代码、数据和堆栈。

3、这段代码中的 ebp 来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?

ebp是用户栈地址,一定要设置,不设置子进程就没有用户栈了。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值