Linux0.11 内核体系结构(八)(上)

简介: Linux0.11 内核体系结构(八)

一、Linux 对内存的管理和使用

1、内存分段机制

   虚拟地址 通过分段机制映射到 线性地址4G 寻址空间),然后是 线性地址物理地址 的变换。

下图列出了 GDTLDT 描述符信息图:

2、内存分页管理

下图显示了分页变换的过程:

3、系统内存空间划分

 对于 Linux 0.11 系统,内核设置全局描述符表 GDT 中的段描述符项数最大为 256,其中 2 项空闲、2 项系统使用,每个进程使用两项。因此,此时系统可以最多容(256-4)/ 2 = 126 个任务,并且虚拟地址范围是 ((256-4)/ 2)* 64MB 约等于 8G。但 0.11 内核中人工定义最大任务数 NR_TASKS = 64 个,每个任务逻辑地址范围是 64M,并且各个任务在线性地址空间中的起始位置是(任务号)* 64MB。因此全部任务所使用的线性地址空间范围是 64MB*64 =4G,见图 5-10 所示。图中示出了当系统具有 4 个任务时的情况。内核代码段和数据段被映射到线性地址空间的开始 16MB 部分,并且代码和数据段都映射到同一个区域,完全互相重叠。而第 1个任务(任务 0)是由内核"人工"启动运行的,其代码和数据包含在内核代码和数据中,因此该任务所占用的线性地址空间范围比较特殊。任务 0 的代码段和数据段的长度是从线性地址 0 开始的 640KB 范围,其代码和数据段也完全重叠,并且与内核代码段和数据段有重叠的部分。实际上,Linux 0.11 中所有任务的指令空间 I(Instruction)和数据空间 D(Data)都合用一块内存,即一个进程的所有代码、数据和堆栈部分都处于同一内存段中,也即是 I&D 不分离的一种使用方式。

  任务 1 的线性地址空间范围也只有从 64MB 开始的 640KB 长度。它们之间的详细对应关系见后面说明。任务 2 和任务 3 分别被映射线性地址 128MB 和 192MB 开始的地方,并且它们的逻辑地址范围均是 64MB。由于 4G 地址空间范围正好是 CPU 的线性地址空间范围和可寻址的最大物理地址空间范围,而且在把任务 0 和任务 1 的逻辑地址范围看作 64MB 时,系统中同时可有任务的逻辑地址范围总和也是 4GB,因此在 0.11 内核中比较容易混滑三种地址概念。

   如果也按照线性空间中任务的排列顺序排列虚拟空间中的任务,那么我们可以有图 5-11 所示的系统同时可拥有所有任务在虚拟地址空间中的示意图,所占用虚拟空间范围也是 4GB。其中没有考虑内核代码和数据在虚拟空间中所占用的范围。另外,在图中对于进程 2 和进程 3 还分别给出了各自逻辑空间中代码段和数据段(包括数据和堆栈内容)的位置示意图。


  请还需注意,进程逻辑地址空间中代码段(Code Section)和数据段(Data Section)的概念与 CPU 分段机制中的代码段和数据段不是同一个概念。CPU 分段机制中段的概念确定了在线性地址空间中一个段的用途以及被执行或访问的约束和限制,每个段可以设置在 4GB 线性地址空间中的任何地方,它们可以相互独立也可以完全重叠或部分重叠。而进程在其逻辑地址空间中的代码段和数据段则是指由编译器在编译程序和操作系统在加载程序时规定的在进程逻辑空间中顺序排列的代码区域、初始化和未初始化的数据区域以及堆栈区域。进程逻辑地址空间中代码段和数据段等结构形式见图所示。有关逻辑地址空间的说明请参见内存管理一章内容。

4、CPU 多任务和保护方式

  Intel 80X86 CPU 共分 4 个保护级,0 级具有最高优先级,而 3 级优先级最低。Linux 0.11 操作系统使用了 CPU 的 0 和 3 两个保护级。内核代码本身会由系统中的所有任务共享。而每个任务则都有自己的代码和数据区,这两个区域保存于局部地址空间,因此系统中的其他任务是看不见的(不能访问的)。而内核代码和数据是由所有任务共享的,因此它保存在全局地址空间中。图 5-13 给出了这种结构的示意图。图中同心圆代表 CPU 的保护级别(保护层),这里仅使用了 CPU 的 0 级和 3 级。而径向射线则用来区分系统中的各个任务。每条径向射线指出了各任务的边界。除了每个任务虚拟地址空间的全局地址区域,任务 1 中的地址与任务 2 中相同地址处是无关的。

  当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0 级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3 级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。进程的内核态和用户态将在后面有关进程运行状态一节中作更详细的说明。

任务 0 的地址对应关系

  任务 0 是系统中一个人工启动的第一个任务。它的代码段和数据段长度被设置为 640KB。该任务的代码和数据直接包含在内核代码和数据中,是从线性地址 0 开始的 640KB 内容,因此可以它直接使用内核代码已经设置好的页目录和页表进行分页地址变换。同样,它的代码和数据段在线性地址空间中也是重叠的。对应的任务状态段 TSS0 也是手工预设置好的,并且位于任务 0 数据结构信息中,参见 sched.h 第 113 行开始的数据。TSS0 段位于内核 sched.c 程序的代码中,长度为 104 字节,具体位置可参见图 5-23 中"任务 0 结构信息"一项所示。三个地址空间中的映射对应关系见图 5-15 所示。

   由于任务 0 直接被包含在内核代码中,因此不需要为其再另外分配内存页。它运行时所需要的内核态堆栈用户态堆栈空间也都在内核代码区中, 并且由于在内核初始化时(head.s)这些内核页面在页表项中的属性都已经被设置成了 0b111,即对应页面用户可读写并且存在,因此用户堆栈 user_stack[] 空间虽然在内核空间中,但任务 0 仍然能对其进行读写操作。

任务 1 的地址对应关系

  与任务 0 类似,任务 1 也是一个特殊的任务。它的代码也在内核代码区域中。与任务 0 不同的是在线性地址空间中,系统在使用 fork() 创建任务 1(init 进程)时为存放任务 1 的二级页表而在主内存区申请了一页内存来存放,并复制了父进程(任务 0)的页目录和二级页表项。因此任务 1 有自己的页目录和页表表项,它把任务 1 占用的线性空间范围 64MB –128MB(实际上是 64MB – 64MB+640KB)也同样映射到了物理地址 0–640KB 处。此时任务 1 的长度也是 640KB,并且其代码段和数据段相重叠,只占用一个页目录项和一个二级页表。另外,系统还会为任务 1 在主内存区域中申请一页内存用来存放它的任务数据结构和用作任务 1 的内核堆栈空间。任务数据结构(也称进程控制块 PCB)信息中包括任务 1 的 TSS 段结构信息。见图 5-16 所示。


  任务 1 的用户态堆栈空间将直接共享使用处于内核代码和数据区域(线性地址 0–640KB)中任务 0 的用户态堆栈空间 user_stack[](参见 kernel/sched.c,第 67–72 行),因此这个堆栈需要在任务 1 实际使用之前保持"干净",以确保被复制用于任务 1 的堆栈不含有无用数据。在刚开始创建任务 1 时,任务 0 的用户态堆栈 user_stack[]与任务 1 共享使用,但当任务 1 开始运行时,由于任务 1 映射到 user_stack[] 处的页表项被设置成只读,使得任务 1 在执行堆栈操作时将会引起写页面异常,从而由内核另行分配主内存区页面作为堆栈空间使用。

其他任务的地址对应关系

  对于被创建的从任务 2 开始的其他任务,它们的父进程都是 init(任务 1)进程。我们已经知道,在 Linux 0.11 系统中共可以有 64 个进程同时存在。下面我们以任务 2 为例来说明其他任何任务对地址空间的使用情况。

  从任务 2 开始,如果任务号以 nr 来表示,那么任务 nr 在线性地址空间中的起始位置将被设定在 nr * 64MB 处。例如任务 2 的开始位置 = nr*64MB = 2 * 64MB = 128MB。任务代码段和数据段的最大长度被设置为 64MB,因此任务 2 占有的线性地址空间范围是 128MB–192MB,共占用 64MB/4MB = 16 个页目录项。虚拟空间中任务代码段和数据段都被映射到线性地址空间相同的范围,因此它们也完全重叠。图 5-17 显示出了任务 2 的代码段和数据段在三种地址空间中的对应关系。

  在任务 2 被创建出来之后,将在其中运行 execve()函数来执行 shell 程序。当内核通过复制任务 1 刚创建任务 2 时,除了占用线性地址空间范围不同外(128MB–128MB+640KB),此时任务 2 的代码和数据在三种地址空间中的关系与任务 1 的类似。当任务 2 的代码(init())调用 execve()系统调用开始加载并执行 shell 程序时,该系统调用会释放掉从任务 1 复制的页目录和页表表项及相应内存页面,然后为新的执行程序 shell 重新设置相关页目录和页表表项。图 5-17 给出的是任务 2 中开始执行 shell 程序时的情况,即任务 2 原先复制任务 1 的代码和数据被 shell 程序的代码段和数据段替换后的情况。图中显示出已经映射了一页物理内存页面的情况。这里请注意,在执行 execve()函数时,系统虽然在线性地址空间为任务 2 分配了 64MB 的空间范围,但是内核并不会立刻为其分配和映射物理内存页面。只有当任务 2 开始执行时由于发生缺页而引起异常时才会由内存管理程序为其在主内存区中分配并映射一页物理内存到其线性地址空间中。这种分配和映射物理内存页面的方法称为需求加载(Load on demand)。

   从 Linux 内核 0.99 版以后,对内存空间的使用方式发生了变化。每个进程可以单独享用整个 4G 的地址空间范围。如果我们能理解本节描述的内存管理概念,那么对于现在所使用的 Linux 2.x 内核中所使用的内存管理原理也能立刻明白。

5、用户申请内存的动态分配

  当用户应用程序使用 C 函数库中的内存分配函数 malloc()申请内存时,这些动态申请的内存容量或大小均由高层次的 C 库函数 malloc()来进行管理,内核本身并不会插手管理。因为内核已经为每个进程(除了任务 0 和 1,它们与内核代码一起常驻内存中)在 CPU 的 4G 线性地址空间中分配了 64MB 的空间,所以只要进程执行时寻址的范围在它的 64MB 范围内,内核也同样会通过内存缺页管理机制自动为寻址对应的页面分配物理内存页面并进行映射操作。但是内核会为进程使用的代码和数据空间维护一个当前位置值 brk,这个值保存在每个进程的数据结构中。它指出了进程代码和数据(包括动态分配的数据空间)在进程地址空间中的末端位置。当 malloc()函数为程序分配内存时,它会通过系统调用 brk()把程序要求新增的空间长度通知内核,内核代码从而可以根据 malloc()所提供的信息来更新 brk 的值,但此时并不为新申请的空间映射物理内存页面。只有当程序寻址到某个不存在对应物理页面的地址时,内核才会进行相关物理内存页面的映射操作。

  若进程代码寻址的某个数据所在的页面不存在,并且该页面所处位置属于进程堆范围,即不属于其执行文件映像文件对应的内存范围中,那么 CPU 就会产生一个缺页异常,并在异常处理程序中为指定的页面分配并映射一页物理内存页面。至于用户程序此次申请内存的字节长度数量和在对应物理页面中的具体位置,则均由 C 库中内存分配函数 malloc()负责管理。内核以页面为单位分配和映射物理内存,该函数则具体记录用户程序使用了一页内存的多少字节。剩余的容量将保留给程序再申请内存时使用。

  当用户使用内存释放函数 free()动态释放已申请的内存块时,C 库中的内存管理函数就会把所释放的内存块标记为空闲,以备程序再次申请内存时使用。在这个过程中内核为该进程所分配的这个物理页面并不会被释放掉。只有当进程最终结束时内核才会全面收回已分配和映射到该进程地址空间范围的所有物理内存页面。

有关库函数 malloc()和 free()的具体代码实现请参见内核库中的 lib/malloc.c 程序。

Linux0.11 内核体系结构(八)(下):https://developer.aliyun.com/article/1597299

目录
相关文章
|
7天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
23 3
|
17天前
|
缓存 安全 Unix
Linux 内核黑客不可靠指南【ChatGPT】
Linux 内核黑客不可靠指南【ChatGPT】
|
17天前
|
Linux 开发者
Linux内核贡献成熟度模型 【ChatGPT】
Linux内核贡献成熟度模型 【ChatGPT】
|
16天前
|
网络协议 Ubuntu Linux
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
|
16天前
|
Linux
用clang编译Linux内核
用clang编译Linux内核
|
17天前
|
Linux API C语言
Linux 内核补丁提交的清单 【ChatGPT】
Linux 内核补丁提交的清单 【ChatGPT】
|
17天前
|
安全 Linux 开发者
Linux内核管理风格 【ChatGPT】
Linux内核管理风格 【ChatGPT】
|
17天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
17天前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
17天前
|
Unix Linux API
Linux内核许可规则 【ChatGPT】
Linux内核许可规则 【ChatGPT】