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

news2024/9/21 2:33:44

目录

【前言】

代码分析

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 就是和父进程一样的内容,所以会切换到父进程的用户栈,并来到父进程用户程序停止时的位置继续往下执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/594679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MyBatis——缓存

是一种临时存储少量数据至内存或者是磁盘的一种技术.减少数据的加载次数,可以降低工作量,提高程序响应速度 缓存的重要性是不言而喻的。mybatis的缓存将相同查询条件的SQL语句执行一遍后所得到的结果存在内存或者某种缓存介质当中&#xff0c;当下次遇到一模一样的查询SQL时候…

Yolov8涨点神器:ODConv+ConvNeXt提升小目标检测能力

1.涨点神器结合,助力YOLO 1.1 ICLR 2022涨点神器——即插即用的动态卷积ODConv 论文:Omni-Dimensional Dynamic Convolution 论文地址:Omni-Dimensional Dynamic Convolution | OpenReview ODConv通过并行策略引入一种多维注意力机制以对卷积核空间的四个维度学习更灵活的…

项目所需的Python版本+国内源+虚拟环境+用Conda创建环境

文章目录 &#xff08;一&#xff09;Python&#xff08;1.1&#xff09;国内源&#xff08;1.2&#xff09;设置源&#xff08;全局&#xff09;&#xff08;1.3&#xff09;使用源&#xff08;单次&#xff09; &#xff08;二&#xff09;Python环境&#xff08;2.1&#xf…

【数据结构与算法】力扣:栈和队列(一)

1 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾 int pop() 从队列的开头移除并返回元素 int peek() 返回队列开…

vue diff算法与虚拟dom知识整理(13) 手写patch子节点更新换位策略

上一文中我们编写了 patch中新旧节点都有子节点的 插入节点的逻辑 但旧节点的子节点发生顺序 或数量变化 我们还没有处理 那我们现在继续 我们先来看看 原本是怎么写的 我们打开我们的案例 找到 node_modules 下面的snabbdom/src下面的 init.ts文件 我们在里面找到一个 updat…

Openai+Deeplearning.AI: ChatGPT Prompt Engineering(六)

想和大家分享一下最近学习的Deeplearning.AI和openai联合打造ChatGPT Prompt Engineering在线课程.以下是我写的关于该课程的前五篇博客&#xff1a; ChatGPT Prompt Engineering(一)ChatGPT Prompt Engineering(二)ChatGPT Prompt Engineering(三)ChatGPT Prompt Engineering…

QML Text 元素

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 在 QML 中,Text 和 Label 是两种常用的元素(Label 继承于 Text),用于显示文本内容。虽然它们看起来很相似,但在使用和功能上有一些不同。 对于 Text 元素,比较重要的是 text 与 font 属性,一旦指定了…

Linux常见IO模型-3

在前两篇博客中&#xff0c;我们讲述了常见的IO模型&#xff0c;并对多路转接模型中的select模型进行了细致介绍和具体的代码实现。那么接下来&#xff0c;我们在本篇博客中将对剩余的多路转接模型中的&#xff1a;poll和epoll进行介绍。 目录 1.poll 1.1操作流程 1.2代码实…

硬件系统工程师宝典(27)-----MOSFET、BJT搭配,干活不累

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。上篇我们说到场效应管的原理是通过“监测”栅极和源极间电压来实现控制漏极-源极之间电流的目的。今天我们来讲讲场效应管如何和BJT搭配设计控制电路…

【P44】JMeter 模块控制器(Module Controller)

文章目录 一、模块控制器&#xff08;Module Controller&#xff09;参数说明二、测试计划设计 一、模块控制器&#xff08;Module Controller&#xff09;参数说明 提供了一种在运行时将测试计划片段替换为当前测试计划的机制&#xff1b;模块控制器的目标是为增加脚本的复用…

springboot+vue实验室预约设备报修管理系统

本实验室管理系统管理员功能有个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;公告信息管理&#xff0c;知识库管理&#xff0c;实验课程管理&#xff0c;实验室信息管理&#xff0c;实验室预约管理&#xff0c;实验设备管理&#xff0c;采购记录管理&#xff0…

addon.MediaStream,erizo::MediaStream 还有addon.MediaXXX,erizo::MediaXXX

9. MediaStream 和erizo::MediaStream 类的继承关系 erizo::MediaStream source/agent/webrtc/rtcConn/erizo/src/erizo/MediaStream.h source/agent/webrtc/rtcConn/erizo/src/erizo/MediaDefinitions.h erizo::MediaSource source/agent/webrtc/rtcConn/erizo/src/erizo/…

【C++进阶4-AVLTree】尽可能条理清晰地为你讲解比普通BST更强的——AVLTree

今天&#xff0c;带来AVLTree的讲解。文中不足错漏之处望请斧正&#xff01; 是什么 AVLTree是一种自平衡的二叉搜索树。 它通过控制左右子树的高度差不超过1来调节平衡&#xff0c;从而提高搜索&#xff0c;插入和删除的效率。 实现 结构 AVLTree为了能自动调节平衡&#…

第十四章行为性模式—策略模式

文章目录 命令模式解决的问题结构实例存在的问题适用场景 JDK源码解析 行为型模式用于描述程序在运行时复杂的流程控制&#xff0c;即描述多个类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务&#xff0c;它涉及算法与对象间职责的分配。行为型模式分为类行为模式…

Java阶段三Day07

Java阶段三Day07 文章目录 Java阶段三Day07Spring Security自定义UserDetails密码加密授权 JDK包结构文档注释规范JDK APIString APIString是不可变对象String常量池内存编码及长度使用indexOf实现检索使用substring获取子串trim去除两侧空白charAtstartsWith和endsWith大小写变…

3.1 矩阵连乘问题

博主简介&#xff1a;一个爱打游戏的计算机专业学生博主主页&#xff1a; 夏驰和徐策所属专栏&#xff1a;算法设计与分析 学习目标&#xff1a; 如果我要学习动态规划中的矩阵连乘问题&#xff0c;我会采取以下学习方法&#xff1a; 1. **理解问题的背景和目标&#xff1a;首…

pytorch中分布式Collective通信API学习

随着各种大模型的不断迭出&#xff0c;感觉现在的NLP领域已经来到了大模型的时代。那么怎么来训练一个大模型呢&#xff1f;其中比较关键的一个技术基础就是分布式训练。在阅读GLM源码的时候&#xff0c;感觉其中的分布式训练代码不是很熟悉&#xff0c;看起来有点吃力&#xf…

【Nature 2023】Computational approaches streamlining drug discovery

今天为大家介绍的是一篇发表于“Nature”上面的综述文章“Computational approaches streamlining drug discovery”&#xff0c; Computer-aided drug discovery has been around for decades, although the past few years have seen a tectonic shift towards embracing com…

马尔科夫链 Markov chain

马尔科夫链 1.实例参考文献 马尔科夫链(Markov chain)及其模型在机器学习中应用广泛&#xff0c;本文结合一些参考资料做一个总结。 注意的是&#xff0c;本文中提到的马尔科夫链&#xff0c;要与隐马尔科夫(HMM)作以区分。 1.实例 设某人的训练计划中有以下四项运动&#x…