浅析Linux操作系统工作的基础
环境:lubuntu 13.04 kernel 3.9.7
作者:SA12226265 katao
简介: 本文根据 Linux™ 系统工作基础的分析,对存储程序计算机、堆栈(函数调用堆栈)机制和中断机制进行概述。文中将为您提供操作系统(内核)如何工作的细节,进一步从宏观概述结合关键点进行微观(CS:EIP、EBP/ESP等的变化)分析。
一、存储程序计算机
首先让我们了解一下,什么是存储程序计算机,并对存储程序计算机的整个运行过程及所需的硬件组件进行简单介绍
上图是程序存储计算机的物理框架,主要包含CPU(包含各类寄存器,如程序寄存器,指令寄存器等),主存,I/O设备,一个最简单的的程序存储计算机只需要以下部件来完成计算机工作:
– 主存,也就是我们普通PC上内存,用于存储指令和数据
– 处理器, 用于执行算术和逻辑操作
– 控制单元, 解析需要操作的指令集
程序存储在计算机主存当中,并以数据的形式被CPU访问和读写,程序中各条指令都被获取并放到一个EIP寄存器,EIP寄存器中数据控制整个处理单元的运行,取“下一条”指令,继续运行
PC = 程序计数器
IR = 指令寄存器
MAR = 存储器地址寄存器
MBR = 存储器缓冲寄存器
在Linux系统中,一般同时会有几个程序一起运行,运行过程中这些程序的都存储在主存中,而CPU只会在同一时间内运行其中优先级较高的某一个,并根据优先级顺序不断的切换多个进程运行,使得计算机操作者会有多个程序同时运行的错觉。
在存储程序计算机中,最重要的部分就是多个进程的切换,是什么控制着进程间的切换,如何保证进程切换过程中能够使得多个进程运行时不发生混乱,这一切都是由Linux内核控制的,下面我们深入解析Linux内核的在进程切换时的工作机制。
先看调度的方式。
由于调度时机发生时进程在进入了内核态这样,内核必须等待该进程即将结束内核态时才进行切换操作,而进程如果正在用户态时则切换工作会立即执行,所以,一般进程调度发生在当前进程从内核态(包括从系统调用而进入内核态)返回用户态的前夕。至于调度的政策,均按照前面所提到的以优先级为基础的调度。
针对不同的进程有不同的调度政策,主要有SCHED_FIFO,SCHED_RR, SCHED_OTHER(源码集中在kernel\sched 目录下),其中FIFO适用于时间性要求比较高的进程,而RR针对时间片耗尽的进程,由于没有研究过源码这里不做详细描述。
当切换进程已经选好后,就开始用户虚拟空间的处理,然后就是进程的切换switch_to()。所谓进程的切换主要就是堆栈的切换,这是由宏操作switch_to()完成的,定义于\linux-3.9.7\arch\x86\include\asm\switch_to.h中:
#define switch_to(prev, next, last) \
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
unsigned long ebx, ecx, edx, esi, edi; \
\
asm volatile("pushfl\n\t" /* save flags */ \
"pushl %%ebp\n\t" /* save EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
"pushl %[next_ip]\n\t" /* restore EIP */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while ()
这里的输出部分有三个参数,表示这段程序执行后有三项数据会有改变。其中[prev_sp]、[prev_ip] 都在内存中分别为prev->thread.sp、prev->thread.ip,而最后一个参数则与寄存器EAX结合,对应于参数中的last。而输入部则有4个参数,其中[next_sp]、[next_ip]在内存中,分别为next->thread.sp 与next->thread.ip,剩余的两个参数则与寄存器EAX,EDX结合,分别对应prev,next。
先看开头有两条push指令和结尾处有两条pop指令,再看14行将当前的esp,也就是当前进程的prev的内核态的堆栈指针存入prev->thread.sp,第15行又将新收到调度要进入运行的进程next的内核态的堆栈之争next->thread.sp置入esp。这样一来,CPU在第15行与第16行这两条指令之间就已经切换了堆栈。假定我们有A,B两个进程,在本次切换中prev指向A,而next指向B。也就是说,在本次切换中A为要“调离”的进程,而B为要“切入”的进程。那么,在这里的第12到15行是在使用A的堆栈,而从第16行开始就是在用B的堆栈了。换言之,从第16行开始,“当前进程”,已经是B而不是A了。在内核代码中当需要访问当前进程的task_struct结构时使用的指针current时实际上是宏定义,它根据当前的堆栈指针的ESP计算出所需的地址。如果第16行处引用current的话,那就是已经指向B的task_struct结构了。所以进程切换其实在第15行指令执行完就已经完成了。但是,构成一个进程的另一个要素是程序的执行,所以还要进行其他步骤。由于12,13行事push进A的堆栈,而在 21行至22行从B的堆栈中POP出来,本质就是恢复新切入的进程在上一次被调离时的push进堆栈的内容。理论上了,进程的切换过程中,多个进程都已经在执行了只是暂时的撤离cpu,所以切换过程就是在堆栈之间进程切换。
那么如何完成程序执行的切换,看一下之后的16行至20行。第16行的[prev_ip]所在位置,实际上就是将第21行的pop指令所在的地址保持在prev->thread.ip中,作为进程A下一次被调度运行而切入时的“返回”地址。然后,又将next->thread.ip压入堆栈。所以,这里的next->thread.ip正是进程B上一次被调离时在第16行中保存的。它也指向这里的[prev_ip],即21行的pop指令。接着,在19行通过jmp命令,而不是call命令,转入了一个函数__switch_to()。暂时不讨论__switch_to(),当CPU执行到哪里的iret指令时,由于是通过jmp指令转过去的,最后进入对战的next->thread.ip就变成了返回地址,而这就是[prev_ip]所在的地址,也就是21行的pop指令所在的地址。由于每个进程在被调离时都要执行这里的第16行,这就决定了每个进程在收到调度恢复运行时都是从这里的第21行开始。
上面都是已有进程的切换。但新创建的进程会是怎么样切换的。新创建的进程并没有在“上一次调离时”执行过这里的第12至16行,所以要将其task_struct结构中的thread.ip事先设置好,并且设置“返回地址”时不一定是[prev_ip]所在的地址,这里取决于内核态堆栈的设置。
那么,我们可以看看前面上一篇文章浅析Linux计算机进程地址空间与内核装载ELF中fork()的介绍,这个地址在copy_thread()中确定,由于未能完整的阅读整个process_32.c中的源码,以下内容只是推测如有错误请博友指正,在新进程被创建时,在父进程执行完fork之后只会返回会从调用系统调用时状态,而子进程的“返回地址”也被设置成这个地址,所以__switch_to()一执行ret指令就直接回到了那里。
最后,在__switch_to()中到底干了些什么呢?看一下Linux3.9.7中/arch/x86/kernel/process_32.c 248行
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
int cpu = smp_processor_id();
struct tss_struct *tss = &per_cpu(init_tss, cpu);
fpu_switch_t fpu; /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */ fpu = switch_fpu_prepare(prev_p, next_p, cpu); /*
* Reload esp0.
*/
load_sp0(tss, next); /*
* Save away %gs. No need to save %fs, as it was saved on the
* stack on entry. No need to save %es and %ds, as those are
* always kernel segments while inside the kernel. Doing this
* before setting the new TLS descriptors avoids the situation
* where we temporarily have non-reloadable segments in %fs
* and %gs. This could be an issue if the NMI handler ever
* used %fs or %gs (it does not today), or if the kernel is
* running inside of a hypervisor layer.
*/
lazy_save_gs(prev->gs); /*
* Load the per-thread Thread-Local Storage descriptor.
*/
load_TLS(next, cpu); /*
* Restore IOPL if needed. In normal use, the flags restore
* in the switch assembly will handle this. But if the kernel
* is running virtualized at a non-zero CPL, the popf will
* not restore flags, so it must be done in a separate step.
*/
if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
set_iopl_mask(next->iopl); /*
* Now maybe handle debug registers and/or IO bitmaps
*/
if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
__switch_to_xtra(prev_p, next_p, tss); /*
* Leave lazy mode, flushing any hypercalls made here.
* This must be done before restoring TLS segments so
* the GDT and LDT are properly updated, and must be
* done before math_state_restore, so the TS bit is up
* to date.
*/
arch_end_context_switch(next_p); /*
* Restore %gs if needed (which is common)
*/
if (prev->gs | next->gs)
lazy_load_gs(next->gs); switch_fpu_finish(next_p, fpu); this_cpu_write(current_task, next_p); return prev_p;
}
这里主要处理的是TSS,核心在16行,把next_p->thread.esp0装入对应于本地cpu的tss的esp0字段;任何由sysenter汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到esp寄存器中。其次段寄存器gs中的内容也做了相应的切换。然后把next进程使用的县城局部存储(TLS)段装入本地CPU的全局描述符表;三个段选择符保存在进程描述符内的tls_array数组中。
所以,除了刚创建新进程外,所有进程在受到调度时的切入点都在宏定义switch_to()中的标号[prev_ip],一直运行到在下一次进入switch_to()以后在__switch_to()中执行ret为止。或者也可以认为,切入点在switch_to()中的21行,一直运行到在下一次进入switch_to()后的19行。总之,这新、旧当前进程的交接点就在switch_to这段代码中。
二、堆栈(函数调用堆栈)机制
接下来,我们分析一下在Linux系统中的函数调用堆栈的机制,之前在浅析Linux计算机工作机制中全面的分析了函数在堆栈调用过程中堆栈中变量及寄存器的数值的变化。
pushl %ebp
.cfi_def_cfa_offset
.cfi_offset , -
movl %esp, %ebp
即,在程序执行到一个函数的真正函数体时,已经有以下数据顺序入栈:参数,返回地址,EBP。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关):

如此形成递归,直至到达栈底。这就是函数调用栈。
三、中断机制
中断是CPU提供的一种功能,不属于linux内核,而对应的中断处理程序则属于内核控制,在执行新指令前,控制单元会检查在执行前一条指令的过程中是否有中断发生,如果有控制大院就会抛下指令,进入下面流程:
.确定与中断关联的向量i(<=i<=)
.寻找向量对于的处理程序
.保存当前的“工作现场”,执行中断处理程序
.处理程序执行完毕后,把控制权交还给控制单元
.控制单元恢复现场,返回继续执行原程序
所以整个中断的处理过程中,对于CPU,处理过程是一样的,中断现行程序,转到中断服务程序处执行,回到被中断的程序继续执行。CPU总共可以处理256种中断。
那什么是中断处理程序,在介绍中断处理程序之前,让我们先了解一下什么是软中断、tasklet和工作队列:
软中断:软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应的,"硬中断是外部设备对CPU的中断","软中断通常是硬中断服务程序对内核的中断","信号则是由内核(或其他进程)对某个进程的中断"(《Linux内核源代码情景分析》第三章)。
tasklet:tasklet是由软中断引出的, 内核定义了两个软中断掩码HI_SOFTIRQ和TASKLET_SOFTIRQ(两者优先级不同), 这两个掩码对应的软中断处理函数作为入口, 进入tasklet处理过程.
工作队列:定义一个work结构(包含了处理函数), 然后在中断处理过程中调用schedule_work函数, work便被添加到workqueue中, 等待处理.工作队列有着自己的处理线程, 这些work被推迟到这些线程中去处理.内核默认启动了一个工作队列, 对应一组工作线程events/n(n代表处理器编号, 这样的线程有n个). 驱动程序可以直接向这个工作队列添加任务. 某些驱动程序还可能会创建并使用属于自己的工作队列.
那么,让我们看看目前最主要引起中断的原因:IO中断,时钟中断,系统调用。所谓的中断处理程序就是在响应一个特定中断的时候,内核会执行一个函数,该函数就是中断处理程序。下图为中断处理程序的处理流程:
从上图可以看出,每个中断处理都要经历保存、处理与恢复过程,我们可以总结出以下步骤:1.保存现场。2.执行具体的中断处理程序。3.从中断处理返回。4.恢复现场。
那么为什么会有软中断,tasklet,和工作队列呢??由于中断处理程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断处理程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理。对应于上下半部的处理,才有了以上这些概念。
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
参数资料
【1】http://www.longene.org/forum/viewtopic.php?f=21&t=4646
【2】http://wenku.baidu.com/view/79fe36c10c22590103029d04
【3】 深入理解Linux内核
本文综述部分系个人理解,如有错误请指正,转载请声明
浅析Linux操作系统工作的基础的更多相关文章
- Linux操作系统工作的基础
简介: 本文根据 Linux™ 系统工作基础的分析,对存储程序计算机.堆栈(函数调用堆栈)机制和中断机制进行概述.文中将为您提供操作系统(内核)如何工作的细节,进一步从宏观概述结合关键点进行微观(CS ...
- 浅析Linux操作系统是如何工作的(思维导图)
SA***189 多任务计算机运转机制如下思维导图所示: 小结: Linux操作系统是一个在时钟的节拍下,各个模块紧密协作.密不可分的整体,而整个Linux系统都是建立在存储程序的基础之上,正是有了程 ...
- 【Linux 操作系统】Ubuntu 基础操作 基础命令 热键 man手册使用 关机 重启等命令使用
. : 关机, 如果将Linux默认运行等级设置为0, 系统将无法启动; -- : 多用户模式, 允许使用网络文件系统, 一般不使用图形界面登陆就是这种模式; -- : 多用户图形界面模式, 该模式下 ...
- Linux操作系统是如何工作的
<实验五——Linux操作系统是如何工作的?破解操作系统的奥秘> 姓名:方超 学号:SA12**6201 Linux操作系统工作的基础 存储程序计算机.堆栈(函数调用堆栈)机制和中断机制是 ...
- Linux操作系统是如何工作的?破解操作系统的奥秘
学号:SA12**6112 研究笔记: 1:计算机是怎么样工作的 2:用户态到内核态切换之奥秘解析 3:进程切换之奥秘解析 本博文主要是根据前3篇笔记来总结Linux内核的工作机制. 一:操作系统工作 ...
- Linux操作系统学习_操作系统是如何工作的
实验五:Linux操作系统是如何工作的? 学号:SA1****369 操作系统工作的基础:存储程序计算机.堆栈(函数调用堆栈)机制和中断机制 首先要整明白的一个问题是什么是存储程序计算机?其实存储程序 ...
- Linux操作系统基础(完结)
摘要 一.Linux操作系统概述 二.Linux操作系统安装 三.Linux文件系统及文件基础 四.Linux操作系统命令使用基础 五.Linux应用程序的安装与卸载基础 五.用户及进程 六.相关信息 ...
- Linux内核设计第二周——操作系统工作原理
Linux内核设计第二周 ——操作系统工作原理 作者:宋宸宁(20135315) 一.实验过程 图1 执行效果 从图中可以看出,每执行my_ start_ kernel函数两次或一次,my_ time ...
- Linux操作系统基础(四)保护模式内存管理(2)【转】
转自:http://blog.csdn.net/rosetta/article/details/8570681 Linux操作系统基础(四)保护模式内存管理(2) 转载请注明出处:http://blo ...
随机推荐
- devm_kzalloc and kmalloc
Move resources allocated using unmanaged interface to managed devm interface So today let's talk abo ...
- pcap文件格式
pcap文件格式 pcap文件格式是bpf保存原始数据包的格式,很多软件都在使用,比如tcpdump.wireshark等等,了解pcap格式可以加深对原始数据包的了解,自己也可以手工构造任意的数 ...
- 基于HBase0.98.13搭建HBase HA分布式集群
在hadoop2.6.0分布式集群上搭建hbase ha分布式集群.搭建hadoop2.6.0分布式集群,请参考“基于hadoop2.6.0搭建5个节点的分布式集群”.下面我们开始啦 1.规划 1.主 ...
- Java基础知识强化之集合框架笔记57:Map集合之HashMap集合(HashMap<Student,String>)的案例
1. HashMap集合(HashMap<Student,String>)的案例 HashMap<Student,String>键:Student 要求:如果两个对象 ...
- Android(java)学习笔记192:SQLite数据库(表)的创建 以及 SQLite数据库的升级
一.数据库的创建 1.文件的创建 //引用,如果文件不存在是不会创建的 File file = new File("haha.txt"): //输出流写数据 ...
- float浮动之后高度自适应失效解决方案
float浮动之后高度自适应失效解决方案 >>>>>>>>>>>>>>>>>>>> ...
- asp.net 调用前台JS调用后台,后台掉前台JS
C#前台js调用后台代码前台js<script type="text/javascript" language="javascript"> func ...
- mysql-distinct去重、mysql-group …
一.MYSQL-distinct用法 在使用mysql时,有时需要查询出某个字段不重复的记录,虽然mysql提供有distinct这个关键字来过滤掉多余的重复记录只保留一条,但往往只用它来返回不重复记 ...
- C# 实现的多线程异步Socket数据包接收器框架
转载自Csdn : http://blog.csdn.net/jubao_liang/article/details/4005438 几天前在博问中看到一个C# Socket问题,就想到笔者2004年 ...
- Activity是如何挂载Pargment的Day35
Activity是如何挂载Pargment的Day35 mebile5.0 1.Fragment优化早上任务 思路 一个主Activity 短信群发器 中秋给朋友发短信,都能够加上他们的姓名,这样他们 ...