一、操作系统工作的基础
1、存储程序计算机
1945年,美藉匈牙利科学家冯·诺依曼(J.Von Neumann)提出的,是现代计算机的理论基础。现代计算机已经发展到第四代,但仍遵循着这个原理。
存储程序和程序控制原理的要点是,程序输入到计算机中,存储在内存储器中(存储原理),在运行时,控制器按地址顺序取出存放在内存储器中的指令(按地址顺序访问指令),然后分析指令,执行指令的功能,遇到转移指令时,则转移到转移地址,再按地址顺序访问指令(程序控制)。
虽然计算机技术发展很快,但“存储程序原理”至今仍然是计算机内在的基本工作原理。自计算机诞生的那一天起,这一原理就决定了人们使用计算机的主要方式——编写程序和运行程序。
冯·诺依曼结构(John von Neumann)也就是存储程序奠定了现代计算机的基本结构,其特点是:
1)使用单一的处理部件来完成计算、存储以及通信的工作。
2)存储单元是定长的线性组织。
3)存储空间的单元是直接寻址的。
4)使用低级机器语言,指令通过操作码来完成简单的操作。
5)对计算进行集中的顺序控制。
6)计算机硬件系统由运算器、存储器、控制器、输入设备、输出设备五大部件组成并规定了它们的基本功能。
7)采用二进制形式表示数据和指令。
8)在执行程序和处理数据时必须将程序和数据道德从外存储器装入主存储器中,然后才能使计算机在工作时能够自动调整地从存储器中取出指令并加以执行。
2、堆栈(函数调用堆栈)机制
参见实验二报告:Linux操作系统实验二:进程的创建与可执行程序的加载
3、中断机制
中断可以很简单的形容为cpu暂停目前正在处理的任务,转而去处理新的请求处理的任务,当新任务处理完成后,再回到原来暂停的位置,继续处理原有的任务。
中断处理分硬件处理部分和软件处理部分,以下摘自李春杰老师课件:
中断和异常的硬件处理:
CPU的正常运行:
当执行了一条指令后,cs和eip这对寄存器包含了下一条将要执行的指令的逻辑地址。
在执行这条指令之前,CPU控制单元会检查在运行前一条指令时是否发生了一个中断或者异常。如果发生了一个中断或异常,那么CPU控制单元执行下列操作:
1,确定与中断或者异常关联的向量i(0~255)
2,读idtr寄存器指向的IDT表中的第i项
3,从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符
4,确定中断是由授权的发生源发出的。
n 中断:中断处理程序的特权不能低于引起中断的程序的特权(对应GDT表项中的DPL vs CS寄存器中的CPL)
n 编程异常:还需比较CPL与对应IDT表项中的DPL
5,检查是否发生了特权级的变化,一般指是否由用户态陷入了内核态。
如果是由用户态陷入了内核态,控制单元必须开始使用与新的特权级相关的堆栈
n a,读tr寄存器,访问运行进程的tss段
n b,用与新特权级相关的栈段和栈指针装载ss和esp寄存器。这些值可以在进程的tss段中找到
n c,在新的栈中保存ss和esp以前的值,这些值指明了与旧特权级相关的栈的逻辑地址
6,若发生的是故障,用引起异常的指令地址修改cs和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行
7,在栈中保存eflags、cs和eip的内容
8,如果异常产生一个硬件出错码,则将它保存在栈中
9,装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这对寄存器值给出中断或者异常处理程序的第一条指定的逻辑地址
中断和异常的软件处理:
1. 将中断向量入栈
2. 保存所有其他寄存器
3. 调用do_IRQ
4. 跳转到ret_from_intr
整个过程如下图所示:
二、操作系统是如何工作的
操作系统最重要的工作莫过于进程的管理和调度。每个进程通过一个叫做进程描述符
task_struct的数据结构进行描述,其中包含了进程所有的属性。下面描述操作系统如何进行进程间调度:
如下图所示,当某进程A正在运行,esp寄存器指向进程A的用户栈栈顶,eip寄存器指向进程A的代码区,若进程A的时间片用完并发生中断,那么CPU要做两件工作:
(1)将当前的eip和esp压入到进程A的内核栈;
(2)将esp指向进程A的内核栈,并将eip指向中断处理入口,进入到内核态。
在进行SAVE_ALL即保存相关寄存器等之后,开始执行中断处理程序,进行的切换与调度就是发生在中断处理程序中。我们会根据相应的调度策略,选择下一个要执行的进程Next,并切换到进程Next。这里内核是通过执行switch_to函数来进行进程切换的,在switch_to中我们主要进行了进程的寄存器register和进程的内核栈stack的切换。当进程切换完以后,我们的esp就指向了进程B的内核栈,我们可以看到,在进程B的内核栈中保存着进程B被挂起以前的esp以及eip,我们进行出栈操作,则当前的eip指向进程B的代码区,而esp指向进程B的用户栈,这样通过esp和eip的的切换我们就切换到了进程B继续执行,完成了整个进程从A切换到进程B。
switch_to()函数的代码如下:
#define switch_to(prev,next,last) do{ \
unsigned long esi,edi; \
asmvolatile("pushfl\n\t" /* Saveflags */ \
"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)