深入理解Linux内核(第三版)- 进程切换

2023-11-15

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。

硬件上下文

尽管每个进程可以拥有自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文(hardware context)。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态堆栈中。

在下面的描述中,我们假定用prev局部变量表示切换出的进程的描述符,next表示切换进的进程的描述符。因此,我们把进程切换定义为这样的行为:保存prev硬件上下文,用next硬件上下文代替prev。

进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已经保存在内核态堆栈上,这也包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。

任务状态段

80x86体系结构包括了一个特殊的段类型,叫任务状态段(Task State Segment,TSS)来存放硬件上下文。尽管Linux并不使用硬件上下文切换,但是强制它为系统中每个不同的CPU创建一个TSS。这样做出于两个目的:

  • 当80x86的一个CPU从用户态切换到内核态时,它就从TSS中获取内核态堆栈的地址。
  • 当用户态试图通过in或out指令访问一个I/O端口时,CPU需要访问存放在TSS中的I/O许可权位图(Permission Bitmap)以检查该进程是否有访问端口的权力。

更确切的说,当进程在用户态下执行in或out指令时,控制单元执行下列操作:

  1. 它检查eflags寄存器中的2位IOPL字段。如果该字段值为3,控制单元就执行I/O指令。否则,执行下一个检查。
  2. 访问tr寄存器以确定当前的TSS和相应的I/O许可权位图。
  3. 检查I/O指令中指定端口在I/O许可权位图中对应的位。如果该位清零,这条I/O指令就执行,否则控制单元产生一个"General protection"异常。

tss_struct结构描述TSS的格式。init_tss数组为系统上每个不同的CPU存放一个TSS。在每次进程切换时,内核都更新TSS的某些字段以便相应的CPU控制单元可以安全地检索到它需要的信息。因此,TSS反映了CPU上的当前进程的特权级,但不必为没有在运行的进程保留TSS。

每个TSS都有它自己的8字节的任务状态段描述符(Task State Segment Descriptor,TSSD)。这个描述符包括指向TSS起始地址的32位Base字段,20位Limit字段。TSSD的S标志位被清零,以表示相应的TSS是系统段。

Type字段置为11或9以表示这个段实际上是一个TSS。在Intel的原始设计中,系统中的每个进程都应当指向自己的TSS,Type字段的第二个有效位叫做Busy位;如果进程正由CPU执行,则该位置1,否则该位置0。在Linux的设计中,每个CPU只有一个TSS,因此,Busy位总置1。

由Linux创建的TSSD存放在全局描述符表(GDT)中,GDT的基地址存放在每个CPU的gdtr寄存器中。每个CPU的tr寄存器包含相应TSS的TSSD选择符,也包含了两个隐藏的非编程字段:TSSD的Base字段和Limit字段。这样,处理器就能直接对TSS寻址而不用从GDT中检索TSS的地址。

thread字段 

在每次进程切换时,被替换进程的硬件上下文必须保存在别处。不能向Intel原始设计那样把它保存在TSS中,因为Linux为每个处理器而不是为每个进程使用TSS。

因此,每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等等这些通用寄存器,它们的值保存在内核堆栈中。

执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数。这里,我们仅关注内核如何执行一个进程切换。

从本质上说,进程切换由两步组成:

1、切换页全局目录以安装一个新的地址空间

2、切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。

switch_to 宏

进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一。

首先,该宏有三个参数,它们是prev,next和last。我们很容易猜到prev和next的作用:它们仅是局部变量prev和next的占位符,即它们是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。

那第三个参数last呢?在任何进程切换中,涉及到三个进程而不是两个。假设内核决定暂停进程A而激活进程B。在schedule( )函数中,prev指向A的描述符而next指向B的描述符。switch_to宏一但使A暂停,A的执行流就冻结。

随后,当内核想再次此激活A,就必须暂停另一个进程C(这通常不同于B),于是就要用prev指向C而next指向A来执行另一个switch_to宏。当A恢复它的执行流时,就会找到它原来的内核栈,于是prev局部变量还是指向A的描述符而next 指向B的描述符。此时,代表进程A执行的内核就失去了对C的任何引用。但是,事实表明这个引用对于完成进程切换是很有用的。

switch_to宏的最后一个参数是输出参数,它表示宏把进程C的描述符地址写在内存的什么位置了(当然,这是在A恢复执行之后完成的)。在进程切换之前,宏把第一个输入参数prev(即在A的内核堆栈中分配的prev局部变量)表示的变量的内容存入CPU的eax寄存器。在完成进程切换,A已经恢复执行时,宏把CPU的eax寄存器的内容写入由第三个输出参数——last所指示的A在内存中的位置。因为CPU寄存器不会在切换点发生变化,所以C的描述符地址也存在内存的这个位置。在schedule()执行过程中,参数last 指向A的局部变量prev,所以prev被C的地址覆盖。

下图1显示了进程A,B,C内核堆栈的内容以及eax寄存器的内容。必须注意的是:图中显示的是在被eax寄存器的内容覆盖以前的prev局部变量的值。

 图1:通过一个进程切换保留对进程C的引用

由于switch_to宏采用扩展的内联汇编语言编码,所以可读性比较差:实际上这段代码通过特殊位置记数法使用寄存器,而实际使用的通用寄存器由编译器自由选择。我们将采用标准汇编语言而不是麻烦的内联汇编语言来描述switch_to宏在80x86微处理器上所完成的典型工作。

1、在eax和edx寄存器中分别保存prev和next的值:

movl prev, %eax
movl next, %edx

2、把eflags和ebp寄存器的内容保存在prev内核栈中。必须保存它们的原因是编译器认为在switch_to结束之前它们的值应当保持不变。

pushfl
pushl %ebp

3、把esp的内容保存到prev->thread.esp中以使该字段指向prev内核栈的栈顶:

movl %esp, 484(%eax)

484(%eax)操作数表示内存单元的地址为eax内容加上484。

4、把next->thread.esp装入esp。此时,内核开始在next的内核栈上操作,因此这条指令实际上完成了从prev到next的切换。由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变当前进程。

movl 484(%edx), %esp

5、把标记为1的地址存入prev->thread.eip。当被替换的进程重新恢复执行时,进程执行被标记为1的那条指令:

movl $1f, 480 (%eax)

6、宏把next->thread.eip的值(绝大多数情况下是一个被标记为1的地址)压入next 的内核栈:

pushl 480(%edx)

7、跳到__switch_to () C函数(见下面):

jmp_ _switch_to

8、这里被进程B替换的进程A再次获得CPU:它执行一些保存eflags和ebp寄存器内容的指令,这两条指令的第一条指令被标记为1。

1:
    popl %ebp
    popfl

注意这些pop指令是怎样引用prev进程的内核栈的。当进程调度程序选择了prev作为新进程在CPU上运行时,将执行这些指令。于是,以prev作为第二个参数调用switch_to。因此,esp寄存器指向prev的内核栈。

9、拷贝eax寄存器(上面步骤1中被装载)的内容到switch_to宏的第三个参数last标识的内存区域中:

movl %eax, last

正如先前讨论的,eax寄存器指向刚被替换的进程的描述符(当前执行的schedule()函数重新使用了prev局部变量,于是汇编语言指令就是: movl %eax , prev)


参考文献:《深入理解Linux内核(第三版)》  中国电力出版社

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解Linux内核(第三版)- 进程切换 的相关文章

  • iptables通过注释删除特定规则

    我需要删除一些具有相同评论的规则 例如 我有带有 comment test it 的规则 所以我可以像这样获得它们的列表 sudo iptables t nat L grep test it 但是我怎样才能删除所有带有注释 测试它 的 PR
  • jpegtran 优化而不更改文件名

    我需要优化一些图像 但不更改它们的名称 jpegtran copy none optimize image jpg gt image jpg 但是 这似乎创建了 0 的文件大小 当我对不同的文件名执行此操作时 大小仍然完全相同 怎么样 jp
  • Linux 中什么处理 ping?

    我想覆盖 更改 linux 处理 ping icmp echo 请求数据包的方式 这意味着我想运行自己的服务器来回复传入的 icmp 回显请求或其他 数据包 但为了使其正常工作 我想我需要禁用 Linux 的默认 ping icmp 数据包
  • 如何通过保持目录结构完整来同步路径中匹配模式的文件?

    我想将所有文件从服务器 A 复制到服务器 B 这些文件在不同级别的文件系统层次结构中具有相同的父目录名称 例如 var lib data sub1 sub2 commonname filetobecopied foo var lib dat
  • C语言中如何通过内存地址映射函数名和行号?

    如何用 GCC 中的内存地址映射回函数名称和行号 即假设一个 C 语言原型 void func Get the address of caller maybe this could be avoided MemoryAddress get
  • SSE:跨页边界的未对齐加载和存储

    我在页面边界旁边执行未对齐加载或存储之前读过某处 例如使用 mm loadu si128 mm storeu si128内在函数 代码应首先检查整个向量 在本例中为 16 个字节 是否属于同一页 如果不属于同一页 则切换到非向量指令 我知道
  • 将 jar 作为 Linux 服务运行 - init.d 脚本在启动应用程序时卡住

    我目前正在致力于在 Linux VM 上实现一个可运行的 jar 作为后台服务 我已经使用了找到的例子here https gist github com shirish4you 5089019作为工作的基础 并将 start 方法修改为
  • 我的线程图像生成应用程序如何将其数据传输到 GUI?

    Mandelbrot 生成器的缓慢多精度实现 线程化 使用 POSIX 线程 Gtk 图形用户界面 我有点失落了 这是我第一次尝试编写线程程序 我实际上并没有尝试转换它的单线程版本 只是尝试实现基本框架 到目前为止它是如何工作的简要描述 M
  • 查找哪个程序运行另一个程序

    我有一个 NAS 运行在 Redhat Linux 的有限版本上 我按照指示破解了它 这样我就可以访问 shell 这很有帮助 我还做了一些修改 其他人也做过修改 除了一个问题之外 它们似乎都工作得很好 不知何故 每隔 22 天 系统就会关
  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute
  • 内核模式下的线程(和进程)与用户模式下的线程(和进程)有什么区别?

    我的问题 1 书中现代操作系统 它说线程和进程可以处于内核模式或用户模式 但没有明确说明它们之间有什么区别 2 为什么内核态线程和进程的切换比用户态线程和进程的切换花费更多 3 现在 我正在学习Linux 我想知道如何在LINUX系统中分别
  • 当 grep "\\" XXFile 我得到“尾随反斜杠”

    现在我想查找是否有包含 字符的行 我试过grep XXFile但它暗示 尾随反斜杠 但当我尝试时grep XXFile没关系 谁能解释一下为什么第一个案例无法运行 谢谢 区别在于 shell 处理反斜杠的方式 当你写的时候 在双引号中 sh
  • MySQL 中的创建/写入权限

    我的设备遇到一些权限问题SELECT INTO OUTFILE陈述 当我登录数据库并执行简单的导出命令时 例如 mysql gt select from XYZ into outfile home mropa Photos Desktop
  • 如何减去两个 gettimeofday 实例?

    我想减去两个 gettimeofday 实例 并以毫秒为单位给出答案 这个想法是 static struct timeval tv gettimeofday tv NULL static struct timeval tv2 gettime
  • Urwid:使光标不可见

    我正在使用 urwid 它是一个用于在 ncurses 中设计终端用户界面的 Python 框架 但有一件事我在 urwid 中无法做到 而这在 Curses 中很容易做到 使光标不可见 现在 选择按钮时光标是可见的 而且看起来很丑 有办法
  • 在 Mac OSX 上交叉编译 x86_64-unknown-linux-gnu 失败

    我尝试将我的 Rust 项目之一编译到 x86 64 unknown linux gnu 目标 cargo build target x86 64 unknown linux gnu Compiling deployer v0 1 0 fi
  • Python 脚本作为 Linux 服务/守护进程

    Hallo 我试图让 python 脚本作为服务 守护进程 在 ubuntu linux 上运行 网络上存在多种解决方案 例如 http pypi python org pypi python daemon http pypi python
  • 限制 Imagemagick 使用的空间和内存

    我在 Rails 应用程序上使用 Imagemagick 使用 rmagick 但我的服务器 Ubuntu 不是很大 当我启动转换进程时 Imagemagick 占据了我的服务器 30GB HDD 的所有位置 内存 我想限制内存和 tmp
  • Linux 上的 RTLD_LOCAL 和dynamic_cast

    我们有一个由应用程序中的一些共享库构成的插件 我们需要在应用程序运行时更新它 出于性能原因 我们在卸载旧插件之前加载并开始使用新插件 并且只有当所有线程都使用旧插件完成后 我们才卸载它 由于新插件和旧插件的库具有相同的符号 我们dlopen
  • Fedora dnf 更新不起作用?

    当我尝试使用 update 命令更新 Fedora 22 时 sudo dnf update 我收到以下错误 错误 无法同步存储库 更新 的缓存 无法准备内部镜像列表 Curl 错误 6 无法解析主机名 无法解析主机 mirrors fed

随机推荐

  • 文件管理.

    1 touch 创建测试用的空文件修改文件的时间戳记 格式 touch 选项 文件名 2 echo 创建文件并编辑内容 格式 echo 123 gt 111 3 dd转换和拷贝文件 格式 dd if 拿取容量的文件名 of 要创建的文件名
  • 多路查找树——2-3树和2-3-4树

    目录 2 3树定义 2 3树的插入 2 3树的删除 PS 2 3 4树定义 2 3 4树插入 2 3 4树删除 PS 2 3树定义 定义 多路查找树 其中每一个结点都具有两个孩子 称为2结点 或三个孩子 称为3结点 所有叶节点都在树结构的同
  • IBM MQ开发通用方法,包括客户端连接、服务器端连接、发送接受消息

    1 接口方法 IQueueManager java author weiya public interface IQueueManager 发送消息 param b param queueName roseuid 447BE52F01C2
  • cs寄存器 x86 特权模式_segmentation和保护模式(二)

    segmentation和保护模式 一 上文讲到了segment descriptor 把这些descriptors放在一起 在内存里连续分布 就构成了GDT Global Descriptor Table 所以GDT也可以被称为段 描述符
  • 总离差平方和推导公式

    总离差平方和推导
  • 使用股票程序交易系统应该注意哪些问题?

    尽管使用了程序交易系统 但交易者应该明白 交易的主体是人而不是程序交易系统 交易系统不过是贯彻交易者的思想 执行了交易者的指令而已 交易者仍是交易的主体 这一点不因使用了程序交易系统而改变 交易系统有其高峰期和低谷期 交易系统从大类来分可分
  • stream新特性

    package com jeethink system domain public class Employee public Integer age public String name public Integer getAge ret
  • 十几行代码就可以让你的微信小程序挂掉

    mpvue github 地址请参见 是一个使用Vue js 开发小程序的前端框架 框架基于 Vue js核心 mpvue 修改了 Vue js 的runtime 和compiler 实现 使其可以运行在小程序环境中 从而为小程序开发引入了
  • HTML5

    文章目录 前言 滚动长画幅 实现细节 语义化标签 语言的本地化 前言 本文将分析 AirPods Pro 产品介绍使用的技巧与有趣的第三方库 滚动长画幅 这次AirPods Pro 的产品介绍以一个由用户手动进行滚动推进的长画幅组成 这个长
  • day02-HTML5列表/表格/媒体元素/结构元素

    0目录 补充知识点 HTML5列表 HTML5表格 HTML5媒体元素 HTML5结构元素 1 行内元素和块元素 行内元素 不独占一行 例如 a 标签 strong标签 em标签 块级元素 独占一行 例如 p 标签 h1 h6标签 2 HT
  • Python爬虫系列之爬取猫眼电影,没办法出门就补一下往期电影吧

    前言 今天给大家介绍利用Python爬取并简单分析猫眼电影影评 让我们愉快地开始吧 开发工具 Python版本 3 6 4 相关模块 requests模块 pyecharts模块 jieba模块 scipy模块 wordcloud模块 以及
  • 运行Pangolin时提示以下错误: terminate called after throwing an instance of 'std::runtime_error'

    在运行Pangolin时提示以下错误 terminate called after throwing an instance of std runtime error what Pangolin X11 Unable to retrieve
  • 增强现实代码+注释解析(三)

    1 书名 Mastering OpenCV with Practical Computer Vision Projects 2 章节 Chapter 3 Marker less Augmented Reality 3 书中源代码的最新更新可
  • CustomEditor+ScripableObject 简单用法

    写在前面 看了一整天 算是明白了点 记录一下 要是不知道怎么入门可以看一下 希望能帮到您 Ps 本文一律采用c 进行讲解 用途 自定义inspector 监视器 面板 举个例子 你在ScriptableObejct里声明了一个string类
  • Linux安装——VMware + RedHat

    文章目录 1 安装VMware虚拟机 2 安装RedHat红帽系统 2 1 虚拟机设置 2 2 开启虚拟机 3 cannot updata read only repo 3 1 删除自带yum包 3 2 下载centos版本yum包替换 3
  • Mysql 5.7 / 5.8 性能测试

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 转载于 https my oschina net u 582827 blog 1802981
  • Python训练了个模型,怎么交给Java用呢?

    最近碰到几个人问 如何实现 java 调用他们写好的 Python 应用 模型 这里我就把几种常见的办法做下汇总整理 喜欢本文记得收藏 关注 点赞 注 文末提供技术交流群 推荐文章 李宏毅 机器学习 国语课程 2022 来了 有人把吴恩达老
  • 【应用层】DNS协议

    一 概述 本篇文章基于 计算机网络 和 计算机网络 自顶向下方法 为笔者的读书笔记 主要内容如下所示 DNS提供的服务 互联网的域名结构 DNS服务器的分布 DNS的工作原理 DNS记录 往DNS插入记录 二 DNS提供的服务 域名系统 D
  • (二十九)admin-boot项目之自定义全局拦截404异常

    二十九 自定义全局拦截404异常 项目地址 https gitee com springzb admin boot 如果觉得不错 给个 star 简介 这是一个基础的企业级基础后端脚手架项目 主要由springboot为基础搭建 后期整合一
  • 深入理解Linux内核(第三版)- 进程切换

    为了控制进程的执行 内核必须有能力挂起正在CPU上运行的进程 并恢复以前挂起的某个进程的执行 这种行为被称为进程切换 process switch 任务切换 task switch 或上下文切换 context switch 硬件上下文 尽