Linux0.11内核部分图解


总结自赵炯《Linux内核完全注释》,做为学习笔记

Linux 内核模式

Linux 0.11 内核,采用了单内核模式。单内核模式的主要优点是内核代码结构紧凑、执行速度快,不足之处主要是层次结构性不强。可粗略地分为三个层次:调用服务的主程序层、执行系统调用的服务层和支持系统调用的底层函数。如图:
在这里插入图片描述

Linux 内核系统体系结构

Linux 内核主要由 5 个模块构成,它们分别是:进程调度模块、内存管理模块、文件系统模块、进程间通信模块和网络接口模块。这几个模块之间的依赖关系如图:
在这里插入图片描述
所有的模块都与进程调度模块存在依赖关系。因为它们都需要依靠进程调度程序来挂起(暂停)或重新运行它们的进程。通常,一个模块会在等待硬件操作期间被挂起,而在操作完成后才可继续运行。

根据Linux 0.11 内核源代码的结构将内核主要模块绘制,如图:
在这里插入图片描述
所有这些模块还会依赖于内核中的通用资源。这些资源包 括内核所有子系统都会调用的内存分配和收回函数、打印警告或出错信息函数以及一些系统调试函数。

中断机制

在使用 80X86 组成的 PC 机中,采用了两片 8259A 可编程中断控制芯片。每片可以管理 8 个中断源。 通过多片的级联方式,能构成最多管理 64 个中断向量的系统。在PC/AT 系列兼容机中,使用了两片 8259A 芯片,共可管理 15 级中断向量,如图:
在这里插入图片描述
在总线控制器控制下,8259A 芯片可以处于编程状态和操作状态。编程状态是 CPU 使用 IN 或 OUT 指令对 8259A 芯片进行初始化编程的状态。一旦完成了初始化编程,芯片即进入操作状态,此时芯片即 可随时响应外部设备提出的中断请求(IRQ0 – IRQ15)。

通过中断判优选择,芯片将选中当前高优先级 的中断请求作为中断服务对象,并通过 CPU 引脚 INT 通知 CPU 外中断请求的到来,CPU响应后,芯片从数据总线 D7-D0 将编程设定的当前服务对象的中断号送出,CPU 由此获取对应的中断向量值,并执行 中断服务程序。

对于 Linux 内核来说,中断信号通常分为两类:硬件中断和软件中断(异常)。每个中断是由 0-255 之间的一个数字来标识。对于中断 int0–int31(0x00–0x1f),每个中断的功能由 Intel 公司固定设定或保留用, 属于软件中断,但 Intel 公司称之为异常。因为这些中断是在 CPU 执行指令时探测到异常情况而引起的。中断 int32–int255 (0x20–0xff)可以由用户自己设定。 在 Linux 系统中,则将 int32–int47(0x20–0x2f)对应于 8259A 中断控制芯片发出的硬件中断请求信号 IRQ0-IRQ15,并把程序编程发出的系统调用(system_call)中断设置为 int128(0x80)。

进程管理

内核程序通过进程表(task)对进程进行管理,每个进程在进程表中占有一项。在 Linux 系统中,进程表项是一个 task_struct 任务结构指针。这就是我们熟知的进程控制块 PCB(Process Control Block)或进程描述符 PD(Processor Descriptor)。

当一个进程在执行时,CPU 的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上 下文。当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前 进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。

进程状态

一个进程在其生存期内,可处于一组不同的状态下,称为进程状态。如图:
在这里插入图片描述

  • 运行状态(TASK_RUNNING) 当进程正在被 CPU 执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态 (running)。进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤 醒而进入准备运行状态,该状态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同, 都被成为处于 TASK_RUNNING 状态。
  • 可中断睡眠状态(TASK_INTERRUPTIBLE) 当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正 在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态) 。
  • 不可中断睡眠状态(TASK_UNINTERRUPTIBLE) 与可中断睡眠状态类似。但处于该状态的进程只有被使用 wake_up()函数明确唤醒时才能转换到可 运行的就绪状态。
  • 暂停状态(TASK_STOPPED) 当进程收到信号 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 时就会进入暂停状态。可向其发送 SIGCONT 信号让进程转换到可运行状态。在 Linux 0.11 中,还未实现对该状态的转换处理。处于该 状态的进程将被作为进程终止来处理。
  • 僵死状态(TASK_ZOMBIE) 当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。

进程初始化

在 boot/目录中引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执 行系统初始化程序 init/main.c。

main程序

  1. 首先确定如何分配使用系统物理内存,
  2. 然后调用内核各部分的初 始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。

在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到任务 0 (进程 0)中运行,并使用 fork()调用首次创建出进程 1。

原进程 0 则会在系统空闲时被调度执行,此时任务 0 仅执行 pause()系统调用, 并又会调用调度函数。

内核初始化是一个特殊过程,内核初始化代码也即是任务 0 的代码。

创建新进程

Linux 系统中创建新进程使用 fork()系统调用。所有进程都是通过复制进程 0 而得到的,都是进程 0 的子进程。

创建新进程的过程:

  1. 在任务数组中找出一个还没有被任何进程使用的空项。(如 果系统已经有 64 个进程在运行,则 fork()系统调用会因为任务数组表中没有可用空项而出错返回)
  2. 系统为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构 中的所有内容作为新进程任务数据结构的模板。为了防止这个还未处理完成的新建进程被调度函数执行, 此时应该立刻将新进程状态置为不可中断的等待状态。
  3. 对复制的任务数据结构进行修改。把当前进程设置为新进程的父进程,清除信号位图并复位新 进程各统计值,并设置初始运行时间片值。
  4. 根据当前进程设置任务 状态段(TSS)中各寄存器的值。
  5. 设置新任务的代码和数据段基址、限长并复制当前进程内存分页管理的页表。如果父进程 中有文件是打开的,则应将对应文件的打开次数增 1。接着在 GDT 中设置新任务的 TSS 和 LDT 描述符 项,其中基地址信息指向新进程任务结构中的 tss 和 ldt。
  6. 将新任务设置成可运行状态并返回新进 程号。

进程调度

进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。

为了能让进程有效地使用系统资源,又能使进程有较快的响应时间,就需要对进程的切换调度采用 一定的调度策略。

主要程序:schedule()

调度的流程:

  1. schedule()函数首先扫描任务数组。通过比较每个就绪态(TASK_RUNNING)任务的运行时间递减 滴答计数 counter 的值来确定当前哪个进程运行的时间少。

  2. 如果此时所有处于 TASK_RUNNING 状态进程的时间片都已经用完,系统就会根据每个进程的优先 权值 priority,对系统中所有进程(包括正在睡眠的进程)重新计算每个任务需要运行的时间片值 counter。

  3. schdeule()函数重新扫描任务数组中所有处于 TASK_RUNNING 状态,重复上述过程,直到选择出 一个进程为止。后调用 switch_to()执行实际的进程切换操作。

  4. 如果此时没有其它进程可运行,系统就会选择进程0运行。对于Linux 0.11来说,进程0会调用pause() 把自己置为可中断的睡眠状态并再次调用 schedule()。

进程切换

主要程序: switch_to()

过程:

  1. switch_to() 首先检查要切换到的进程是否就是当前进程,如果是则什么也不做,直接退出。
  2. 先把内核全局 变量 current 置为新任务的指针
  3. 长跳转到新任务的任务状态段 TSS 组成的地址处。
  4. CPU 执行任务切换操作,CPU 把其所有寄存器的状态保存到当前任务寄存器 TR 中 TSS 段选择符所指向的 当前进程任务数据结构的 tss 结构中,然后把新任务状态段选择符所指向的新任务数据结构中 tss 结构中 的寄存器信息恢复到 CPU 中,系统正式开始运行新切换的任务了。具体过程如图:
    在这里插入图片描述

终止进程

用户程序调用 exit()系统调用时,执行内核函数 do_exit()。

函数执行过程:

  1. 释放进程代码段 和数据段占用的内存页面。
  2. 关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程 序的 i 节点进行同步操作
  3. 如果进程有子进程,则让 init 进程作为其所有子进程的父进程。
  4. 如果进程是 一个会话头进程并且有控制终端,则释放控制终端,并向属于该会话的所有进程发送挂断信号 SIGHUP, 这通常会终止该会话中的所有进程。然后把进程状态置为僵死状态 TASK_ZOMBIE。并向其原父进程发 送 SIGCHLD 信号,通知其某个子进程已经终止。

注意:在进程被终止时,它的任务数据结构仍然保留着。因为其父进程还需要使用其中的信息。

在子进程在执行期间,父进程通常使用 wait()或 waitpid()函数等待其某个子进程终止。当等待的子 进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中。终释放已 终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。

内存的划分

为了有效地使用机器中的物理内存,内存被划分成几个功能区域,如图:
在这里插入图片描述Intel CPU 使用段的概念来对程序进行寻址。在 Linux 0.11 中,程序逻辑地址到线性地址的变换过程使用了 CPU 的 全局段描述符表 GDT 和局部段描述符表 LDT。由 GDT 映射的地址空间称为全局地址空间,由 LDT 映射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。具体的使用方式如图 :
在这里插入图片描述
内存分页管理的基本原理是将整个主内存区域划分成 4096 字节为一页的内存页面。程序申请使用内 存时,就以内存页为单位进行分配。

Linux 0.11 内核, 系统设置全局描述符表 GDT 中的段描述符项数大为 256。最大任务数为64个,每个进程虚拟地址范围是 64M,地址空间如图:
在这里插入图片描述

Linux系统堆栈的使用

Linux 0.11 系统中共使用了四种堆栈:

  1. 系统初始化时临时使用的堆栈
  2. 供内核程序自 己使用的堆栈(内核堆栈),位于系统地址空间固定的位置,也是后来任务 0 的用户态堆栈;
  3. 每个任务通过系统调用,执行内核程序时使用的堆栈,即任务的内核态堆栈。
  4. 任务在用户态执行的堆栈,位于任务(进程)地址空间的末端。

初始化阶段

开机初始化时

bootsect 被移动到 0x9000:0 处时,才把堆栈段寄存器 SS 设置为 0x9000,堆栈指针 esp 寄存器设置为 0xff00,也即堆栈顶端在 0x9000:0xff00 处。

进入保护模式时

从 head.s 程序起,系统开始正式在保护模式下运行。此时堆栈段被设置为内核数据段(0x10),堆 栈指针 esp 设置成指向 user_stack 数组的顶端,保留了 1 页内存(4K)作为堆栈使用。如图:
在这里插入图片描述

初始化时

在 main.c 中,在执行move_to_user_mode()代码之前,系统一直使用上述堆栈。而在执行过 move_to_user_mode()之后,main.c 的代码被“切换”成任务 0 中执行。通过执行 fork()系统调用,main.c 中的 init()将在任务 1 中执行,并使用任务 1 的堆栈。而 main()本身则在被“切换”成为任务 0 后,仍然继 续使用上述内核程序自己的堆栈作为任务 0 的用户态堆栈。

任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态 堆栈。

主要区别在于任务的内核态堆栈很小,所保存的数据量多不能超过(4096 – 任务数据结构)个字节,大约为 3K 字节。而任务的用户态堆栈却可以在用户的 64MB 空间内延伸。

内核态堆栈如图:
在这里插入图片描述
任务0的堆栈:

  1. 任务 0 的代码段和数据段相同,段基地址都是从 0 开始,限长也都是 640KB。这个地址范围也就是 内核代码和基本数据所在的地方。
  2. 在执行了 move_to_user_mode()之后,它的内核态堆栈位于其任务数据结构所在页面的末端,而它的用户态堆栈就是前面进入保护模式后所使用的堆栈,也即 sched.c 的 user_stack 数组的位置。
  3. 任务 0 的内核态堆栈是在其人工设置的初始化任务数据结构中指定的,而它的用 户态堆栈是在执行 movie_to_user_mode()时,在模拟 iret 返回之前的堆栈中设置的。在该堆栈中,esp 仍 然是 user_stack 中原来的位置,而 ss 被设置成 0x17,也即用户态局部表中的数据段,也即从内存地址 0 开始并且限长为 640KB 的段。

任务内核态堆栈与用户态堆栈之间的切换

任务调用系统调用时就会进入内核,执行内核代码。此时内核代码就会使用该任务的内核态堆栈进行操作。

当进入内核程序时,由于优先级别发生了改变(从用户态转到内核态),用户态堆栈的堆栈段和堆栈指针以及 eflags 会被保存在任务的内核态堆栈中。

在执行 iret 退出内核程序返回到用户程序时, 将恢复用户态的堆栈和 eflags。具体如图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_39021670/article/details/109218197