第十一章
任意进程的页目录表第0~767个页目录项属于用户空间,指向用户页表。第768~1023个页目录项指向内核页表。每创建一个新的用户进程,就将内核页目录项复制到用户进程的页目录表,其次需要把用户页目录表中最后一个页目录项更新为用户进程自己的页目录表的物理地址。
每个进程有自己单独的位图,存储在进程pcb中的userprog_vaddr中。
LDT
当前运行的任务,其LDT位于LDTR指向的地址(选择子)。每切换一个任务时,需要lldt指令重新加载新任务的LDT到LDTR
TSS
CPU自动用此结构体变量保存任务的状态(任务的上下文环境,寄存器的值)和自动从此结构体变量中载入任务的状态。TR寄存器始终指向当前任务的TSS,这样每个任务必须有单独的TSS,但Linux并未这么做。Linux所有任务共享一个TSS。
除了中断和调用门返回外,CPU不允许从高特权级到低特权级。CPU在不同的特权级下使用不同的栈。
TSS和LDT一样,必须要在GDT中注册。
现代操作系统采用的任务切换方式
我们使用TSS的唯一理由是为0特权级的任务提供栈。
Linux中为每个CPU创建一个TSS,在各个CPU上的所有任务共享同一个TSS,各CPU的TR寄存器保存各自的TSS,在用ltr指令加载TSS后,TR寄存器永远指向同一个TSS,进程切换时只把这个TSS中的SS0和esp0更新为新任务的内核栈的段地址和栈指针。
第十二章
系统调用
Linux只占用一个中断向量号0X80,在寄存器eax中写入子功能号。所有系统调用都可以通过syscall函数(不是由系统提供的,是glibc提供的库函数,直接系统调用为_syscall)完成。总之对用户进程而言,在 Linux 上执行系统调用,只需要提供子功能号和参数就行了。
堆内存管理
arena
将一大块内存划分为无数小内存块的内存仓库。
arena 是个提供内在分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块数量,这其中包括内存块描述符指针(后面介绍),通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。另一部分就是内存地区域,这里面有无数的内存块,此部分占用 arena 大量的空间。
本书中针对小内存块的arena占用1页框内存。
每个内存块命名为mem_block,分别为每一种规格的内存块建立一个内存块描述符即mem_bloc_desc。
struct mem_block_desc (
uint32 t block size //内存块大小
uint32_t blocks_per_arena; //本arena中可容纳此 mem_block 的数量
struct list free list //目前可用同类的mem_block链表
实现sys_malloc
传入参数size,代表申请多少字节内存
1.首先判断用哪个内存池,内核还是用户
2.若申请的内存不在内存池容量范围内,直接返回NULL
3.超过1024就直接分配页框
4.若小于等于1024,循环各种规格,找到最合适的规格
5.判断该规格free_list是否为空,若mem_block_desc的free_list中已经没有可用的mem_block,就创建新的arena提供mem_block
6.从free_list中弹出一个内存块,通过elem2entry宏转换成mem_block的地址,返回内存块地址
内存释放(页框级别)
分配内存的步骤
1.虚拟地址池中分配虚拟地址,操作位图
2.物理内存池中分配物理地址,操作位图
3.完成虚拟地址和物理地址的映射
释放内存的步骤
1.释放物理页地址,操作位图
2.在页表中去掉虚拟地址的映射,将虚拟地址对应pte的P位置0
3.在虚拟地址中释放虚拟地址,操作位图
当物理内存不多时,就将其数据移到硬盘中,然后对应页表项pte的P位置0,当CPU访问时会引发pagefault中断,中断处理程序将数据物理页更新到pte中,再将P位置1,CPU会再次访问引起pagefault的虚拟地址。
实现sys_free
对于大内存,就是把页框在虚拟内存池和物理内存池的位图中相应位置0。
对于小内存,是将arena中的内存块重新放回到内存块描述符的空闲块链表free_list。
第十三章
编写硬盘驱动(略)
第十四章
硬盘的读写单位是扇区,数据一般积攒到足够大小才一次性访问硬盘,足够大小的数据就是块,一个块由多个扇区构成。
FAT32
用链表的方式来连接每个数据块,查询某个数据块很耗时
inode
控制,管理文件相关信息的数据结构是FCB,inode就是其中一种。
用索引来查找数据块,在UNIX系统中,一个文件必须对应一个inode(索引表),磁盘中有多少文件就有多少个inode。inode的结构如下如图,前12个索引为直接指针,后面的3个为间接索引的地址。每个间接索引表都可存256个块。
在Linux中每分区inode数量是固定的,分区中所有文件的inode通过一个大表格来维护,此表格称为inode_table。
目录项
通过文件名找文件实体数据的流程是:
1.在目录中找到文件名所在的目录项
2.从目录项中获取inode编号
3.用inode编号作为inode数组的索引下标,找到inode
4.从该inode中获取数据块的地址,读取数据块
目录项仅存在于inode指向的数据块中,有目录项的数据块就是目录,目录项所属的inode指向的所有数据块便是目录。
每个分区都有自己的根目录,根目录/的位置是固定不变的,查找任意文件时,都直接到根目录的数据块中找相关的目录项,然后递归查找,最终可以找到任意子目录中的文件。
超级块
超级块是保存文件系统元信息的元信息。它被固定在各分区的第2个扇区。
文件系统布局
Linux早期文件系统布局如下图
第十五章
fork
fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
为什么fork后父子进程同一个变量地址打印出来相同
假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。
fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。
每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
具体过程是这样的:
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,知道其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。
这就是所谓的“写时复制”。正因为fork采用了这种写时复制的机制,所以fork出来子进程之后,父子进程哪个先调度呢?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。。。,这就避免了“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度滴。
(注1:在理解时,你可以认为fork后,这两个相同的虚拟地址指向的是不同的物理地址,这样方便理解父子进程之间的独立性)
(注2:但实际上,Linux为了提高 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))
wait和exit
进程间通信必须要借助内核(无论管道、消息队列、还是共享内存等进程间通信形式),子进程的返回值肯定是先交给内核,然后父进程向内核要子进程的返回值。父进程调用pid_t wait(int *status)后,内核就把子进程的返回值存储到status指向的内存空间。子进程的返回值存放在它的PCB中,调用exit后内核会把进程占用的大部分资源回收,比如内存、页表等,但不能回收PCB,需要将其中的返回值交给父进程后才能回收。
exit调用表面上是结束子进程运行并传递返回值给内核,本质上是内核在幕后将进程除了PCB以外的所有资源回收。
孤儿进程
父进程提前退出,父进程的子进程会称为孤儿进程,被init进程收养
僵尸进程
僵尸进程就是没有父进程来给某进程收尸,也就是调用wait收取返回值,因此其PCB一直不能被回收。
管道
Linux中管道实现如下图:
其实就是对一页框大小的内存区域做读写操作
匿名管道:在内核中,父子进程因为有相同的管道描述符,所以都可以访问这个管道。
有名管道:在文件系统中创建一个管道文件(FIFO,是一种特殊的文件类型),使得该管道对任何进程都可见,因此没有父子关系也能通信。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。
Linux共享内存,信号
http://blog.csdn.net/lqygame/article/details/71424917
http://blog.csdn.net/lqygame/article/details/73555430
写在最后
每天晚上看一点,历时接近1个月终于把这本书读完,感谢钢哥,这本书刷新了我对操作系统的认知,总算对操作系统有了个很全面的了解。这些笔记是针对我自己的一些感觉需要记录下来的东西,OS还需要学习的东西还有很多啊。
---------------------
作者:月黑风高云游诗人
来源:CSDN
原文:https://blog.csdn.net/lqygame/article/details/73850249
版权声明:本文为博主原创文章,转载请附上博文链接!