当前位置: 首页 > 科技观察

操作系统是一个由这两个访问结构管理的缓冲区

时间:2023-03-20 17:21:44 科技观察

新读者看这里,老读者直接跳过。本系列将带你以看小说的心态,从开机后的代码执行顺序开始阅读领略Linux0.11的所有核心代码,了解操作系统的技术细节和设计思路。你将跟随我,见证一个操作系统从无到有,一步步实现其复杂精妙的设计。看完这个系列,希望你能感叹一下,原来操作系统的源代码就是这狗屎。本书接续上一章。上一本书讲到进程调度的初始化,定义了一个长度为64的任务数组来管理所有进程的结构体。然后在GDT中预定义了进程调度所需要的TSS和LDT结构。之后开启定时器,准备迎接时钟中断的到来,进而触发进程调度。那么我们静下心来,回到main函数,继续看接下来的初始化过程。那就是缓冲区初始化buffer_init,来吧,所剩无几了!voidmain(void){...mem_init(main_memory_start,memory_end);陷阱初始化();blk_dev_init();chr_dev_init();tty_init();时间初始化();计划初始化();buffer_init(buffer_memory_end);高清初始化();软盘初始化();斯蒂();move_to_user_mode();如果(!fork()){初始化();}for(;;)pause();}首先要注意的是这个函数传递了一个参数buffer_memory_end,这个参数早就设置好了。就在第12章之前|管理内存,分了三个边界值,我们复习一下。记住?而我们在第13章|中使用mem_init设置了主内存管理结构mam_map|主内存初始化mem_init。如果记不住了,就需要把前面的章节再看一遍,不然后面会越来越难。前面管理了主存区,所以今天剩下的缓冲区也进行了初始化和管理。目的就这么简单,我们来看代码。我们还是沿用之前的方法,假设内存只有8M,去掉一些不相关的分支,方便理解。externintend;structbuffer_head*start_buffer=(structbuffer_head*)&end;voidbuffer_init(longbuffer_end){structbuffer_head*h=start_buffer;void*b=(void*)buffer_end;while((b-=1024)>=((void*)(h+1))){h->b_dev=0;h->b_dirt=0;h->b_count=0;h->b_lock=0;h->b_uptodate=0;h->b_wait=NULL;h->b_next=NULL;h->b_prev=NULL;h->b_data=(char*)b;h->b_prev_free=h-1;h->b_next_free=h+1;h++;}H-;free_list=start_buffer;free_list->b_prev_free=h;h->b_next_free=free_list;for(inti=0;i<307;i++)hash_table[i]=NULL;}虽然很长,但实际上只是创建了两个数据结构。但是等等,让我们先看看这行代码。externintend;voidbuffer_init(longbuffer_end){structbuffer_head*start_buffer=(structbuffer_head*)&end;...}这里有一个外部变量end,我们buffer的start_buffer就等于这个变量的内存地址。这个外部变量end并不是操作系统代码写的,而是链接器ld在链接整个程序时设置的一个外部变量,帮助我们计算出整个内核代码的结束地址。在此之前,内核代码区不得使用,在此之后,用于缓冲。所以我们的内存分布图可以更准确。你看,之前的疑惑都解开了?这很容易理解。内核程序和缓冲区之间必须有一个分界线。这条分界线就是结束变量的值。这个值多少合适?比如代码中直接写了主存和buffer的分界线,就是上图中的2M。但是内核程序写的时候不知道占用多少内存。即使我知道,我改一点代码,它也会改变,所以在程序编译链接的时候,链接器程序会帮我们计算出内核代码的结束地址,作为一个外部变量end是方便我们立即使用。好吧,我们回头看看,整个代码创建了哪两个管理结构?我们先看看这个结构。voidbuffer_init(longbuffer_end){structbuffer_head*h=start_buffer;void*b=(void*)buffer_end;while((b-=1024)>=((void*)(h+1))){...h->b_data=(char*)b;h->b_prev_free=h-1;h->b_next_free=h+1;h++;}...}只有两个变量。一个是buffer_head结构体的h,代表bufferhead,它的指针值为start_buffer,就是我们刚才计算出来的,也就是图中kernelcodeend的结束地址,也就是buffer的开始。一个是b,代表bufferblock,指针值为buffer_end,图中为2M,也就是buffer的结束。buffer末尾的b每次循环-1024,也就是一页的值,buffer末尾的h每次循环+1(一块buffer_head大小的内存),直到碰到一个block。可以看出b其实代表bufferblock,h代表bufferhead,一个从上到下,一个从下到上。并且在这个过程中,h附加了一个属性值,关键点就是这个buffer所代表的数据部分b_data,指向上面的bufferblockb。还有这个buffer前后的freebuffer的指针b_prev_free和b_next_free。即绘制如下。当缓冲区头h的所有next和prev指针都指向彼此时,就形成了一个双向链表。继续阅读。voidbuffer_init(longbuffer_end){...free_list=start_buffer;free_list->b_prev_free=h;h->b_next_free=free_list;...}这三行代码,结合刚才的双向链表h,我画图,你我搞定。看,free_list指向bufferhead双向链表的第一个结构体,然后可以按照这个结构体从双向链表中遍历到任意一个bufferhead结构体,通过bufferhead对应的bufferblock缓冲头。简单的说,bufferheader就是具体bufferblock的管理结构,free_list开头的双向链表就是bufferheader的管理结构,整个管理体系就是这样建立起来的。现在,从free_list开始遍历,找到这里的所有内容。然而,还有最后一件事可以帮助更好地管理,向下看。voidbuffer_init(longbuffer_end){...for(i=0;i<307;i++)hash_table[i]=NULL;}一个大小为307的hash_table数组,这是做什么用的?其实今天的代码是在buffer.c中的,而buffer.c在fs包下,也就是文件系统包下。所以以后会服务于文件系统。具体来说,如果内核程序需要访问块设备中的数据,就需要通过缓冲区间接进行操作。也就是说,要读取块设备的数据(硬盘中的数据),需要先读入缓冲区。如果缓冲区已经存在,则不需要从块设备中读取,直接拿走即可。那么怎么知道buffer中已经有blockdevice中的数据要读取了呢?当然可以从双向链表的开头开始遍历,但是这样效率太低了。所以需要一个hashmap结构来进行快速查找,这就是hash_table数组的作用。现在我只是初始化这个hash_table,并没有在任何地方使用它,所以我先简单地剧透一下。之后,当要读取某个块设备上的数据时,首先要搜索对应的缓冲块,也就是下面的函数。#define_hashfn(dev,block)(((unsigned)(dev^block))%307)#definehash(dev,block)hash_table[_hashfn(dev,block)]//搜索合适的bufferblockstructbuffer_head*getblk(intdev,intblock){...structbuffer_headbh=get_hash_table(dev,block);...}structbuffer_head*get_hash_table(intdev,intblock){...find_buffer(dev,block);...}staticstructbuffer_head*find_buffer(intdev,intblock){...hash(dev,block);...}一路往下找,也就是通过dev^block%307(设备号^逻辑块号)Mod307在hash_table中找到索引下标,然后就类似于Java中的HashMap。如果hash冲突,就形成一个链表,画图的时候是这样的。哈希表+双向链表,如果算法题太多,很容易认为这样可以实现LRU算法。是的,就是这个算法对buffer的后续使用和弃用起到了一定的作用。也就是说,以后通过文件系统读取硬盘文件时,需要使用和丢弃这个缓冲区中的内容。缓冲区是用户进程的内存和硬盘之间的桥梁。好了,再多说几句文件系统中的读操作。压力太大了。这一章主要是了解这个buffer的管理是如何初始化的,为后面做铺垫。让我们回顾一下我们目前的进展!voidmain(void){...mem_init(main_memory_start,memory_end);陷阱初始化();blk_dev_init();chr_dev_init();tty_init();时间初始化();计划初始化();buffer_init(buffer_memory_end);高清初始化();软盘初始化();斯蒂();move_to_user_mode();if(!fork()){init();}for(;;)pause();}整个初始化的一部分,唯一缺少的就是hd_init和floppy_init这两个块设备的初始化。幸运的是,floppy_init是软盘的初始化。现在软盘几乎淘汰了,电脑里也没有软盘驱动器,所以我们完全可以忽略这个,然后就只有一个hd_init来初始化硬盘了。这很简单!还记得小时候特别喜欢收集软盘,把自己制作的Flash动画分门别类地存放在里面,然后在软盘上的纸标签上写上文字,表示软盘上存放的是什么.想一想或记住它。离这很远。前面各种初始化任务建立的数据结构,将在后面的模块中发挥最核心的作用。任何操作系统的管理都离不开这些初始化任务所建立的数据结构,所以我们必须把这些基础打好,不用担心。当所有的初始化工作完成后,我会用它来为大家梳理一下,大家尽量记住大部分初始化的数据结构!