在看Linux内核的时候发现,CPU自己认得(或者说is
expecting)很多struct,很多时候内核要做的事情是在内存里准备好这些struct里CPU需要的数据,以供CPU完成相应的任务。比如寻址中的paging部分,内核只需要把page directory中的数据准备好,并把page
directory的地址放入cr3,CPU自己就能根据page
directory中的数据进行寻址。就像一种契约,CPU对struct的期望,正是内核所要做的事情,反过来说,内核要做的事情仅仅是满足CPU的期望而已。
不知读者是否与我有同感,但对于我而言,这使得写操作系统突然变得远远不如想象中那么困难了。因为困难的地方在底层,在硬件。这正是学编程的世界,
没学之前,你永远觉得编程是不可能的事情——如果刚刚学会了C的语法,你会觉得,C里头把数据在内存里移来移去,
加加减减,明明是只能让小孩子玩过家家的东西,怎么就可以在屏幕上画画?让机器做事?后来意识到了好多好多的库,原来自己只需要调用API就好了,那
API的那一边又是怎么实现的呢?终于知道API里面是怎么实现的了,却发现这些实现永远也只是在调用另外一层API,只不过更为底层的API。往地里越钻越深,穿越一层又一层的API,才发现最终不过是在为硬件的期望准备内存中的数据。当然这样的描述忽略了同时在底层我们也发出了汇编指令让机器去做一些除了操作内存加加减减的事情,但硬件才是生命自身,它的电路决定了它如何理会指令、中断和各种事件,如何突然不执行我们(比如,当前用户进程)给它的下一
个指令,突然知道利用内存中的数据去进行上下文转换,如此等等。
其实上面这番话也可以反过来说。每当我们的知识前进一步,学的更深了,回头望去,我们承学的东西,不过是一层API,一层界面罢了。
一点感想,下面进入正题,这次的笔记是讲述Process的切换:
TSS
先介绍一下对80x86的hardware context switch很重要的TSS结构。
Task State Segment
A task gate descriptor provides an indirect, protected reference to a Task State Segment.
The Task State Segment is a special x86 structure which holds
information about a task. It is used by the operating system kernel for
task management. Specifically, the following information is stored in
the TSS:
* Processor register state
* I/O Port permissions
* Inner level stack pointers
* Previous TSS link
All this information should be stored at specific locations within the TSS as specified in the IA-32 manuals.
在Linux低版本中,进程切换仅仅需要far jmp到要切换的进程的TSS的selector所在就可以了。(far jmp除了修改eip还修改cs)。
在Linux
2.6当中,TSS保存在每CPU一个的GDT(其地址存在gdtr中)中,虽然每个process的TSS不同,但Linux 2.6却不利用其中的
hardware context switch以一个far jmp来实现任务转换,而用一系列的mov指令来实现。这样做的原因是:
1、可以检验ds和es的值,以防恶意的forge。
2、硬转换和软转换所用时间相近,而且硬转换是无法再优化的,软转换则可以。
Linux 2.6对TSS的使用仅限于:
1、User Mode向Kernel Mode切换的时候,从TSS中获取Kernel Stack。
2、User Mode使用in或者out指令的时候,用TSS中的 I/O port permission bitmap验证权限.
有一点要注意,process switching是发生在Kernel Mode,在转为Kernel
Mode的时候,用户进程使用的通用register已经保存在Kernel
Stack上了。然而非通用的register,如esp,由于不能放在TSS中,所以是放在task_t中的一个类型为thread_struct的
thread字段中。
process切换两部分:切换paging这里不讲,切换kernel stack、hardware context是由switch_to宏完成的。
switch_to宏中的last
switch_to宏的任务就是让一个process停下来,然后让另外一个process运行起来。
switch_to(prev, next, last)。prev、next分别是切换前后的process的process descriptor(task_t)的地址。last的存在要解释一下:
由于switch_to中造成了进程的切换,所以其中前半部分指令在prev的语境(context、Kernel Stack)中执行,后半部分却在next的语境中执行。
假设B曾切换为O,那么由于一切换,B就停下来了,所以在B的感觉保持是next为O,prev为B。当我们要从A切换到B的时候,一切换B就醒了,但它却仍然以为next是O,prev是B,就不认识A了。然而A switch_to B中的后半部分却需要B知道A。
因此这个宏通常都是这么用的:switch_to(X, Y, X)。
switch_to详解
书上认为直接看pseudo的汇编代码比较好,我却觉得直接看Linux源代码中的inline汇编代码更为自在(为了阅读方便和语法高亮有效,却掉了原代码中宏定义的换行,想查看原来的代码,请访问http://lxr.linux.no/linux+v2.6.11/include/asm-i386/system.h#L15
):
#define switch_to(prev,next,last)
do {
unsigned long esi,edi;
asm volatile("pushfl\n\t"
"pushl %%ebp\n\t"
"movl %%esp,%0\n\t" /* save ESP */
"movl %5,%%esp\n\t" /* restore ESP */
"movl $1f,%1\n\t" /* save EIP */
"pushl %6\n\t" /* restore EIP */
"jmp __switch_to\n"
"1:\t"
"popl %%ebp\n\t"
"popfl"
:"=m" (prev->thread.esp),"=m" (prev->thread.eip),
"=a" (last),"=S" (esi),"=D" (edi)
:"m" (next->thread.esp),"m" (next->thread.eip),
"2" (prev), "d" (next));
} while (0)
简单解说一下这里用到的gcc的inline汇编语法。首先看上去像是汇编代码的自然就是汇编代码了,每个指令写到一对""中(这是换行接着写同一个
string的好办法)还要加\n\t实在是比较麻烦但还算清晰可读。如果熟悉AT&T的汇编语法,读起来不是难事。
第一个冒号后面有很多类似于"=m"
(prev->thread.esp)的东东以逗号相隔,这些是这段汇编所输出的操作数,=表达了这个意思。其中m代表内存中的变量,a代
表%eax,S代表%esi,D代表%edi。但"=m"
(prev->thread.esp)和"=a"(last)是完全不同的输出方向,前者在movl
%%esp,%0一句中(%0代表了prev->thread.esp)把%esp的内容输出给了prev->thread.esp,后者则
独立成句,直接在整段汇编的最后自动将last的值写到%eax,完成了last的使命。
第二个冒号后面的则是输入给这段汇编的操作数。其中d代表%edx。2代表了prev的值将与%2(也就是"=a"(last))共用一个寄存器。
这些操作数在汇编中以%n(n是数字)的形式引用,输出和输入站在一个队里报数:输出的第一个是%0,顺次递增,到了"m" (next->thread.esp)就排到了%5,依此类推。
本来还应该有一个冒号,用来告诉编译器会被破坏的寄存器(因为笨笨的C编译器认为只有他自己在改寄存器,常常自作主张作出假设进行优化)。这里中途
在jmp
__switch_to我们的确破坏过%eax,但我们巧妙地改回来了(看下面),我们也破坏了%ebp和eflags,但我们通过一对push和pop
却也恢复了它们。因此我们不需要告诉编译器我们改过,因为我们改回来了。
asm后面的volatile是告诉C编译器不要随便以优化为理由改变其中代码的执行顺序。
还有一个地方需要解释,那就是$1f,这个指的是标号为1的代码的起始地址。在"1:\t"这一行我们定义了这个标号。
如果对gcc的inline汇编产生了兴趣,参见:http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s5
下面开始详细分析:
/* 首先,我们在prev的语境中执行 */
/* 保存ebp和eflags于prev的Kernel Stack上 */
pushfl
pushl %ebp
%esp => prev->thread.esp /*保存了prev的esp */
next->thread.esp => %esp /*读出next之前保存的esp。这个时候,由于esp被改成了next的Kernel Stack,而标示process的thread_info挨着esp(参见笔记process-1中的对current的解释),我们现在实际变成了是在进程next的语境中执行了。不过我们还没有真正开始执行next自己的代码,且看下面 */
1 => prev->thread.eip /*把标号为1的代码的地址存入prev->thread.eip中,以备将来恢复。如果有人不知道,说明一下:CPU的eip寄存器中放的是CPU要执行的下一行代码的地址 */
/* 正是下面这两句的巧妙配合使得这两句执行完后,CPU完完全全跑去执行next代码,不再执行后面的代码。这也正是原书没有讲清楚的(过于分散了),各位读者注意咯!*/
pushl next->thread.eip /* 把原先保存下来的next的下一条指令地址,push到next的Kernel stack顶部。这个next->thread.eip通常储存的是next被切换之前push进stack的那个标号为1的代码地址(简称:next的1),但如果next从未被切换过,即是一个刚被fork了、新开始执行的进程,那么存在next->thread.eip中的就是 ret_from_fork()函数的起始地址。 */
jmp __switch_to /* __switch_to是一个用寄存器来传达参数的函数,里面执行了检查、保存FPU、保存debug寄存器等琐事。重点是:__switch_to是一个函数!这里居然用的是jmp而不是call!这正是巧妙之处。__switch_to()作为一个函数执行完了之后会返回(ret),但由于我们不是call它的(call 会自动把下一条指令的地址push入stack顶部,相应地返回的时候ret会从stack的顶部获取返回地址——下一条指令的地址,这是一种完美的配合),ret就把上一句push入stack顶部的next->thread.eip当作下一条指令了,于是我们就自然而然地顺着next之前执行的地址执行下去了,直到下一次process切换回来。 */
/* .......下面的代码不会继续执行......直到进程切换回来然后跳到prev的1 */
1:
popl %ebp
popfl
/* 到这里这个宏就结束了,所以就会顺着执行prev的接下来的代码。这也正是为什么我们之前把prev的1的地址push进stack就可以达到回到prev自己的代码的原因。 */
这篇笔记不会解释__switch内部琐屑的细节了,因为最神奇的事情不是发生在里面,人生苦短,不用去琢磨过于琐屑的事情。
分享到:
相关推荐
华清远见-Linux2.6内核标准教程-高清扫描版part2
linux-2.6.15.5--kgdb-2.4.rar 含有 linux-2.6.15.5-kgdb-2.4.tar.bz2 kgdb_docu_full-2.4.pdf kgdb_full_2.2.pdf kgdbquickstart-2.4.pdf
Linux 2.6内核的Fair-Share调度算法是一种旨在实现资源公平分配的调度策略,主要目的是确保系统中的所有进程都能获得相对平等的CPU执行时间。这种算法在多任务环境中尤其重要,因为它防止了某个高优先级或长时间运行...
1. **模块化驱动**:Linux 2.6内核支持动态加载和卸载驱动模块,这使得不需要在编译内核时就包含所有可能用到的驱动,而是根据需要加载相应的驱动模块。 2. **通用设备模型**:内核引入了通用设备模型,它统一了...
1. **文件系统**:Linux 2.6内核支持多种文件系统,如EXT2、EXT3、EXT4、XFS、JFS等。这些文件系统各有优缺点,例如EXT4引入了日志记录、预分配和大文件支持,提高了性能和可靠性。文件系统的实现涉及挂载、卸载、...
3. **I/O调度**:Linux内核中的I/O调度器负责决定何时以及如何读写磁盘。不同的调度策略(如电梯算法、Deadline、NOOP等)会影响系统的响应时间和吞吐量。书中会阐述这些调度器的工作原理及其优缺点。 4. **内存...
### Linux2.6 内核的 Initrd 机制解析 #### 深入理解Initrd技术 Initrd,全称Init RAM Disk,是Linux启动过程中一个关键的技术环节,尤其是在Linux2.6内核中,其机制与早期版本如2.4内核有了显著的变化。本文旨在...
《Linux2.6内核标准教程》适合Linux内核爱好者、Linux驱动开发人员、Linux系统工程师参考使用,也可以作为计算机及相关专业学生深入学 习操作系统的参考书。 引用: 目录 第1章 Linux内核学习基础 1 1.1 为什么...
"LINUX2.6内核makefile详解" Linux 2.6 内核 Makefile 详解是 Linux 内核开发中非常重要的一部分。Makefile 是一个脚本文件,用于描述如何编译和构建 Linux 内核。该文件是 Linux 内核开发的核心组件之一,对开发...
commons-lang-2.6.jar包commons-lang-2.6.jar包commons-lang-2.6.jar包commons-lang-2.6.jar包commons-lang-2.6.jar包加源码
2. 许可证声明:在Linux2.6内核中,驱动程序需要声明许可证信息,使用GPL MODULE_LICENSE("Dual BSD/GPL");宏,老版本使用MODULE_LICENSE("GPL");宏。 3. 模块参数:在Linux2.6内核中,驱动程序的参数需要显式地...
### 存储技术原理分析:基于Linux_2.6内核源代码 #### 一、存储技术概述 存储技术是计算机系统中一个重要的组成部分,它主要用于数据的持久化保存。随着信息技术的发展,存储技术也在不断地演进和发展。本文将重点...
LINUX 2.6内核标准教程(华清远见,河秦)(高清PDF共218M)10/10
### Linux 2.6 内核机制之 Initrd 技术深度解析 #### 一、引言 在深入探讨Linux 2.6内核中Initrd(Initial RAM Disk)技术的具体实现之前,有必要先理解Initrd的基本概念及其重要性。Initrd在Linux系统启动过程中...
7. **内核设置编辑器的演变**:Linux 2.6内核引入了新的图形化设置编辑器,使得配置更直观,减少了以前的config、oldconfig、menuconfig和xconfig等命令行或文本界面的复杂性。 8. **软件工具支持**:工具如...
Linux 2.6.32.2 Mini2440是针对ARM架构的嵌入式开发板设计的一个轻量级操作系统内核版本。这个压缩包文件"linux-2.6.32.2-mini2440-20110413.tar.gz"包含了与2011年4月13日发布相关的源代码,适用于Mini2440开发板。...
《Linux 2.6内核标准教程》是一本专为Linux内核爱好者、驱动开发者和系统工程师设计的深度解析书籍。它旨在帮助读者理解和掌握Linux内核的核心工作原理,通过详细解析内核的关键组件,引领读者进入Linux内核的世界。...
《Linux2.6内核标准教程》适合Linux内核爱好者、Linux驱动开发人员、Linux系统工程师参考使用,也可以作为计算机及相关专业学生深入学 习操作系统的参考书。 引用: 目录 第1章 Linux内核学习基础 1 1.1 为什么...
"Linux 2.6内核测试及其到ARM嵌入式平台的移植" 本文将深入探讨Linux 2.6内核对嵌入式应用的影响,及其到ARM嵌入式平台的移植。Linux 2.6内核相对以前的Linux内核在可配置性和实时性方面有了很大的改进,特别是在...