一、linux系统概念模型
1. 概述
linux系统是一个多用户多任务的分时操作系统,函数调用是操作系统的三大法宝之一,使得编程极为灵活。由于CPU的运行速度远远大于外设,所以中断机制的使用解决了CPU等待外设的情况。系统调用是一种特殊的中断,封装了对系统的一些底层的操作,保证了系统的安全性。在中断返回时可能发生进程切换,内核线程也可以主动发起进程切换
本篇博文是对整个Linux操作系统分析课程的整体回顾,下面对上述焦点问题分别详细阐述
2. 函数调用
函数调用过程概述
-
首先调用者的call函数将它的下一条指令地址保存在栈顶,将eip(rip)设置为被调用函数的起始地址
-
建立被调用者函数的堆栈框架、执行被调用者函数体、拆除被调用者函数框架
-
ret指令将调用者的下一条指令地址恢复到eip(rip)
函数调用中的硬件操作
例如对于函数调用:call 0x12345
/*32位*/
pushl %eip (*)
movl $0x12345, %eip (*)
/*64位*/
pushq %rip (*)
movq $0x12345, %rip (*)
函数返回:ret
/*32位*/
popl %eip (*)
/*64位*/
popq %rip (*)
上面的指令均为伪指令,这个动作由硬件一次性完成,这是由于EIP寄存器不能被直接修改和使用
函数调用中的软件操作
例如对于函数:
int main() {
return f(8)+1;
}
在32位系统中,汇编为:
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $8, (%esp)
call f
addl $1, %eax
movl %ebp,%esp
popl %ebp
ret
在64位系统中,汇编为:
main:
pushq %rbp
movq %rsp, %rbp
movl $8, %edi
call f
addl $1, %eax
popq %rbp
ret
我们对比32位系统和64位系统的函数调用:
-
共同点:
-
都在调用之前保存了原来的栈,为新函数开辟了新的栈空间
-
调用返回时都恢复了原来的栈空间
-
函数的返回值都在eax(rax)寄存器中保存
-
-
区别:
- 参数传递上存在差别,32位系统将参数存在新栈的栈底
3. 中断和异常
中断和异常的区别与联系:
中断是异步的,由硬件随机产生,在程序执行的任何时候可能出现
异常是同步的,在特殊的或出错的指令执行时由CPU控制单元 产生
我们用“中断信号”来通称这两种类型的中断
中断上下文
中断上下文不同于进程上下文,中断上下文只包含了很有限的几个寄存器,建立和终止这个上下文所需要的时间很少。中断程序只能使用被中断进程的内核栈作为自己的运行栈
中断和异常的硬件级处理
- 当CPU正常运行时,执行一条指令后,cs和eip包含了下一条将要执行的指令的逻辑地址
- 在执行这条指令之前,CPU会检查在运行前一条指令时是否发生了一个中断或者异常。
- 如果发生了中断或异常:
- 确定中断向量号i,读idtr寄存器指向的IDT表中的第i项
- 从gdtr寄存器获得GDT的基地址,并在GDT中查找, 以读取IDT表项中的段选择符所标识的段描述符
- 确定中断是由授权的发生源发出的,只允许从低特权级陷入高特权级,反之不可以
- 如果是由用户态陷入内核态,用与新特权级相关的栈段和栈指针装载ss和esp寄存器,在新的栈中保存ss和esp以前的值
- 如果发生的是故障,用引起异常的指令地址修改cs 和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行
- 在栈中保存eflags、cs、eip;如果异常产生一个硬件出错码,则将它保存在栈中
- 装载cs和eip寄存器,其值分别是IDT表中第i项描述符的段选择符和偏移量
- 中断返回:
- 用保存在栈中的值装载cs、eip和eflags寄存器
- 检查中断时是在内核态还是用户态,如果在内核态,则终止;如果在用户态,从栈中装载ss和esp寄存器
中断和异常的软件级处理
异常处理:
-
按照pt_regs结构定义的堆栈数据格式完成相应的入栈操作,进一步完成现场的保存
-
把堆栈地址中的do_handler_name()函数的地址装入edi寄存器中,并在这个位置写入fs值,使栈结构进一步与pt_regs结构完全一致
-
最后执行call *%edi指令
中断处理:
可以用下面的汇编代码来总结:
SAVE_ALL
movl %esp,%eax
call do_IRQ jmp
$ret_from_intr
4. 系统调用
系统调用是陷阱这种软中断方式主动从用户态进入内核态的
传统的系统调用:
int $0x80 指令会触发系统调用,CPU压栈⼀些关键寄存器,根据eax寄存器传递的系统调用号调用对应的内核处理函数,接着内核负责保存现场,系统调用内核函数处理完后恢复现场,后通过iret出栈哪些CPU压栈的关键寄存器。
快速系统调用:
sysenter和syscall都借助CPU内部的MSR寄存器来查找系统调用处理入口,其余操作与传统系统调用相同
系统调用的参数传递:
32位x86和64位的x86都是通过寄存器来传递。注意:在普通的函数调用中,32位x86使用压栈来传递参数,而64位x86仍然使用寄存器传递参数
5. 进程管理
进程创建
0号进程初始化是通过硬件编码,其他进程的初始化都是通过do_fork复制父进程的方式初始化
1号进程为是kernel_init,是所有用户进程的祖先;2号进程时kthreadd,是所有内核线程的祖先,1、2号进程都是复制0号进程得到的
父进程通过fork系统调用进入内核_do_fork函数,主要完成了:
- 调用copy_process()复制父进程
- 分配子进程的内核堆栈并对内核堆栈和thread等进程关键山下文进行初始化
- 调用wake_up_new_task将子进程加入就绪队列等待调度执行
进程切换
进程调度时机: Linux内核通过schedule函数实现进程调度,分别为中断返回前和内核线程主动调用schedule
进程上下文:
-
用户地址空间:包括程序代码、数据、用户堆栈等
-
控制信息:进程描述符、内核堆栈等
-
进程的CPU上下文,相关寄存器的值
当调用schedule函数时其中的switch_to做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程的内核堆栈,并完成了进程上下文所需的EIP等寄存器状态切换
6. 文件系统
文件对用户来说是最直接可见的部分
linux中文件时按名存取的,系统给每个文件设置一个文件控制块——FCB
文件系统的一个重要的任务是文件系统的存储空间的管理,实质上是空闲块的组织和管理分配和回收等问题
用户可以使用系统调用来实现文件操作,如open、read、write、close
虚拟文件系统:要实现操作系统对不同文件系统的支持,就要将对各种不同文件系统的操作和管理纳入到一个统一的框架中。对用户程序隐藏实现细节
二、Linux模型的举例
我们举一个在linux命令行输入vim,并按下回车的例子:
我们在键盘输入回车键系统能有反应,说明发生了中断,该中断处理程序处理了键盘的输入信号
中断返回的时候,父进程fork系统调用进入内核_do_fork函数
调用copy_process()复制父进程
分配子进程的内核堆栈并对内核堆栈和thread等进程关键山下文进行初始化
调用wake_up_new_task将子进程加入就绪队列等待调度执行
Linux内核通过schedule函数实现进程调度
schedule函数中的switch_to做了关键的进程上下文切换,将之前进程的内核堆栈以及相关寄存器切换到vim进程上来,这样我们就看到进入了vim程序的界面
三、对课程的心得体会
本课程两位老师通过理论和实验双管齐下,对linux系统的原理进行了深刻的阐述,使得本人对Linux操作系统自有的特性有了一定的认识,不再像之前只是操作系统这个笼统的概念。
改进意见:增加一些个在实际工程中使用Linux的实验,这样可以让同学们在应用中从不同的角度理解Linux系统