一、基础知识
1、Liunx汇编
主要寄存器:EAX、EBX、ECX、EDX、EBP、ESI、EDI、EIP、ESP(其中ESP是堆栈栈顶寄存器,EBP是堆栈基址指证针,EIP寄存器不能直接使用和修改。调用call时会修改EIP指针。EBP和ESP总指向同一个堆栈,EBP指向栈底,ESP指向栈顶)
pushl $8 movl %esp, %ebp subl $4, %esp movl $8, (%esp)
- 上述代码首先将ESP寄存器值减4(栈是从高地址向低地址增长的),然后把立即数8放入当前栈顶的位置,完成8的压栈
- 将ESP的值放入EBP中
- 将ESP的值减4,栈顶指针指向下一个存储单元
- 将立即数8放入ESP寄存器所指的位置,通过间接寻址入栈
二、中断和系统调用
1、中断
中断(异步的):由硬件随机产生,在程序执行的任何时候可能出现,比如键盘输入。
异常(同步的): 在(特殊的或出错的)指令执行时由CPU控制单元产生,比如除0、INT指令、缺页。
在进程的内核态堆栈保存程序计数器的当前值(即eip和cs寄存器)以便处理完中断的时候能正确返回到中断点,并把与中断信号相关的一个地址放入进程序计数器, 从而进入中断的处理。
- CS是程序代码段选择符(表示它在哪个段中运行),EIP是程序代码段的偏移,CS+EIP表示程序的逻辑地址
- 中断为了跳转要改变CS和EIP,为了返回要保存CS和EIP
- 保存在被中断进程的内核栈中,因为中断没有自己的上下文,占用被中断进程的上下文
- 发生中断嵌套时保存现场都是占用的被中断进程的内核栈
- 在临界区中,中断必须被禁止
- 中断上下文是一个内核控制路径
- 在中断中不能被进程抢占,因为中断处理程序中没有调度程序存在(但中断退出函数中可以调度)。只有中断可以打断中断执行
如图,A、B为中断嵌套,C为被中断进程,B也是用C进程的内核栈,B执行完一定返回A,但A执行完不一定返回C。A会判断自己是否是最后一级中断,如果是且进程允许调度则执行调度程序,如果C的优先级高,在恢复上下文。
中断控制器完成中断事件和中断向量的对应,CPU通过向量来群顶设备和对应的中断处理程序入口。Intel给中断控制器分配的中断向量号从32开始,0~31分配给异常。
中断描述符表:每个中断或异常向量在表中有相应的中断或者 异常处理程序的入口地址(包括CS、EIP、权限和类型等)。每个描述符8个字节(寻址:IDT+n*8)
当中断发生时,Linux系统会跳转到asm_do_IRQ()函数(所有中断程序的总入口函数),并且把中断号irq传进来。根据中断号,找到中断号对应的irq_desc结构(irq_desc结构为内核中中断的描述结构,内核中有一个irq_desc结构的数组irq_desc_ptrs[NR_IRQS]),然后调用irq_desc中的handle_irq函数,即中断入口函数。
中断过程:
1)CPU的正常运行:
- 当执行了一条指令后,cs和eip这对寄存器包含了下一条将要执行的指令的逻辑地址。 在执行这条指令之前,CPU控制单元会检查在运行前一条指令时是否发生了一个中断或者异常。如果发生了一个中断或异常,那么CPU控制单元执行下列操作:
- 确定与中断或者异常关联的向量i(0~255)
- 读idtr寄存器指向的IDT表中的第i项(i*8)
- 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符:GDT全局描述符表所有进程共享,描述了一些用户代码段和数据段、内核段。CS段描述符描述了代码段的基地址和上限
- 确定中断是由授权的发生源发出的
- 中断:中断处理程序的特权不能低于引起中断的程序的特权(对应GDT表项中的DPL vs CS寄存器中的 CPL)。被中断程序的权限可以通过当前CS的最后两位获得,未来程序的权限可以通过中断描述符获得
- 检查是否发生了特权级的变化,一般指是否由用户态陷入了内核态。 如果是由用户态陷入了内核态,控制单元必须开始使用与新的特权级相关的堆栈。a,读tr寄存器,访问运行进程的tss段 b,用与新特权级相关的栈段和栈指针装载ss和esp寄存 器。这些值可以在进程的tss段中找到 c,在新的栈中保存ss和esp以前的值,这些值指明了与 旧特权级相关的栈的逻辑地址
- 若发生的是故障,用引起异常的指令地址修改cs 和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行
- 在栈中保存eflags、cs和eip的内容
- 如果异常产生一个硬件出错码,则将它保存在栈中
- 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这对寄存器值给出中断或者异常处理程序的第一条指定的逻辑地址
中断/异常处理完后,相应的处理程序会执行一条iret汇编指令,这条汇编指令让 CPU控制单元做如下事情:
1,用保存在栈中的值装载cs、eip和eflags寄存器。如果一个硬件出错码曾被压入栈中, 那么弹出这个硬件出错码
2,检查处理程序的特权级是否等于cs中最低两位的值(这意味着进程在被中断的时候是运行在内核态还是用户态)。若是,iret终止执行;否则,转入3
3,从栈中装载ss和esp寄存器。这步意味着返回到与旧特权级相关的栈
4,检查ds、es、fs和gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且特权级比当前特权级高,则清除相应的寄存器。
初始化中断描述符表:调用不同函数对中断描述符表进行填充
用_set_gate中的参数生成一个中断描述符,放在第N项上。IDT实际上是经过两次初始化,第一次在start kernel之前,用ignore_int()函数填充256个idt_table表项
ignore_int()的作用:保存一部分内容在栈上,调用打印程序打印int_msg,恢复现场。
Start_kernel中的IDT表初始化:trap_init()、init_IRQ()
异常:
- 保存现场(pt-regs)
- 把栈的esp给eax,把硬件出错码给edx,除0没有硬件出错码,因此压0
- 调用对应c函数
I/O中断处理程序:
- 在内核态堆栈保存IRQ的值和寄存器的内容
- 为正在给IRQ线服务的PIC发送一个应答, 这将允许PIC进一步发出中断
- 执行共享这个IRQ的所有设备的中断服务例程(因为不知道是哪个设备发出的)
- 跳到ret_from_intr()的地址后中断跳出
三、进程调度
进程描述符放在动态内存中而且和内核态的进 程栈放在一个独立的8KB的内存区中
0号进程是所有进程的父进程, 0号创建init进程,init完成相关初始化,执行相关程序。Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构: Thread_info和进程的内核堆
可运行队列:
- active:活动的可运行队列(这个进程没有被运行过)
- expired:过期的可运行队列(这个进程刚刚被运行过,因为时间片过期了)
- 当需要调度时通过查找位图,找到优先级最高的链表,再对链表上的进程按FIFO运行
- 如果活跃进程数为0,就把expired链表拷贝到active中
进程切换:
schedule() --> context_switch() --> switch_to -> __switch_to()
进程切换主要有 两部分:1、切换全局页表项;2、切换内核堆栈和硬 件上下文。切换全局页表项这个切换工作由 context_switch()完成。其中switch_to和__switch_to() 主要完成第二部分。
四、文件
1、根文件系统挂载
-
挂载虚拟的文件系统rootfs作为初始文件系统
-
挂载一个真正的根文件系统替换rootfs(先生成一个挂载点,为0号进程的根目录,在挂载点上挂载文件系统)
- 在挂载根文件系统之前,内核已经知道跟文件在哪个设备上存着
sys_mount:扫描内核已注册文件链表,找到和它挂载类型相匹配的结构,执行相应的函数来完成文件系统挂载,如果没有,挂载失败(因此挂载之前要先注册)
已注册文件系统链表结构体:
get_sb:给文件系统创建超级块和根目录对象,超级块完成文件系统的描述。mount根据文件系统类型运行不同的get_sb
根文件挂载的过程:
populate_rootfs函数负责加载initramfs和cpio-initrd到根文件系统rootfs;对于不是以上两种情况把它放入将其释放到/initrd.image.
2、文件系统
1)open系统调用过程
open执行--->INT 80 05--->查找中断异常向量表(trap_init)--->保存现场--->对指令分析参数05--->执行系统调用表--->sys_open--->进行命名查找--->得到文件控制块,检查文件类型--->根据不同类型调用不同的文件打开函数--->创建一个file结构表(文件打开表),用文件控制块填写--->返回进程(进程中也有file,有fd数组,fd指针指向刚创建的系统打开表)
文件read前要open它,以建立用户和文件的联系,打开一个文件创建一个file结构
VFS虚拟文件系统:不用考虑文件属性。各种不同的文件系统通过mount(挂载、安装)到根文件系统中。它的超级快对象中有各种文件的私有结构,实现对不同文件的兼容
每个安装的文件系统都有一个超级块结构,mount会创建一个与它相应的VFS超级块对象
五、总结
本次课程学习到了Linux的基础知识,包括进程切换、中断和系统调用、驱动设备、文件系统等,受益良多。