课程学习总结报告

进程管理

Linux内核中的进程是非常复杂的,在操作系统原理中,我们通过进程控制块PCB描述进程。为了管理进程,内核要描述进程的结构,我们也称其为进程描述符,进程描述符直接或间接提供了进程相关的所有信息。

进程控制块PCB是名字为struct task_struct的数据结构,它称为任务结构体,为了很好地描述一个进程地各个信息,其内部包含的数据非常多,通过下面的示意图可以从整体上看清内部结构关系。

进程的状态

Linux主要有五中进程状态,分别是运行态、可运行态(就绪态)、等待态、暂停态和僵死态,各个状态的转换关系如下图所示:

需要注意的是,就绪态和运行态在系统中的状态都是TASK_RUNNING,也就是说,在Linux内核中,当进程是TASK_RUNNING状态时,它是可运⾏的,也就是就绪态,是否在运⾏取决于它有没有获得CPU的控制权,也就是说这个进程有没有在CPU中实际执⾏。如果在CPU中实际执⾏着,进程状态就是运⾏态;如果被内核调度出去了,在等待队列⾥就是就绪态。

进程的创建

在内核启动过程中,会创建0号进程init_task,它是所有进程的父进程,其他进程的创建都是通过fork父进程的方式创建并初始化的。fork在前几次的实验中已经分析过,此处不再详细介绍。

进程的调度

对于多任务系统来说,进程调度是必不可少的。

调度时机

  1. 进程状态发生变化时
  2. 当前进程时间片用完时
  3. 进程从系统调用返回到用户态时
  4. 中断处理后,进程返回到用户态时

调度策略

Linux对实时进程和普通进程采用不同的调度策略。

  • 普通进程(优先级100-139):采用时间片轮转算法
  • 实时进程(优先级1-99) :可选择先进先出或时间片轮转算法

可运行队列

为了方便进程调度,将所有处于TASK_RUNNING状态的进程进程集中管理,存放于可运行队列中。

首先将这些进程分为活动进程和过期进程两类:

  • 活动进程:处于可运行状态的进程,并且还没有用完他们的时间片,他们等待被运行;
  • 过期进程:处于可运行状态的进程,但已经用完了自己的时间片,在其他进程没有用完它们的时间片之前,他们不能再被运行。

因此,调度程序的工作就是在活动进程集合中选取一个最佳优先级的进程。

可运行队列的结构大体如下图所示:

arrays[0]arrays[1]分别存放过期进程和活动进程,且内部给每个优先级都分配有一个链表。

调度过程

对于实时进程而言,当它的时间片用完后,会被放到活动进程对应链表的末尾,等待下次执行。也就是说,实时进程总是被当做活动进程,当实时进程在运行时,普通进程无法运行。

对于普通进程而言,又将其分为交互进程和批处理进程。对于批处理进程,时间片用完后变为过期进程,而对于交互进程,时间片用完后一般仍然是活动进程,除非出现以下情况:

  • 最老的过期进程等待了很长时间
  • 过期进程中有比交互进程的优先级高的进程

当活动进程为空时,过期进程迁移到活动进程队列中继续执行。

进程的切换

本质上说进程切换由两步组成:

  1. 切换页全局目录以安装一个新的地址空间
  2. 切换内核态堆栈和硬件上下文

中断管理

内核的一个主要功能就是处理硬件外设I/O,而cpu的速度比外设快很多,如果使用轮询方式与外设交互,显然会浪费许多cpu的资源,所以需要操作系统支持中断。

中断描述符表 IDT

中断描述符表是一个系统表,它与每一个中断或者异常向量相联系,在表中存有每个中断或异常的处理程序的入口地址。IDT需要在启用中断前完成初始化,主要由trap_init()init_IRQ()进行初始化。idtr寄存器指向IDT表的物理基地址。

中断和异常的硬件处理

进入中断/异常

当cpu执行完一条指令后,会检查是否发生了中断或者异常。如果发生了中断或异常,则执行下列操作:

  1. 确定与中断或者异常关联的向量i(0~255)
  2. 读idtr寄存器指向的IDT表中的第i项
  3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符
  4. 比较程序的权限,确定中断是由授权的发生源发出
  5. 检查是否发生了特权级的变化,如果是由用户态进入内核态,需要进程堆栈切换
  6. 若发生的是故障,用引起异常的指令地址修改cs和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行
  7. 在栈中保存eflags、cs和eip的内容
  8. 如果异常产生一个硬件出错码,则将它保存在栈中
  9. 装载cs和eip寄存器,其值分别是IDT表中第i项描述符的段选择符和偏移量字段。

此时,进程的内核堆栈如下图所示:

中断服务程序占用的是被中断进程的内核栈,因此在中断服务程序正式执行之前,还需要将硬件没有自动入栈的一些通用寄存器进行手动入栈,其顺序与pt_regs结构相对应。

从中断/异常返回

中断/异常处理完后,相应的处理程序会执行一条iret汇编指令,这条汇编指令让CPU控制单元做如下事情:

  1. 用保存在栈中的值装载cs、eip和eflags寄存器。如果一个硬件出错码曾被压入栈中,那么弹出这个硬件出错码
  2. 检查处理程序的特权级是否等于cs中最低两位的值(这意味着进程在被中断的时候是运行在内核态还是用户态)。若是,iret终止执行;否则,转入3
  3. 从栈中装载ss和esp寄存器。这步意味着返回到与旧特权级相关的栈
  4. 检查ds、es、fs和gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且特权级比当前特权级高,则清除相应的寄存器。这么做是防止怀有恶意的用户程序利用这些寄存器访问内核空间

中断处理

主要流程

  1. 在内核态堆栈保存IRQ的值和寄存器的内容
  2. 为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断
  3. 调用do_IRQ(),执行共享这个IRQ的所有设备的中断服务例程
  4. 跳到ret_from_intr()的地址后中断跳出

do_IRQ

显然,do_IRQ()是一个很重要的函数,通过它可以执行多有注册的设备的中断服务程序。

do_IRQ()根据中断向量号i找到对应的中断描述符,将注册到本中断的各个设备的irqaction都执行一遍。

系统调用

进行系统调用的一种方式是通过使用int $0x80产生一个中断来实现的。在内核启动时,使用set_system_trap_gate(SYSCALL_VECTOR, &system_call);来将系统调用执行的函数与0x80号中断进行绑定。那么,当我们发出对应中断时,就会调用system_call进行处理。具体的处理过程不再赘述。

时钟管理

x86体系的Linux中,主要用到了三种时钟:实时时钟RTC、时间戳计数器TSC及可编程间隔定时器PIT。

RTC一般自带电池,系统掉电后仍可计时,所以Linux刚启动时使用RTC来获取时间。

TSC精度高,进程时间相关的变量一般采用此时钟值进行记录。

PIT虽然精度没有TSC高,但是可以周期性的产生中断

关键数据结构

时钟部分主要有以下三个关键的数据结构。

  • 计时时钟源
  • Jiffies变量,系统启动后的时间
  • Xtime变量,当前时间

时钟的初始化

time_init()函数中,有两个函数的作用很大。

  • setup_pit_timer()
    • 这个函数将pit_clockevent注册为clockevents设备,这样当PIT对应的中断触发后,其处理函数中的global_clock_event->event_handler(xxx)最终会调用到tick_periodic()函数
    • 且将pit中断指定为外部中断0
  • time_init_hook()
    • 设定外部中断0的中断服务程序为timer_interrupt()函数
    • timer_interrupt()调用do_timer_interrupt_hook(),也就是上面提到的会调用global_clock_event->event_handler(xxx)

这样,系统就会周期性的调用tick_periodic()函数,这个函数主要做了如下几件事:

  • 更新自系统启动以来所经过的时间(Jiffies)
  • 更新时间和日期(RTC)
  • 确定当前进程的执行时间,考虑是否要抢占
  • 更新资源使用统计计数
  • 检查到期的软定时器

文件系统

VFS

VFS是一个软件层,用来处理与Unix标准文件系统相关的所有系统调用,能为各种文件系统提供一个通用的、统一的接口。对于用户而言,不再需要自己针对每个不同的文件系统执行不同的操作命令,只需要用统一的open\read\write等操作。

VFS与具体文件系统的关系如下图所示,它向上提供统一的接口,向下兼容各种不同的文件系统。

基本思想是引入一个通用文件模型,这个模型能够表示所有支持的文件系统,这个模型有以下几个对象类型组成:

  • 超级块对象
    • 存放文件系统相关信息:例如文件系统控制块
  • 索引节点对象
    • 存放具体文件的一般信息:文件控制块/inode
  • 文件对象
    • 存放已打开的文件和进程之间交互的信息
  • 目录项对象
    • 存放目录项与文件的链接信息

主要数据结构

  • 系统打开文件表:包含每个已打开文件的FCB的副本,以及其他信息。
  • 进程打开文件表:包含一个指向系统打开文件表相应项的指针,以及其他信息。

当使用sys_open打开一个文件时,会创建系统打开文件表,并从进程打开文件表的fd数组中找到一个空闲位置i,将其指向创建的系统文件打开表,返回对应的索引值i。

当我们需要读取或写入文件时,通过返回的索引值i找到系统打开文件表,调用其中对应的读或写函数对文件进行读写。

所以,在进行读写之前,必须先使用open来创建系统打开文件表,否则就无法找到对应文件的读写操作函数,也就无法读写文件了。

posted @ 2020-07-06 22:31  maxiaowei0216  阅读(249)  评论(1编辑  收藏  举报