操作系统随笔-操作系统真象还原

复习操作系统真象还原,记录一些知识点

实模式内存布局


image.png

如下图所示: 编译器给程序中各符号(变量名或函数名等)分配的地址,就是各符号相对于文件开头的偏移量。


image.png

如下图所示:关键字 section 并没有对程序中的地址产生任何影响,即在默认情况下,有没有 section 都一个样, section 中数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用 。


image.png

如下图所示:
vstart xxx 和 orgxxxx 这两个关键字是同一功能,但很多同学都混淆其意义。它们并不是告诉编译器程序加载到地址 xxxx处。vstart 和 org ,它们的功能是告诉编译器:“嘿,老兄,你帮我把后面所有数据〈指令和变量 )的地址以 xxxx 为起始开始编吧”
用 vstart 的时机是:我预先知道我的程序将来被加载到某地址处。程序只有加载到非 0 地址时 vstart 才是有用的,程序默认起始地址是 0 。
mbr 用 vstart=Ox7c00 来修饰的原因,是因为开发人员知道 mbr 要被加载器( BIOS )加载到物理地址Ox7c00, mbr 中后续的物理地址都是 Ox7c00+。
只有开发人员知道以xxxx 为起始的原因是将来有某个加载器要把我的这个程序放到内存的 xxxx 地址,如果我程序中引用的所
有地址不是以口xx 为起始的,那就坏了,访问错的数据肯定出事。


image.png

关于汇编中vstart的作用:
https://blog.csdn.net/qq_39286701/article/details/118486106

实模式下的段寄存器:


image.png

实模式下的通用寄存器:


image.png

image.png

eflags寄存器:


image.png

CPU与IO通信:


image.png

从实模式进入保护模式:


image.png

image.png

段描述符表:


image.png

段界限计算方式:


image.png

全局描述符表:GDT
全局描述符表 GDT 相当于是描述符的数组,数组中的每个元素都是8 宇节的描述符。
GOT 中的第 0 个段描述符是不可用的,原因是定义在 GOT 中的段描述符是要用选择子来访问的,如果使用的选择子忘记初始化,选择子的值便会是 0,这便会访问到第 0 个段描述符。
lgdt 的指令格式是: lgdt 48 位内存数据。
在保护模式下重新换个 GDT 的原因是实模式下只能访问低端 IMB 空间,所以 GDT 只能位于 IMB 之内。根据操作系统的实际情况,有可能需要把 GDT 放在其他的内存位置,所以在进入保护模式后,访问的内存空间突破了 IMB,可以将 GDT 放在合适的位置后再重新加载进来。
如下图, GDT共48位,这 48 位内存数据划分为两部分,其中前 16 位是 GDT 以宇节为单位的界限值,所以这 16 位相当于GDT 的字节大小减 1 。后 32 位是 GDT 的起始地址。由于 GDT 的大小是 16 位二进制,其表示的范围是 2的 16 次方等于 65536 字节。每个描述符大小是 8 字节,故, GDT 中最多可容纳的描述符数量是 65536/8=8192个,即 GDT 中可容纳 8192 个段或门。


image.png

选择子:
段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 O~ 1 位,用来存储 RPL,即请求特权级,可以表示 0、 1 、 2、 3 四种特权级。
段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 O~ 1 位,用来存储 RPL,即请求特权级,可以表示 0、 1 、 2、 3 四种特权级。
段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 O~ 1 位,用来存储 RPL,即请求特权级,可以表示 0、 1 、 2、 3 四种特权级。


image.png

进入保护模式的步骤:
1 打开A20
2 加载gdt
3 将cr0的pe位置1

;----------------- 打开A20 ----------------
in al, 0x92
or al, 0000_0010B
out 0x92, al
;------------------ 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

进入保护模式后, jmp SELECTOR_CODE:p_mode_start ,使用远跳指令清空流水线, 同时更新段描述符缓冲寄存器。

在 Linux 2.6 内核中,是用 detect_memory 函数来获取内存容量的。其函数在本质上是通过调用 BIOS 中断 Ox15 实现的,分别
是 BIOS 中断 Ox15 的 3 个子功能,子功能号要存放到寄存器 EAX 或 AX 中,如下。
• EAX=OxE820:遍历主机上全部内存。
• AX=OxE801: 分别检测低 1见面和 16孔但~4GB 的内存,最大支持 4GB 。
• AH=Ox88:最多检测出 64MB 内存,实际内存超过此容量也按照 64MB 返回。

分页机制的思想是:

通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续 。
分页机制的作用有两方面 。
• 将线性地址转换成物理地址。
• 用大小相等的页代替大小不等的段。

image.png

一级页表缺点:
(1 )一级页表中最多可容纳 IM ( 1048576 )个页表项,每个页表项是 4 字节,如果页表项全满的话,
便是 4画面大小。
(2 )一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 IGB,
用户进程要占用低 3GB 。
(3 )每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。

二级页表:
每个页表中可容纳 1024 个物理页,故每个页表可表示的内存容量是 1024叫阻=4陋。页目录中共有1024 个页表,故所有页表可表示的内存容量是 1024*仙但=4GB ,这己经达到了 32 位地址空间的最大容量。
二级页表的转换:
(1 )用虚拟地址的高 10 位乘以 4 ,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
(2 )用虚拟地址的中间 1 0 位乘以 4 ,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。 -
(3 )虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PIE 的索引值,所以它们需要乘以 4 。但低 12 位就不是索引值啦,其表示的范围是 0~Ox筒,作为页内偏移最合适,所以虚拟地址的低 12 位加上第 2 步
中得到的物理页地址,所得的和便是最终转换的物理地址。


image.png
image.png
启用分页机制,我们要按顺序做好三件事 。

(1 )准备好页目录表及页表。
(2 )将页表地址写入控制寄存器 cr3 。
(3 )寄存器 c呻的 PG 位置 1 。
控制寄存器 cr3 用于存储页表物理地址,所以 cr3 寄存器又称为页目录基址寄存器( Page DirectorγBase 31 12 11 5 4 3 2 1 O
Register, PDBR ) 。

操作系统真象还原中的分页布局如下:


image.png

用虚拟地址获取页表中各数据类型的方法
获取页目录表物理地址:让虚拟地址的高 20 位为 0xfffff,低 12 位为 0x000 ,即 0xfffff000,这也是页目录表中第 0 个页目录项自身的物理地址。
访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为 0xfffffxxx,其中 xxx 是页目录项的索引乘以 4 的积 。

TLB ,即Translation Lookaside Buffer,俗称快表。TLB 中的条目是虚拟地址的高 20 位到物理地址高 20 位的映射结果,实际上就是从虚拟页框到物理页框的映射。除此之外 TLB中还有一些属性位,比如页表项的 RW 属性。
  TLB 对开发人员不可见,但依然有两种方法可以间接更新 TLB ,一个是针对 TLB 中所有条目的方法一一重新加载 CR3 ,比如将 CR3 寄存器的数据读出来后再写入 CR3 ,这会使整个 TLB 失效。另一个方法是针对 TLB 中某个条目的更新。处理器提供了指令 invlpg (invalidate page ),它用于在 TLB 中刷新, 某个虚拟地址对应的条目,
image.png

操作系统真象还原的硬盘分配:
MBR 写在了硬盘的第 0 扇区,第 1 扇区是空着的,原因是个人喜好,其实不空着也行,不过硬盘那么大,何必搞得那么拥挤呢。 因此 loader 写在硬盘的第 2 扇区,由于 loader.bin 目前的大小是 1342 字节,占用 3 个扇区,所以第 2~4 扇区不能再用啦,从第 5 扇区起我们可以自由使用。但此时我的强迫症又发作啦,我这里并没有接着第 5 扇区写,而是选的第 9 扇区(要是起始为 1 的话算是第 10 个扇区〉, 内核是从第9个扇区开始写的
dd if=$(BUILD_DIR)/kernel.bin
of=/usr/bochs/hd60M.img
bs=512 count=200 seek=9 conv=notrunc
至于为什么把 count 设成这么大,原因是这样的:每次写完内核后,咱们要往磁盘中同步内核文件,这样才能验证内核的正确性。按理说,咱们现在的内核文件不足 4 扇区,count=4 最合适。不过,内核发展越来越大时,每次都要根据实际内核文件大小去改写 count 参数,这样就难免会有忘记修改的情况。之前我就深受其苦, 内核文件变大了,而 count 忘记调整,造成写入硬盘中的内核文件不完整,所以到后来,程序运行不受控制,以至于调试的时候都调晕啦,看着 CPU 中跑的指令我完全蒙圈了,根本不是自己写的。恍然大悟之后,我就干脆一步到位,因为我们将来的内核大小不会超过 100kb,所以直接把 count 改为 200 块扇区。另外请大家不用担心, dd命令会自己判断写入的数据量,如果参数 if指定的文件体积小于 count*bs,只按实际文件大小写入。

image.png
image.png

操作系统真象还原的程序布局:


image.png

调用约定:


image.png
image.png

汇编风格:


image.png

第八章加入内存管理后,程序内存变化:
0xc009e000 是主线程的 PCB,占据一页
内核预计在 70KB 左右,装载到 0x9ff00 以下是绰绰有余的,所以,主线程的栈顶地址 0x9ff00是我们在低端 lMB 中所用到的最高地址 。
32MB 物理内存需要 1024 字节的位图,也就是仅占四分之一页,打算支持 4 页内存的位图,即最大可管理 512MB 的物理内存故再减去 4 页,即 0xc009e000 - 0x4000 = 0xc009a000。故我们的位图地址为 0xc009a000 。
页目录大小为 1 页框,第 0 和第 768 个页目录项指向同一个页表,它们共享这 1 页框空间,第 769~ 1022 个页目录项共指向 254 个页表,故页表总大小等于 256个PG_SIZE,共计 0x200000 字节,2M。注意,最后一个页目录项(第 1023 个 pde )指向页目录表,因此不重复计算空间。因此,1M内存外的2M空间是页目录表以及内核的页表。

分页中将32位虚拟地址映射成物理地址的三个步骤:

  1. 首先处理高 IO 位的 pde 索引,从而处理器得到页表物理地址。
  2. 其次处理中间 IO 位的 pte 索引,进而处理器得到普通物理页的物理地址 。
  3. 最后是把低 12 位作为普通物理页的页内偏移地址,此偏移地址加上物理页的物理地址,得到的地址之和便是最终的物理地址,处理器到此物理地址上进行读写操作。

第八章内存管理memory.c中几个重要的函数:
其中以下两个函数最难理解:
uint32_t* pte_ptr(uint32_t vaddr) /* 得到虚拟地址vaddr对应的pte指针/
uint32_t
pde_ptr(uint32_t vaddr) /* 得到虚拟地址vaddr对应的pde的指针 */

image.png

第九章 线程
总结一下: 上调度器的都是线程,能动的都是线程(哈哈哈哈)。

线程是一套机制,此机制可以为一般的代码块创造它所依赖的上下文环境,从而让代码块具有独立性,因此在原理上线程能使一段函数成为调度单元(或称为执行流),使函数能被调度器“认可”,从而能够被专门调度到处理器上执行。

进程拥有整个地址空间,其中包括各种资源,而进程中的所有线程共享同一个地址空间,原因很简单,因为这个地址空间中有线程运行所需要的资源。
由于各个进程都拥有自己的虚拟地址空间,正常情况下它们彼此无法访问到对方的内部,因为进程之间的安全性是由操作系统的分页机制来保证的,只要操作系统不要把相同的物理页分配给多个进程就行了。

强调下,只有线程才具备能动性,它才是处理器的执行单元,因此它是调度器眼中的调度单位。进程只是个资源整合体,它将进程中所有线程运行时用到资源收集在一起,供进程中的所有线程使用,真正上处理器上运行的其实都叫线程,进程中的线程才是一个个的执行实体、执行流,因此,经调度器送上处理器执行的程序都是钱程。


进程的六个提问:
1) 加载一个任务到处理器上运行,    任务由哪儿来???
2) 找到了任务, 资源从哪儿获得???
3) 任务变成了进程运行了,该运行多久呢???
4) 当前任务换下处理器后,当前进程使用的资源存放在哪儿呢???
5) 进程被换下的原因是什么?还能再把它换上处理器运行吗???
6) 进程独享地址空间,它的地址空间在哪儿???

一个简单的PCB结构:
image.png

PCB 中包含“进程状态飞,它解决了上面第 5 个问题,
“时间片”解决上面第 3 个问题,
“页表”解决了上面第 6 个问题,它代表进程的地址空间
“寄存器映像”是用来解决上面第 4 个问题的
其实第 4 个问题解决了,第 2 个问题也就一同搞定了,再从 PCB 中把寄存器映像加载到寄存器中就行了。
目前只剩下第 1 个问题没有解决了,其实,要解决此问题,就是要单独维护一个进程表,将所有的 PCB结构加载到此表中,由调度器直接在进程表中找相应进程的 PCB ,从而获取到对应进程的信息,将其寄存器映像加载到处理器后,新进程就开始运行了。

进程使用的战也属于 PCB 的一部分,不过此棋是进程所使用的 0 特权级下内核战(并不是 3 特权级下的用户栈) 。我们在 PCB 中还要维护一个“核指针 ” 成员,它记录0级栈栈顶的位置,借此找到进程或线程的“寄存器映像”。

实现线程的两种方式——用户/内核


image.png

讲个题外话,真理在代码中,多debug!!!! 抓重点!!! 梳理代码逻辑!!!!

核心是要理解ret指令:


image.png

汇编语言CALL和RET指令:调用一个过程
http://c.biancheng.net/view/3537.html

程序=算法+数据结构。 我感觉先定义数据结构,程序围绕着数据结构来写。

存储数据只是链表的部分功能,它最主要的功能是“链”。
下面两个宏,很重要!!!


image.png

其实我们从开机到创建第一个线程前,程序都有个执行流,这个执行流带我们从 BIOS 到 mbr 到 loader 到 kernel ,其实它就是我们所说的主线程 。 为此我们还在 loader 中把 esp 置为 Oxc009f000,这是有意为之的设计,意图是把 Oxc009e000 作为主线程的 PCB.

image.png
image.png

完整的程序=用户代码+内核代码

image.png

结合thread.h中PCB的结构体对照着看,内核级线程的切换真有意思:


image.png
image.png

第十章 输入输出系统

公共资源:可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
临界区: 各任务中访问公共资源的指令代码组成的区域就称为临界区。 #即临界区是指令
竞争条件: 多个任务以非互斥的方式同时进入临界区

多线程访问公共资源时出问题的原因是产生了竞争条件,也就是多个任务同时出现在自己的临界区 。

信号量是种同步机制

image.png

第十一章 用户进程

文档是应用程序的应用,应用程序是编译器的应用,编译器又是操作系统的应用 。 那操作系统是谁的应用?当然是硬件的应用啦。

TSS 是 Task State Segment 的缩写,即任务状态段

正是由于好奇心特别大,所以才会想了解操作系统的原理。—— 这句话,我是赞同的。

为什么 GDT 中第 0 个段描述符不可用? 是担心选择子未初始化

image.png

TSS 中的字段基本上全是寄存器名称,这些寄存器就是任务运行中的最新状态。


image.png

我们使用 TSS 唯一的理由是为 0 特权级的任务提供栈。

用户进程进入内核态后,除了拥有单独的地址空间外,其他方面和内核线程是一样的。

image.png
image.png

有个疑问:
下面这句代码,我能理解它是对的,但我感觉最开始初始化用户进程的时候,self_kstack一开始肯定是在固定的位置的。。。。


image.png

第十二章 进一步完善内核
“系统调用”准确地来说应该被称为“操作系统功能调用”
Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信号。
Linux 只占用一个中断向量号,即 Ox80
Linux 在寄存器 eax中写入子功能号

完善堆内存管理:
arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲内存块汇聚到一起,作为某一规格内存块的分配入口。

image.png

核心是下面三个函数:
sys_malloc 申请内存的函数 多看看,有意思,厉害
mfree_page 释放内存的函数
sys_free 释放内存的函数
整个memory.c文件,多看看,挺有意思的。比,写if, else有意思多了!!!!!!!!!!!

第十三章 编写硬盘驱动程序
文件系统是运行在操作系统中的软件模块,是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式,因此,文件系统=数据结构+算法,哈哈,所以它是程序。

磁盘容量:


image.png

分区,逻辑分区;
老实说,没太看懂里面的概念,不过看看这张图,大致了解了。。。。


image.png

硬件是实实在在的东西,要想在软件中管理它们,只能从逻辑上抓住这些硬件的特性,将它们抽象成一些数
据结构,然后这些数据结构便代表了硬件,用这些数据结构来组织硬件的信息及状态,在逻辑上硬件就是这数据
结构。

硬盘驱动程序 获取硬盘信息 扫描分区表 =>有一种软件与硬件的交互的美感, 有种抽象控制硬件的感觉!!!需要多看看,没太看懂!!!!

最重要的是:
intr_hd_handler 硬盘中断处理程序
ide_read 从硬盘读取sec_cnt个扇区到buf
partition_scan 扫描硬盘hd中地址为ext_lba的扇区中的所有分区
硬盘工作完成后,会调用intr_hd_handler 中断函数,才会继续向下走。。。。

/* ata通道结构 */
struct ide_channel {
char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。
uint16_t port_base; // 本通道的起始端口号
uint8_t irq_no; // 本通道所用的中断号
struct lock lock;
bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断
struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒
struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};

这个结构中为什么要一个lock, 加上一个 信号量disk_done, 这个没搞懂?????

lock的作用: 操作硬盘之前先将硬盘所在的通道上锁,从而保证一次只操作同一通道上的一块硬盘。
信号量的作用是: 在硬盘工作过程中让出CPU,通过信号量阻塞自己

第十四章 文件系统
inode 是文件索引结构组织形式的具体体现,必须为每个文件者睁独配备一个划羊的元信息数据结构,因此在UINX 文件系统中,一个文件必须对应一个 inode,磁盘中有多少文件就有多少 inode。

image.png
image.png

在 Linux 中,目录和文件都用 inode 来表示,因此目录也是文件,只是目录是包含文件的文件 。
文件名应该存储在和目录相关的地方
在磁盘上的文件系统中 (注意啦,我说的是磁盘上),没有一种专门称为目录的数据结构,磁盘上有的只是 inode,

image.png

有了目录项后,通过文件名找文件实体数据块的流程是 。
( l )在目录中找到文件名所在的目录项 。
(2 )从目录项中获取 inode 编号 。
(3 )用 inode 编号作为 inode 数组的索引下标,找到 inode
(4 )从该 inode 中获取数据块的地址,读取数据块 。

创建文件的本质是创建了文件的文件控制块,即目录项和 inode

image.png

我们需要在某个固定地方去获取文件系统元信息的配置,这个地方就是超级块,超级块是保存文件系统元信息的元信息。

image.png
image.png

由于 inode 是从硬盘上保存的 , 文件被打开时, 肯定是先要从硬盘上载入其 inode,但硬盘比较慢, 为了避免下次再打开该文件时还要从硬盘上重复载入 inode,应该在该文件第一次被打开时就将其 inode加入到 内存缓存中,每次打开一个文件时,先在此缓冲中查找相关的 inode , 如果有就直接使用, 否则再从硬盘上读取 inode,然后再加入此缓存

image.png

挂载分区的实质是把该分区文件系统的元信息从硬盘上读出来加载到内存中,这样硬盘资源的变化都用内存中元信息来跟踪。如果有写操作,及时将内存中的元信息同步写入到硬盘以持久化

文件描述符所描述的对象是文件的操作。
每次打开一个文件就会产生一个文件结构
文件描述符确实只是个整数,准确地说,它是 PCB 中文件描述符数组元素的下标


image.png
image.png
image.png
image.png

有三个标准的文件描述符, 0 是标准输入, 1 是标准输出, 2 是标准错误

磁盘是一种低速设备,因此文件系统的设计原则是尽量减少硬盘操作。

inode 队列中的所有 inode 应该被所有任务共享,包括内核线程和进程

第十五章 系统交互

fork复制进程资源,然后执行


image.png
image.png

孤儿进程和僵尸进程
当父进程提前退出时,它所有的子进程还在运行,没有一个执行了 exit,因为它们的生命周期尚未结束,还在运行中,个个都拥有“全尸”(进程体),这些进程就称为孤儿进程。
如果父进程在派生出子进程后井没有调用 wait 等待接收子进程的返回值,这时某个子进程调用 exit 退出了,自然没人来接收返回值了(父进程未退出,因此子进程不能过继给 init, init 也不能帮子进程做善后收尸,只有父进程才有权限为子进程收尸),因此其 pcb 所占的空间不能释放,没人为其“收尸飞自然就成了“僵尸”。

image.png

管道是个环形缓冲区

管道有两端,→端用于从管道中读入数据,另一端用于往管道中写入数据。这两端使用文件描述符的方式来读取,故进程创建管道实际上是内核为其返回了用于读取管道缓冲区的文件描述符, 一个描述符用于读,另一个描述符用于写。

image.png

管道的用法: 进程在创建管道之后马上调用 fork,克隆出一个子进程,子进程完全继承了父进程的一切,也就是说和父进程一模一样,因此也继承了管道的描述符,这为父子进程通信提供了保证。

image.png
image.png
image.png

你可能感兴趣的:(操作系统随笔-操作系统真象还原)