《操作系统》by李治军 | 实验5.pre - copy_process 代码详解

目录

【前言】

代码分析

1. 申请子进程 PCB 空间

2. 找到子进程的内核栈

3. 父子进程共用同一内存、堆栈和数据代码块

4. 相关寄存器入栈

5. switch_to 返回位置

6. 响应 switch_to 的弹栈工作

7. 更新子进程 PCB

8. 子进程切换流程


【前言】

       在李治军老师的《操作系统》课程的实验 5(基于内核栈切换的进程切换)中,需要对 fork() 系统调用进行修改,使得进程创建出能完成内核栈切换的样子,并实现父子进程共用同一个代码、数据和堆栈。

       fork() 的核心是调用 copy_process(),所以主要是修改 copy_process 函数。copy_process 的代码不难获取,本文会对其中重要的语句进行分析,看一看修改后的 fork 创建的子进程如何实现进程的 “复刻” 以及切换,希望能帮助理解基于内核栈的进程创建(即子进程内核栈的初始化)。

copy_process 部分代码:

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;
 
    //用来申请一页内存空间作为子进程的PCB
	p = (struct task_struct *) get_free_page();

	if (!p)
		return -EAGAIN;  //申请失败
    //申请成功
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */

    //子进程PCB部分内容初始化
	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;
 
	//容易看出下面的部分就是基于tss进程切换机制时的代码,所以要将此片段注释掉
    /*
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	...
    p->tss.trace_bitmap = 0x80000000;
	*/
	

	/** 从这里开始添加基于堆栈切换的代码(对fork的修改其实就是对子进程内核栈的初始化) **/
 

	long *krnstack;
 
    //p指针加上页面大小就是子进程的内核栈位置,所以krnstack指向子进程的内核栈
    krnstack = (long)(PAGE_SIZE +(long)p);
 
	//现在开始初始化子进程内核栈(krnstack)中的内容:
 
	//下面的五句话可以完成上图所示的关联效果(父子进程共用同一内存、堆栈和数据代码块)
	//因为下面的ss,esp,eflags,cs,eip这些参数就是来自调用copy_process()函数的进程的内核栈中
	//也就是父进程的内核栈,所以下面5条指令就是将父进程内核栈中的前五个内容拷贝到子进程的内核栈中

	*(--krnstack) = ss & 0xffff;
	*(--krnstack) = esp;
	*(--krnstack) = eflags;
	*(--krnstack) = cs & 0xffff;
	*(--krnstack) = eip;
 
    *(--krnstack) = ds & 0xffff;
    *(--krnstack) = es & 0xffff;
    *(--krnstack) = fs & 0xffff;
    *(--krnstack) = gs & 0xffff;
    *(--krnstack) = esi;
    *(--krnstack) = edi;
    *(--krnstack) = edx;
 
    //处理switch_to返回的位置,详细解释见下文
	*(--krnstack) = (long) first_return_kernel;
 
    //接下来的工作需要和switch_to接在一起考虑
    //故事从哪里开始呢?回顾一下前面给出的switch_to,应该从“切换内核栈”完成的那个地方开始
    //当时已经切换到子进程的内核栈开始工作,所以switch_to最后的四次弹栈以及ret使用的都是子进程内核栈中的东西
 
    /*  switch_to:
        ...
    1:	popl %eax
	    popl %ebx
	    popl %ecx
	    popl %ebp
	    ret
    */
 
    //为了顺利完成上面4条弹栈工作,子进程的内核栈中应该有这些内容
    //所以需要对子进程的krnstack进行相应初始化:(下面的ebp,ecx,ebx一样都来自父进程的内核栈)
	*(--krnstack) = ebp;
	*(--krnstack) = ecx;
	*(--krnstack) = ebx;
 
	*(--krnstack) = 0;
    //这里为啥是0而不继续继承父进程的eax呢?
    //这里的eax就是进程的返回值,也是子进程和父进程的不同之处
    //父进程返回子进程的pid,子进程返回0
 
    //最后别忘了,由于我们初始化了子进程的内核栈,栈顶指针也变了
    //所以要更新存放在PCB中对应的内核栈指针:
	p->kernelstack = krnstack;

	...

}

代码分析

接下来分析 copy_process 中一些关键的语句。

1. 申请子进程 PCB 空间

struct task_struct *p;

p = (struct task_struct *) get_free_page();

>> 申请一页内存空间作为子进程的 PCB,由指针 p 指向 <<

2. 找到子进程的内核栈

long *krnstack;

krnstack = (long)(PAGE_SIZE +(long)p);

>> p 指针加上页面大小就是子进程的内核栈位置,所以 krnstack 指向子进程的内核栈 <<

       现在父进程和子进程的内核栈情况如下图所示,此时子进程内核栈空空如也(父进程通过 schedule() 中的 switch_to 被切换)。我们要做的就是向子进程内核栈中添加相应内容,使得子进程和父进程共用同一个代码、数据和堆栈。并且当子进程被调度时,通过 switch_to() 切换到子进程时能够实现基于内核栈的切换。

3. 父子进程共用同一内存、堆栈和数据代码块

*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;

>> 将 ss、esp、eflags、cs、eip 依次压入子进程的内核栈 <<

       上面的 ss、esp、eflags、cs、eip 这些参数都是来自调用 copy_process() 函数的进程的内核栈 —— 也就是父进程的内核栈。所以以上 5 条指令就是将父进程内核栈中的前五个内容拷贝到子进程的内核栈中(Linux 0.11 内核栈的地址顺序从上往下,是由高到低的)。

压栈后父进程和子进程的内核栈情况如下图所示:

4. 相关寄存器入栈

*(--krnstack) = ds & 0xffff;
*(--krnstack) = es & 0xffff;
*(--krnstack) = fs & 0xffff;
*(--krnstack) = gs & 0xffff;
*(--krnstack) = esi;
*(--krnstack) = edi;
*(--krnstack) = edx;

>> ds、es、fs、gs……依次压入子进程的内核栈 <<

压栈后父进程和子进程的内核栈情况如下图所示:

       为什么这里子进程压栈的内容和父进程栈中内容不太一样呢?因为父进程内核栈中 ds ~ eax 的压栈是通过 system_call 函数完成的(因为父进程通过系统调用进入内核),其中 reschedule 又将 ret_from_sys_call 函数的地址入栈。


       而 ret_from_sys_call 函数又实现将之前由 system_call 保存的 eax ~ ds 依次恢复,并通过 iret 离开内核返回到用户程序。


       但这里不用完全 copy 父进程的内核栈内容,我们也不通过 ret_from_sys_call 退出内核,而是自己编写一个函数 first_return_kernel 实现内核栈到用户栈的切换,效果是一样的。

5. switch_to 返回位置

*(--krnstack) = (long) first_return_kernel;

>> 将 first_return_kernel 函数的地址入栈 <<

       这里的 first_return_kernel 作为 switch_to 的返回位置(具体解释见下文),入栈后内核栈情况如下图所示:

6. 响应 switch_to 的弹栈工作

*(--krnstack) = ebp;

*(--krnstack) = ecx;

*(--krnstack) = ebx;

*(--krnstack) = 0;

>> ebp、ecx、ebx、0 依次入栈 <<

       上面的工作需要和 switch_to 接在一起考虑。从哪里开始接呢?回顾 switch_to 的代码,应该从 “切换内核栈” 完成的地方开始。当时已经切换到下一个进程的内核栈开始工作,所以 switch_to 最后的四次弹栈以及 ret 指令使用的都是下一个进程内核栈中的东西。

// 进入新进程的内核栈工作,接下来的四次弹栈以及ret处理使用的都是新进程内核栈中的东西
popl %eax
popl %ebx
popl %ecx
popl %ebp
ret

       当上面创建的子进程被 schedule() 调度,switch_to() 会依次将 PCB、内核栈、LDT 切换到子进程,接着就执行上面 5 条指令。为了顺利完成前 4 条弹栈工作,子进程的内核栈中应该有这些内容。所以需要对子进程的内核栈进行相应初始化(上面的 ebp、ecx、ebx 同样都来自父进程的内核栈),入栈后内核栈情况如下图所示。

ps:注意这里  *(--krnstack) = 0  而不是  = eax,也就是说 switch_to 进入子进程内核开始工作后执行  popl %eax  会得到 eax = 0。这里其实就是将内核栈中用于返回给 eax 寄存器的内容置为 0,而 eax 的内容会返回给 fork 函数,这就是为什么子进程的返回值为 0

7. 更新子进程 PCB

p->kernelstack = krnstack;

>> 指针 p 指向子进程的 PCB,将其中的内核栈指针置为 krnstack <<

       由于我们对子进程的内核栈进行了一系列初始化,栈顶指针也改变了,所以要更新存放在 PCB 中对应的内核栈指针。

8. 子进程切换流程

(1)现在子进程的内核栈已经初始化完成,栈内情况如下图所示:

(2)当上面的子进程被 schedule() 调度,switch_to() 会依次完成 PCB、内核栈、LDT 的切换。此时该子进程内核栈情况如下图所示:

(3)接着就是 switch_to 的最后 5 条指令:

// 现在进入新进程的内核栈工作了,所以接下来做的四次弹栈以及ret处理使用的都是新进程内核栈中的东西
popl %eax
popl %ebx
popl %ecx
popl %ebp
ret

① 前四次弹栈操作后,子进程内核栈变为:

② 最后一个 ret 指令。ret 就是从内核栈中弹出一个 32 位数作为 EIP 并跳转去执行。所以这里的 ret 会弹出 first_return_kernel 的地址并转去执行。

【 first_return_kernel 】

.align 2
first_return_kernel:
    popl %edx
    popl %edi
    popl %esi
    pop %gs
    pop %fs
    pop %es
    pop %ds
    iret

       可以看出 first_return_kernel 函数就是实现用户栈和用户代码的切换,依靠的核心指令是最后一句 iret。iret 之前是执行现场的恢复,主要是一些寄存器的恢复。这里的出栈内容和 copy_process 中入栈的内容是对应的。

③ 一系列出栈操作后,子进程内核栈如下图所示:

④ 最后的 iret 将推入堆栈的段地址和偏移地址弹出,使程序返回到原来发生中断的地方。而子进程内核栈中的 CS ~ SS 就是和父进程一样的内容,所以会切换到父进程的用户栈,并来到父进程用户程序停止时的位置继续往下执行。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统 治军 pdf》是一本由作者治军撰写的关于操作系统的书籍的电子版PDF文件。操作系统是计算机系统中的一个核心组成部分,它负责管理和控制计算机的硬件和软件资源,使得计算机能够高效地运行各种应用程序。 在这本书中,治军详细介绍了操作系统的基本概念、原理和设计原则。他从操作系统的起源和发展历程出发,讲解了多道、分时和实时操作系统等不同类型的操作系统,并深入解析了其内核结构和功能。此外,书中还讨论了进程管理、内存管理、文件系统、输入输出控制等重要的操作系统主题,为读者提供了全面了解和深入学习的机会。 这本书的PDF版本使得读者可以更加便捷地获取其中的内容,无论是在电脑、平板还是手机上都可以进行阅读。通过阅读这本书,读者可以深入了解操作系统的基本原理和相关技术,有助于他们提升对计算机系统的理解和应用。对于学习计算机科学或相关专业的学生和从事软件开发工作的技术人员来说,这本书是一本宝贵的参考资料。 总之,《操作系统 治军 pdf》是一本全面介绍操作系统的书籍的电子版PDF文件,读者可以通过阅读它来深入了解操作系统的原理和技术。这本书的出现为学习和研究操作系统提供了便利,对于对计算机系统有兴趣的人士来说是一本值得阅读的优秀教材。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值