操作系统还原真相(超长记录版)
操作系统学习记录
1. 导论
硬件输入输出
分为两类,串行和并行,所以,我们相应的接口就是串行接口和并行接口。串行接口跟CPU通信,反过来,CPU通过串行接口跟串行设备进行数据传输。并行接口工作原理如法炮制。
访问外部硬件的方式有两种:
- 把外设的内存映射到一定的地址内存中去,比如说显卡
- 通过IO接口访问
啥是应用程序
我们把应用软件和操作系统解构,本质上就是一串命令和数据。我们使用编译器来讲代码翻译成机器语言。加上操作系统的一些现成的配合。程序才可以很快的运行。
用户态和内核态
内核态(Kernel Mode):运行操作系统程序,操作硬件
用户态(User Mode):运行用户程序
一般而言,用户态需要陷入到内核态发生在下面三种情形:
- 请求系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。比如前例中fork()实际上就是执行了一个创建新进程的系统调用。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
用户程序通常调用库函数,由库函数再调用系统调用,因此有的库函数会使用户程序进入内核态(只要库函数中某处调用了系统调用),有的则不会。
- 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
- 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
值得注意的是:这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
为何给内存分段
事实上,内存是随机读写设备,我们只需要给出一个地址告诉地址总线就好了。内存分段是一个历史遗留问题。我们发现,最开始要是直接给出绝对物理地址,会导致程序之间还需要判断这个地址上是否有其他程序运行,很是麻烦。不如我们加上一层抽象,即:采用分段,让程序使用虚拟地址,CPU在处理的时候,采用“段基址+段内地址偏移”的计算方式。这样,每一个程序好像都在独立的采用从0开始的编号地址,但实际映射的时候,分发到的是不同的绝对物理地址。这样程序就不需要关系底层的事情了。(PS:啊哈,后面到了页表,虚拟内存等就知道怎么回事了!)
上述的行为我们叫重定位。重定位行为需要专门的段基址寄存器存储段基址的值,如cs, ds, es等。这下,只要修改段基址就可以让程序在内存海洋里自由的翱翔了
CPU将外部存储器划分成一个个64k的逻辑段,通过这种方式加上段内地址偏移即可通过16位寄存器实现1MB地址空间的寻址。举个例子:
- 用户输入一个操作指令,该指令通过总线传递到CPU内部,cpu通过段寄存器(cs)找到相应的逻辑段,接着根据段内指针找到相应的存储单元,将存储单元中的指令通过程序计数器读入指令寄存器,然后执行后续操作。
- 对于堆栈操作:cpu通过段寄存器(ss)找到相应的逻辑段,然后根据堆栈指针(sp)找到相应的存储单元,接着执行命令的后续流程。
当然了最后的运行结果要经过地址产生与总线控制逻辑运算得到一个20位地址(简单的讲就是将段基址左移4位,这样就可以表示20位了),然后交给相应的接口电路,继而交给程序进行数据的处理。
代码分段
事实上,代码不分段,也是可以跑的。毕竟CPU不晓得命令和数据的区别,告诉他执行什么机器码他就会执行什么机器码。分段只是方便我们的代码书写和代码调试。
值得注意的是:CPU要求指令是逻辑连续的,不需要物理连续。也就是说,只要源源不断的告诉他指令是什么。当然,实际上,指令还是连续的,当指令送到CPU哪里的时候,指令已经被紧密的排列好(操作码)只需要“当前指令地址 + 当前指令长度 = 下一个指令的地址”,就可以一直正确的执行下去了
分段的好处有三点:
- 第一,可以为它们赋予不同的属性。
例如数据本身是需要修改的,所以数据就需要有可写的属性,不让数据段可写,那程序根本就无法执行啦。程序中的代码是不能被更改的,这样就要求代码段具备只读的属性
- 第二,为了提高CPU内部缓存的命中率。
缓存起作用的原因是程序的局部性原理。在CPU内部也有缓存机制,将程序中的指令和数据分离,这有利于增强程序的局部性。CPU内部有针对数据和针对指令的两种缓存机制,因此,将数据和代码分开存储将使程序运行得更快。
- 第三,节省内存。
程序中存在一些只读的部分,比如代码,当一个程序的多个副本同时运行时(比如同时执行多个ls命令时),没必要在内存中同时存在多个相同的代码段,这将浪费有限的物理内存资源,只要把这一个代码段共享就可以了。
那么问题来了:如何给那一堆东西添加属性呢?是谁做了这个事情?答案是CPU和操作系统配合着干:
在保护模式下,有这样一个数据结构,它叫全局描述符表(Global Descriptor Table,GDT),这个表中的每一项称为段描述符。那什么是描述符?描述符就是描述某种数据的数据结构,是元信息,属于数据的数据。就像人们的身份证,上面有写性别、出生日期、地址等描述个人情况的信息。在段描述符中有段的属性位其实是有2个,一个是S字段,占1bit大小,另外一个是占4bit大小的TYPE字段,这两个字段配合在一起使用就能组合出各种属性,如只读、向下扩展、只执行等。
这个表是操作系统填,操作系统在让CPU进入保护模式之前,首先要准备好GDT,也就是要设置好GDT的相关项,填写好段描述符。段描述符填写成什么样,段具备什么样的属性,这完全取决于操作系统了,在这里大家只要知道,段描述符中的S字段和TYPE字段负责该段的属性,也就是该属性与安全相关。
(1)编译器负责挑选出数据具备的属性,从而根据属性将程序片段分类,比如,划分出了只读属性的代码段和可写属性的数据段。再补充一下,编译器并没有让段具备某种属性,对于代码段,编译器所做的只是将代码归类到一起而已,也就是将程序中的有关代码的多个section合并成一个大的segment(这就是我们所说的代码段),它并没有为代码段添加额外的信息。
(2)操作系统通过设置GDT全局描述符表来构建段描述符,在段描述符中指定段的位置、大小及属性(包括S字段和TYPE字段)。也就是说,操作系统认为代码应该是只读的,所以给用来指向代码段的那个段描述符设置了只读的属性,这才是真正给段添加属性的地方。
(3)CPU中的段寄存器提前被操作系统赋予相应的选择子(后面章节会讲什么是选择子,暂时将其理解为相当于段基址),从而确定了指向的段。在执行指令时,会根据该段的属性来判断指令的行为,若有返回则发出异常。
总之,编译器、操作系统、CPU三个配合在一起才能对程序保护,检测出指令中的违规行为。如果GDT中的代码段描述符具备可写的属性,那编译器再怎么划分代码段都没有用,有判断权利的只有CPU。
编译器编译出来的代码段是指一片连续的内存区域。这个段有自己的起始地址,也有自己的大小范围。用户进程中的段,只是为了便于管理,而编译器或程序员在“美学方面”做出的规划,本质上它并不是CPU用于内存访问的段,但它们都是描述了一段内存,而且程序中的段,其起始地址和大小可以理解为CPU访问内存分段策略中的“段基址:段内偏移地址”,这么说来,至少它们很接近了,让我们更近一步:程序是可以被人为划分成段的,并且可以将划分出来的段地址加载到段寄存器中。看一个《操作系统还原真相》给出的例子
section my_code vstart=0
;通过远跳转的方式给代码段寄存器CS赋值0x90
jmp 0x90:start
start: ;标号start只是为了jmp跳到下一条指令
;初始化数据段寄存器DS
mov ax,section.my_data.start
add ax,0x900 ;加0x900是因为本程序会被mbr加载到内存0x900处
shr ax,4 ;提前右移4位,因为段基址会被CPU段部件左移4位
mov ds,ax
;初始化栈段寄存器SS
mov ax,section.my_stack.start
add ax,0x900 ;加0x900是因为本程序会被mbr加载到内存0x900处
shr ax,4 ;提前右移4位,因为段基址会被CPU段部件左移4位
mov ss,ax
mov sp,stack_top ;初始化栈指针
;此时CS、DS、SS段寄存器已经初始化完成,下面开始正式工作
push word [var2] ;变量名var2编译后变成0x4
jmp $
;自定义的数据段
section my_data align=16 vstart=0
var1 dd 0x1
var2 dd 0x6
;自定义的栈段
section my_stack align=16 vstart=0
times 128 db 0
stack_top: ;此处用于栈顶,标号作用域是当前section,
;以当前section的vstart为基数
这是一个在实模式下运行的程序,其中自定义了三个段,为了和标准的段名(.code、.data等)有所区别,这里代码段取名为my_code,数据段取名为my_data,栈段取名为my_stack。这段代码是由MBR加载到物理内存地址0x900后,mbr通过“jmp 0x900”跳过来的,我们的想法是让各段寄存器左移4位后的段基址与程序中各分段实际内存位置相同,所以对于代码段,希望其基址是0x900,故代码段CS的值为0x90(在实模式下,由CPU的段部件将其左移4位后变成0x900,所以要初始化成左移4位前的值)。但没有办法直接为CS寄存器赋值,所以在代码0-1开头,用“jmp 0x90:0”初始化了程序计数器CS和IP。这样段寄存器CS就是程序中咱们自己划分的代码段了。
在此提醒一下,各section中的定义都有align=16和vstart=0,这是用来指定各section按16位对齐的,各section的起始地址是16的整数倍,即用十六进制表示的话,最后一位是0。所以右移操作如第9行的shr ax,4,结果才是正确的,只是把0移出去了。否则不加align=16的话,section的地址不能保证是16的整数倍,右移4位可能会丢数据。vstart=0是指定各section内数据或指令的地址以0为起始编号,这样做为段内偏移地址时更方便。
第6~10行是初始化数据段寄存器DS,是用程序中自已划分的段my_data的地址来初始化的。由于代码0-1本身是脱离操作系统的程序,是MBR将其加载到0x900后通过跳转指令“jmp 0x900”跳入执行的,所以要将my_data在文件内的地址section.my_data.start加上0x900才是最终在内存中的真实地址。右移4位的原因同代码段相同,都是CPU的段部件会自动将段基址左移4位,故提前右移4位。此地址作为段基址赋值给DS,这样段寄存器DS中的值是程序中咱们自己划分的数据段了。
第12~17行是初始化栈段寄存器,原理和数据段差不多,唯一区别是栈段初始化多了个针指针SP,为它初始化的值stack_top是最后一行,因为栈指针在使用过程中指向的地址越来越低,所以初始化时一定得是栈段的最高地址。
经过代码段、数据段、栈段的初始化,CPU中的段寄存器CS、DS、SS都是指向程序中咱们自己划分的段地址,之后CPU的内存分段机制“段基址:段内偏移地址”,段基址就是程序中咱们自己划分的段,段内偏移地址都是各自定义段内的指令和数据地址,由于在section中有vstart=0限制,地址都是从0开始编号的。所以,程序中的分段和CPU内存访问的分段又是一回事。
让我们对此感到疑惑的原因,可能是我们一般都是用高级语言开发程序,在高级语言中,程序分段这种工作不由我们控制,是由编译器在编译阶段完成的。而且现代操作系统都是在平坦模型(整个4GB空间为1个段)下工作,编译器也是按照平坦模型为程序布局,程序中的代码和数据都在同一个段中整齐排列。
- Section Headers:列出了程序中所有的section,这些section是gcc编译器帮忙划分的。
- Program Headers:列出了程序中的段,即segment,这是程序中section合并后的结果。
- Section to Segment mapping:列出了一个segment中包含了哪些section。
在Section Headers和Program Headers中您会发现,这些分段都是按照地址由低到高在4GB空间中连续整洁地分布的,在平坦模型下和谐融洽。
显然,不用程序员手工分段,并且采用平坦模型,这种操作上的“隔离”固然让我们更加方便,但也让我们更加感到进程空间布局的神秘。如果程序分段像代码0-1那样地直白、亲民,大家肯定不会感到迷惑了。其实我想说的是无论是否为平坦模型,程序中的分段和CPU中的内存分段机制,它们属于物品与容器的关系。
举个例子,程序中划分的段相当于各种水果,比如代码段相当于香蕉,数据段相当于葡萄,栈段相当于西瓜。CPU内存分段策略中的段寄存器相当于盛水果的盘子。可以用一个大盘子将各种水果都放进来,但依然是分门别类地摆放,不能失去美感混成一锅粥,这就是段大小为4GB的平坦模型。
总结一下,程序中的段只是逻辑上的划分,用于不同数据的归类,但是可以用CPU中的段寄存器直接指向它们,然后用内存分段机制去访问程序中的段,在这一点上看,它们很像相片和相框的关系:程序中的段是内存中的内容,相当于相片,属于被展示的内容,而内存分段机制则是访问内存的手段,相当于相框,有了相框,照片才能有地摆放。
我想大家应该已经搞清楚了内存分段和程序分段的关系,其实就是一回事,内存分段指的是处理器为访问内存而采用的机制,称之为内存分段机制,程序分段是软件中人为逻辑划分的内存区域,它本身也是内存,所以处理器在访问该区域时,也会采用内存分段机制,用段寄存器指向该区域的起始地址。
物理地址,逻辑地址,有效地址,线性地址,虚拟地址的区别
物理地址:物理地址就是内存单元的绝对地址,不管CPU内部怎么处理地址,最终访问的都是物理地址。在CPU实模式下“段基址+段内偏移地址”就是物理地址,CPU可以使用此地址直接访问内存。
线性地址、虚拟地址:CPU在保护模式下,“段基址+段内偏移地址”叫做线性地址,注意,保护模式下段基址寄存器中存储的不是真正的段基值(和实模式的含义不一样),而是被称为“段选择子”的东西,通过段选择子在GDT(全局描述表)中找到真正的段基值。另外,如果CPU在保护模式下没有开启分页功能,则线性地址就被当做最终的物理地址来用,若开启了分页功能,则线性地址就叫虚拟地址(在没开启分页功能的情况下线性地址和虚拟地址就是一回事)。但是,如果开启分页功能,虚拟地址(或线性地址)还要通过页部件电路转换成最终的物理地址。
逻辑地址、有效地址:无论CPU在什么模式下,段内偏移地址又称为有效地址或者逻辑地址(只是叫法不一样罢了),例如实模式下 “mov ax, [0x7c00]”,0x7c00就是逻辑地址(或有效地址),但这条指令最终操作的物理地址是DS*16+0x7c00
大端字节序和小端字节序
大端字节序(Big Endian)
高位字节数据存放在内存低地址处,低位字节数据存放在内存高地址处。
小端字节序(Little Endian)
高位字节数据存放在内存高地址处,低位数据存放在内存低地址处。
BIOS中断 DOS中断 Linux中断的区别
BIOS和DOS都是存在于实模式下的程序,由它们建立的中断调用都是建立在中断向量表(Interrupt Vector Table,IVT)中的。它们都是通过软中断指令int 中断号来调用的。
中断向量表中的每个中断向量大小是4字节。这4字节描述了一个中断处理例程(程序)的段基址和段内偏移地址。因为中断向量表的长度为1024字节,故该表最多容纳256个中断向量处理程序。计算机启动之初,中断向量表中的中断例程是由BIOS建立的,它从物理内存地址0x0000处初始化并在中断向量表中添加各种处理例程。
BIOS中断调用的主要功能是提供了硬件访问的方法,该方法使对硬件的操作变得简单易行。这句话是否也表明了不通过BIOS调用也是可以访问硬件的?必须是的,否则BIOS中断处理程序又是如何操作硬件呢?操作硬件无非是通过in/out指令来读写外设的端口,BIOS中断程序处理是用来操作硬件的,故该处理程序中一定到处都是in/out指令。
BIOS为什么添加中断处理例程呢?
(1)给自己用,因为BIOS也是一段程序,是程序就很可能要重复性地执行某段代码,它直接将其写成中断函数,直接调用多省心。
(2)给后来的程序用,如加载器或boot loader。它们在调用硬件资源时就不需要自己重写代码了。
BIOS是如何设置中断处理程序的呢?
BIOS也要调用别人的函数例程。每个外设,包括显卡、键盘、各种控制器等,都有自己的内存(主板也有自己的内存,BIOS就存放在里面),不过这种内存都是只读存储器ROM。硬件自己的功能调用例程及初始化代码就存放在这ROM中。根据规范,第1个内存单元的内容是0x55,第2个存储单元是0xAA,第3个存储单位是该rom中以512字节为单位的代码长度。从第4个存储单元起就是实际代码了,直到第3个存储单元所示的长度为止。
有问题了,CPU如何访问到外设的ROM呢?
访问外设有两种方式。
(1)内存映射:通过地址总线将外设自己的内存映射到某个内存区域(并不是映射到主板上插的内存条中)。
(2)端口操作:外设都有自己的控制器,控制器上有寄存器,这些寄存器就是所谓的端口,通过in/out指令读写端口来访问硬件的内存。
控制显卡用的便是内存映射+端口操作的方式,这个以后说。
从内存的物理地址0xA0000开始到0xFFFFF这部分内存中,一部分是专门用来做映射的,如果硬件存在,硬件自己的ROM会被映射到这片内存中的某处,至于如何映射过去的,咱们暂时先不要深入了,这是硬件完成的工作。
如图0-11所示,BIOS在运行期间会扫描0xC0000到0xE0000之间的内存,若在某个区域发现前两个字节是0x55和0xAA时,这意味着该区域对应的rom中有代码存在,再对该区域做累加和检查,若结果与第3个字节的值相符,说明代码无误,就从第4个字节进入。这时开始执行了硬件自带的例程以初始化硬件自身,最后,BIOS填写中断向量表中相关项,使它们指向硬件自带的例程。
.png)
中断向量表中第0H~1FH项是BIOS中断。
另外,上面说的是BIOS在填写中断向量表,那该表是谁创建的呢?答案就是CPU原生支持的,不用谁负责创建。之前我曾说过,软件是靠硬件来运行的,软件能实现什么功能,很大程度上取决于硬件提供了哪些支持。软件中只要执行int 中断向量号,CPU便会把向量号当作下标,去中断向量表中定位中断处理程序并执行。
DOS是运行在实模式下的,故其建立的中断调用也建立在中断向量表中,只不过其中断向量号和BIOS的不能冲突。
0x20~0x27是DOS中断。因为DOS在实模式下运行,故其可以调用BIOS中断。
DOS中断只占用0x21这个中断号,也就是DOS只有这一个中断例程。
DOS中断调用中那么多功能是如何实现的?是通过先往ah寄存器中写好子功能号,再执行int 0x21。这时在中断向量表中第0x21个表项,即物理地址0x21*4处中的中断处理程序开始根据寄存器ah中的值来调用相应的子功能。
而Linux内核是在进入保护模式后才建立中断例程的,不过在保护模式下,中断向量表已经不存在了,取而代之的是中断描述符表(Interrupt Descriptor Table,IDT)。该表与中断向量表的区别会在讲解中断时详细介绍。所以在Linux下执行的中断调用,访问的中断例程是在中断描述符表中,已不在中断向量表里了。
Linux的系统调用和DOS中断调用类似,不过Linux是通过int 0x80指令进入一个中断程序后再根据eax寄存器的值来调用不同的子功能函数的。再补充一句:如果在实模式下执行int指令,会自动去访问中断向量表。如果在保护模式下执行int指令,则会自动访问中断描述符表。
section和segment的区别
C程序大体上分为预处理、编译、汇编和链接4个阶段。
- 预处理阶段是预处理器将高级语言中的宏展开,去掉代码注释,为调试器添加行号等。
- 编译阶段是将预处理后的高级语言进行词法分析、语法分析、语义分析、优化,最后生成汇编代码。
- 汇编阶段是将汇编代码编译成目标文件,也就是转换成了目标机器平台上的机器指令。
- 链接阶段是将目标文件连接成可执行文件。这里我们只关注汇编和链接这两个阶段。
在汇编源码中,通常用语法关键字section或segment来表示一段区域,它们是编译器提供的伪指令,作用是相同的,都是在程序中“逻辑地”规划一段区域,此区域便是节。注意,此时所说的section或segment都是汇编语法中的关键字,它们在语法中都表示“节”,不是段,只是不同编译器的关键字不同而已,关键字segment在语法中也被认为与section意义相同。首先汇编器根据语法规则,会将汇编源码中表示“节”的语法关键字section或segment在目标文件中编译成“节”,此“节”便是我们要讨论的section。经过汇编生成目标文件之后,由这些section或segment修饰的程序区域便成为了“节”(section)。但操作系统加载程序时并不关心节的数量和大小,操作系统只关心节的属性,因为程序必然是要加载到内存中才能运行的,而内存的访问会涉及到全局描述符表中段描述符的访问权限等属性,保护模式下对任何内存的访问都要经过段描述符才行。比如程序代码所在的段描述符权限属性必须是只读,数据所在的段描述符的权限属性必然是可读写,程序中那些只读的节(比如代码区域)必然不能指向可读写的段描述符,同样,程序中的数据也不能用只读权限的段描述符去访问。
操作系统在加载程序时,不需要对逐个节进行加载,只要给出相同权限的节的集合就行了,例如把所有只读可执行的节(如代码节.text和初始化代码节.init)归并到一块,所有可读写的节(如数据节.data和未初始化节.bss)归并到一块,这样操作系统就能为它们分配不同的段选择子,从而指向不同段描述符,实现不同的访问权限了。为了程序能在操作系统上运行,操作系统和编译器需要相互配合,此时汇编器只生成了目标文件,尚未链接,因此这个将“节”合并的工作是由链接器来完成的,链接器将目标文件中属性相同的节合并成一个大的section集合,此集合便称为segment,也就是段,此段便是我们平时所说的可执行程序内存空间中的代码段和数据段。
section称为节,是指在汇编源码中经由关键字section或segment修饰、逻辑划分的指令或数据区域,汇编器会将这两个关键字修饰的区域在目标文件中编译成节,也就是说“节”最初诞生于目标文件中。
segment称为段,是链接器根据目标文件中属性相同的多个section合并后的section集合,这个集合称为segment,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件中。我们平时所说的可执行程序内存空间中的代码段和数据段就是指的segment。
现在通过实验结果来展示出这两者的不同。其实用一个测试样例就能得出结果,不过为了消除大家的疑虑,测试得更彻底一点,在这里给大家准备了两个小汇编文件,将它们编译链接后,我们通过readelf命令查看其信息来得出结论
这个汇编文件是在本地中声明了字符串,并调用外部的打印函数print
在文件2中声明了函数print。下面将这两个文件分别编译成elf格式,这样方便我们通过readelf来查看其编译结果。开始编译,链接成可执行文件12。
nasm -f elf 1.asm -o 1.o
nasm -f elf 2.asm -o 2.o
ld 1.o 2.o -o 12
没问题,再执行一下。
./12
Hello,world!
打印出了Hello,world!,结果正确。让我们用readelf查看下文件12的头信息,如图0-12所示。
要注意section headers的部分,此部分显示可执行文件中所有的section,也包括我们在两个汇编文件中用关键字section定义的部分。从第2个section到第5个section,是1.asm中的自定义数据section: file1data,自定义代码section: file1text和2.asm中的自定义数据section: file2data和自定义代码section: file2text。
再往下看Program Headers部分,此处一共有两个段,第一个段是我们的代码段,通过其Flg值为RE便可推断,只读(Readonly)可执行(Execute),其MemSiz为0x000c3。此段对应Section to Segment mapping部分中的第00个Segment,此segment中包括section: .text file1data file1text file2data file2text。
第二个段便是我们的数据段,但此数据段中只包含.bss节(section),它用于存储全局未初始化数据,故其Flg必然可读写,其属性为RW。此段MemSiz大小为0x40,即十进制的64,可见,这和1.asm中定义的bss大小一致,而在2.asm中未定义.bbs section,所以此bss指的就是1.asm中的定义。此段对应Section to Segment mapping部分中的第01 个Segment,而此segment只包括.bss节,独立成一个段了。
自定义的section名,会在elf的section header 中显示出来。下面是几个标准的section(节)名,不是segment(段)名,segment没有名称。
节名 说明
.data 用于存入数据,可读可写
.text 用于存入代码,只读可执行
.bss 全局未初始化区域
在汇编代码中,若以标准节名定义section,如我们定义的.bss便是标准节名。编译器会按照以上说明中的要求使用section内的数据。
不管定义了多少节名,最终要把属性相同的section,或者编译认为可以放到一块的,合并到一个大的segment中,也就是elf中说的 program header 中的项。由此可见,某个节(section)属于某个段(segment),段是由节组成的。另外多说一句,最终给加载器用的也是program header中显示的段,这才是进程的资源,这部分内容将在加载内核时展开。
指令集
指令集就是指令的集合,为一串特定的比特序列命名成一个我们人可以一下子读懂的序列。比如说add。
最早的指令级是CISC,而后才是RISC。他们的区别这里不展开,简单的讲就是从复杂走向简练的过程。
库函数
用户程序不具备独立打印字符的功能,它必须借助操作系统的力量才可以,如何借助呢?操作系统提供了一套系统调用接口,用户进程直接调用这些接口就行啦。简单来说,接口就是某个功能模块的入口,通过接口给该模块一个输入,它就返回一个输出,模块内部实现的过程就像个黑盒子一样,咱们看不到,也无需关心。我们能够打印字符的原因就是调用了系统调用,但是大家确实没有亲手写下调用系统调用的代码(后面章节会说),这就是库函数的功劳,它帮你写下了这些。
但我们并没有看到库函数的实现,我们只是包含了所需要的库函数所在的头文件,该头文件中有这样一句函数的声明。比如printf函数所在的头文件是stdio.h,该文件位于磁盘/usr/include/目录下,其中第361行是对printf的声明。
extern int printf (__const char *__restrict __format,...);
头文件被包含进来后,其内容也是原样被展开到include所在的位置,就是把整个头文件中的内容挪了过来,所以在头文件中的内容是什么都可以,未必一定要是函数声明
如果在头文件中定义的是printf函数的实现,也许就容易理解头文件帮我们做了什么,可是事实不是这样的,头文件中一般仅仅有函数声明,这个声明告诉编译器至少两件事。
(1)函数返回值类型、参数类型及个数,用来确定分配的栈空间。
(2)该函数是外部函数,定义在其他文件,现在无法为其分配地址,需要在链接阶段将该函数体所在的目标文件一同链接时再安排地址。
这第二件事是我们所说的重点。
如果预处理后,主调函数所在的文件中找不到所调用函数的函数体,一定要在链接阶段把该函数体所在的目标文件链接进来,否则程序在道理上都讲不通,怎么能通过编译呢。
您看到了,main.c中我把func_inc.d包含进来,include后面并不是尖括号而是双引号“?”,这用的是自定义文件的包含,并不是包含标准文件(也就是平时我们所说的标准库头文件)。如果用了尖括号,系统就会到默认路径下去搜索该头文件。搜索到头文件后,找到其中被调函数的声明,再到另一默认文件中找该函数体的实现。
另一默认文件,按理来说应该是目标文件。它到底在哪里呢?
gcc编译时加-v参数会将编译、链接两个过程详细地打印出来,如图0-16所示。
gcc内部也要将C代码经过编译、汇编、链接三个阶段。
(1)编译阶段是将C代码翻译成汇编代码,由最上面的框框中的C语言编译器cc1来完成,它将C代码文件main.c翻译成汇编文件ccymR62K.s。
(2)汇编阶段是将汇编代码编译成目标文件,用第二个框框中的汇编语言编译器as完成,as将汇编文件ccymR62K.s编译成目标文件cc0yJGmy.o。
(3)链接阶段是将所有使用的目标文件链接成可执行文件,这是用左边最下面框框中的链接器collect2来完成的,它只是链接命令ld的封装,最终还是由ld来完成,在这一堆.o文件中,有咱们上面的目标文件cc0yJGmy.o。
以上我们想展开说的是第3点:链接阶段。
大家看到了,实际参与链接的有多个.o文件,这些都是目标文件,也就是函数体所在的文件。printf的函数体就在这里面其中某个.o文件中,而且,printf中也要调用其他函数,这些被调用的函数也分布在这些.o文件之中。
在图-16中的链接阶段,链接器collect2的参数除了有咱们的main.c生成的目标文件cc0yJGmy.o以外,还有以下这几个以crt开头的目标文件:crt1.o,crti.o,crtbegin.o,crtend.o,crtn.o。
crt是什么?CRT,即C Run-Time library,是C运行时库。运行时库是程序在运行时所需要的库,该库是由众多可复用的函数文件组成的,由编译器提供。所以,C运行时库,就是C程序运行时所需要的库文件,在我们的环境中,它由gcc提供。
在程序中简单地一句include <标准头文件>之所以有效,是因为编译器提供的C运行库中已经为我们准备好了这些标准函数的函数体所在的目标文件,在链接时默默帮我们链接上了。这些目标文件都是待重定位文件,重定位文件意思是文件中的函数是没有地址的,用file命令查看它们时会显示relocatable,它们中的地址是在与用户程序的目标文件链接成一个可执行文件时由链接器统一分配的。所以C运行时库中同样的函数与不同的用户程序链接时,其生成的可执行文件中分配给库函数的地址都可能是不同的。每一个用户程序都需要与它们链接合并成一个可执行文件,所以每一个可执行文件中都有这些库文件的副本,这些库文件相当于被复制到每个用户程序中。所以您清楚了,即使咱们的代码只有十几个字符,最终生成的文件也要几KB,就是这个道理。
总结一下:
(1)操作系统有自己支持、加载用户进程的规则,而C运行时库是针对此操作系统的规则,为了让用户程序开发更加容易,用来支持用户进程的代码库。大家要明白,之所以我们写个程序又链接这又链接那的,完全是因为操作系统规定这样做。
(2)用户进程要与C运行时库的诸多目标文件链接后合并成一个可执行文件,也就是说我们的用户进程被加进了大量的运行库中的代码。
(3)C运行时库作用如其名,是提供程序运行时所需要的库文件,而且还做了程序运行前的初始化工作,所以即使不包含标准库文件,链接阶段也要用到c运行时库。
(4)用户程序可以不和操作系统打交道,但如果需要操作系统的支持,必须要通过系统调用,它是用户进程和操作系统之间的“钩子”,用户进程顶多算是个半成品,只有通过钩子挂上了操作系统,加了上所需要的操作系统的那部分代码,用户程序才能做完一件事,这才算完整,后面章节会有详解。
(5)尽管系统调用封装在库函数中,但用户程序可以直接调用“系统调用”,不过用库函数会比较高效。
这里补充点内容:
库函数其实就是一个函数定义,比如常见read()、write()等函数说明了如何获得一个给定的服务,但是系统调用是通过软中断向内核发出一个明确的请求,再者系统调用是在内核完成的,而用户态的函数是在函数库完成的。
系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。
这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为可以提升效率,减少频繁的状态切换,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。
系统调用与系统命令:系统命令相对API更高一层,每个系统命令都是一个可执行程序,比如常用的系统命令ls、hostname等,比如strace ls就会发现他们调用了
诸如open(),brk(),fstat(),ioctl()等系统调用。
系统调用是用户进程进入内核的接口层,它本身并非内核函数,但他是由内核函数实现的,进入系统内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。也可以说系统调用是服务例程的封装例程。
(参考博客系统调用、库函数和内核函数关系与区别 - friedCoder - 博客园 (cnblogs.com))
MBR EBR DBR OBR
这几个概念主要是围绕计算机系统的控制权交接展开的,整个交接过程就是个接力赛,咱们从头梳理。
计算机在接电之后运行的是基本输入输出系统BIOS,BIOS是位于主板上的一个小程序,其所在的空间有限,代码量较少,功能受限,因此它不可能一人扛下所有的任务需求,也就是肯定不能充当操作系统的角色(比如说让BIOS运行QQ是不可能的),必须采取控制权接力的方式,一步步地让处理器执行更为复杂强大的指令,最终把处理器的使用权交给操作系统
采用接力式控制权交接,BIOS只完成一些简单的检测或初始化工作,然后找机会把处理器使用权交出去。交给谁呢?下一个接力棒的选手是MBR,为了方便BIOS找到MBR,MBR必须在固定的位置等待,因此MBR位于整个硬盘最开始的扇区。
MBR是主引导记录,Master或Main Boot Record,它存在于整个硬盘最开始的那个扇区,即0盘0道1扇区,这个扇区便称为MBR引导扇区。注意这里用CHS方式表示MBR引导扇区的地址,因此扇区地址以1开始,顺便说一句,LBA方式是以0为起始为扇区编址的,有关CHS和LBA的内容会在后面章节介绍。一般情况下扇区大小是512字节,但大伙儿不要把这个当真理,有的硬盘扇区并不是512字节。在MBR引导扇区中的内容是:
(1)446字节的引导程序及参数;
(2)64字节的分区表;
(3)2字节结束标记0x55和0xaa。
在MBR引导扇区中存储引导程序,为的是从BIOS手中接过系统的控制权,也就是处理器的使用权。任何一棒的接力都是由上一棒跳到下一棒,也就是上一棒得知道下一棒在哪里才能跳过去,否则权利还是交不出去。BIOS知道MBR在0盘0道1扇区,这是约定好的,因此它会将0盘0道1扇区中的MBR引导程序加载到物理地址0x7c00,然后跳过去执行,这样BIOS就把处理器使用权移交给MBR了。
既然MBR称为“主”引导程序,有“主”就得有“次”, MBR的作用相当于下一棒的引导程序总入口,BIOS把控制权交给MBR就行了,由MBR从众多可能的接力选手中挑出合适的人选并交出系统控制权,这个过程就是由“主引导程序”去找“次引导程序”,这么说的意思是“次引导程序”不止一个。也许您会问,为什么BIOS不直接把控制权交给“次引导程序”?原因是BIOS受限于其主板上的存储空间,代码量有限,本身的工作还做不过来呢,因此心有余而力不足。好啦,下面开始下一轮的系统控制权接力。不要忘了,MBR引导扇区中除了引导程序外,还有64字节大小的分区表,里面是分区信息。分区表中每个分区表项占16字节,因此MBR分区表中可容纳4个分区,这4个分区就是“次引导程序”的候选人群,MBR引导程序开始遍历这4个分区,想找到合适的人选并把系统控制权交给他。
通常情况下这个“次引导程序”就是操作系统提供的加载器,因此MBR引导程序的任务就是把控制权交给操作系统加载器,由该加载器完成操作系统的自举,最终使控制权交付给操作系统内核。但是各分区都有可能存在操作系统,MBR也不知道操作系统在哪里,它甚至不知道分区上的二进制01串是指令,还是普通数据
为了让MBR知道哪里有操作系统,我们在分区时,如果想在某个分区中安装操作系统,就用分区工具将该分区设置为活动分区,设置活动分区的本质就是把分区表中该分区对应的分区表项中的活动标记为0x80。MBR知道“活动分区”意味着该分区中存在操作系统,这也是约定好的。活动分区标记位于分区表项中最开始的1字节,其值要么为0x80,要么为0,其他值都是非法的。0x80表示此分区上有引导程序,0表示没引导程序,该分区不可引导。MBR在分析分区表时通过辨识“活动分区”的标记0x80开始找活动分区,如果找到了,就将CPU使用权交给此分区上的引导程序,此引导程序通常是内核加载器,下面就直接以它为例。
“控制权交接”是处理器从“上一棒选手”跳到“下一棒选手”来完成的,内核加载器的入口地址是这里所说的“下一棒选手”,但是内核加载器在哪里呢?为了MBR方便找到活动分区上的内核加载器,内核加载器的入口地址也必须在固定的位置,这个位置就是各分区最开始的扇区,这也是约定好的。这个“各分区起始的扇区”中存放的是操作系统引导程序——内核加载器,因此该扇区称为操作系统引导扇区,其中的引导程序(内核加载器)称为操作系统引导记录OBR,即OS Boot Record,此扇区也称为OBR引导扇区。在OBR扇区的前3个字节存放了跳转指令,这同样是约定,因此MBR找到活动分区后,就大胆主动跳到活动分区OBR引导扇区的起始处,该起始处的跳转指令马上将处理器带入操作系统引导程序,从此MBR完成了交接工作
不过OBR中开头的跳转指令跳往的目标地址并不固定,这是由所创建的文件系统决定的,对于FAT32文件系统来说,此跳转指令会跳转到本扇区偏移0x5A字节的操作系统引导程序处。不管跳转目标地址是多少,总之那里通常是操作系统的内核加载器。OBR是从DBR遗留下来的,要想了解OBR,还是先从了解DBR开始。DBR是DOS Boot Record,也就是DOS操作系统的引导记录(程序),DBR中的内容大概是:
(1)跳转指令,使MBR跳转到引导代码;
(2)厂商信息、DOS版本信息;
(3)BIOS参数块BPB,即BIOS Parameter Block;
(4)操作系统引导程序;
(5)结束标记0x55和0xaa。
在DOS时代只有4个分区,不存在扩展分区,这4个分区都相当于主分区,所以各主分区最开始的扇区称为DBR引导扇区。后来有了扩展分区之后,无论分区是主分区,还是逻辑分区,为了兼容,分区最开始的扇区都作为DOS引导扇区。但是其他操作系统如UNIX,Linux等为了兼容MBR也传承了这个习俗,都将各分区最开始的扇区作为自己的引导扇区,在里面存放自己操作系统的引导程序。由于现在这个“分区最开始的扇区”引导的操作系统类型太多了,而且DOS还退出历史舞台了,所以DBR也称为OBR。
这里提到了扩展分区就不得不提到EBR。当初为了解决分区数量限制的问题才有了扩展分区,EBR是扩展分区中为了兼容MBR才提出的概念,主要是兼容MBR中的分区表。分区是用分区表来描述的,MBR中有分区表,扩展分区中的是一个个的逻辑分区,因此扩展分区中也要有分区表,为扩展分区存储分区表的扇区称为EBR,即Expand Boot Record,从名字上看就知道它是为了“兼容”而“扩展”出来的结构,兼容的内容是分区表,因此它与MBR结构相同,只是位置不同,EBR位于各子扩展分区中最开始的扇区(注意,各主分区和各逻辑分区中最开始的扇区是操作系统引导扇区),理论上MBR只有1个,EBR有无数个。有关扩展分区的内容还是要参见后面有关分区的章节,那里介绍得更细致。
EBR与MBR结构相同,但位置和数量都不同,整个硬盘只有1个MBR,其位于整个硬盘最开始的扇区——0道0道1扇区。而EBR可有无数个,具体位置取决于扩展分区的分配情况,总之是位于各子扩展分区最开始的扇区,如果此处不明白子扩展分区是什么,到了以后跟踪分区的章节中大伙儿就会明白。OBR其实就是DBR,指的都是操作系统引导程序,位于各分区(主分区或逻辑分区)最开始的扇区,访扇区称为操作系统引导扇区,即OBR引导扇区。OBR的数量与分区数有关,等于主分区数加逻辑分区数之和,友情提示:一个子扩展分区中只包含1 个逻辑分区。
MBR和EBR是分区工具创建维护的,不属于操作系统管理的范围,因此操作系统不可以往里面写东西,注意这里所说的是“不可以”,其实操作系统是有能力读写任何地址的,只是如果这样做的话会破坏“系统控制权接力赛”所使用的数据,下次开机后就无法启动了。OBR是各分区(主分区或逻辑分区)最开始的扇区,因此属于操作系统管理。
DBR、OBR、MBR、EBR都包含引导程序,因此它们都称为引导扇区,只要该扇区中存在可执行的程序,该扇区就是可引导扇区。若该扇区位于整个硬盘最开始的扇区,并且以0x55和0xaa结束,BIOS就认为该扇区中存在MBR,该扇区就是MBR引导扇区。若该扇区位于各分区最开始的扇区,并且以0x55和0xaa结束,MBR就认为该扇区中有操作系统引导程序OBR,该扇区就是OBR引导扇区。
DBR、OBR、MBR、EBR结构中都有引导代码和结束标记0x55和0xaa,因此很多同学都容易把它们搞混。不过它们最大的区别是分区表只在MBR和EBR中存在,DBR或OBR中绝对没有分区表。MBR、EBR、OBR的位置关系如图0-21所示。
MBR主引导记录
细说电脑开机过程
我们的计算机摁下开机键后,我们运行的第一个软件就是BIOS,全程就是Basic Input Output System
,我们现在就有问题
- 谁加载?
- 加载到哪里
- 他的CS:IP谁来改
BIOS
Intel 8086有二十根地址线,可以访问1MB内存大小,也就是说可以访问$2^{20}$B的大小,也就是1MB大小的内存。地址用一个五字节的数描述:0x00000 ~ 0xFFFFF
让我们看看:现在从低地往上看,也就是看看从0到0x9FFFF地址处就是DRAM,这个指的是动态的RAM(随机存储器),这里若是我们知道硬件的知识就会知道这说明构成RAM的电容是充放电迅速的,数据易失的。
我们看到F0000到FFFEF这个位置,存放的就是BIOS的代码。这里,BIOS会建立中断向量表(Interrupt Vector Table),我们就是这样建立起"int"
中断号来实现相关的硬件调用。当然,这里只是完成最基础的一些工作。
我们知道,为什么BIOS分明在主板上却在实模式上访问不到1MB以外的内存呢?答案是——地址总线的宽度太小,我们的硬件需要经过地址分配映射到地址总线可以访问到的编号大小才可以。所以,在实模式下只好访问1MB了。
BIOS的启动过程
啥?BIOS也启动?事实上,BIOS的代码是厂家已经烧录进ROM(只读存储器)的,这个ROM的内存被映射到了低端1MB的上端(这个映射是实际操作硬件完成的)
BIOS既然是一个程序,就需要执行代码,但是只有16B,只能执行一条:不难猜到就是跳转指令!
[0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
这就是我在启动BIOS的时候抓取到的代码,可以看到,跳转到的地方,也就是0xfe05b这个地方继续执行。
0x7c00
这里是MBR的地址,但是我们先按顺序继续说。BIOS的最后一项工作就是检查位于0盘0道1扇区的内容。这个扇区有这样的规定
若这块内存的结尾是0x55和0xaa,BIOS将会认为这个扇区存在一个神秘的引导程序,也就是MBR其实。
无论是怎样的介质——软盘,硬盘还是什么,只要看到这两个数,就认为这是MBR。
那么为什么是0x7C00呢?答案是这样的,早期只有16位机和32位机,我们的研发工程师就默认32KB了,会在最后(为什么是最后呢?答案是MBR主要就是引导其他机器码进来(jmp xxx),随后就没用了,可以被覆盖从而实现内存的复用,为了使得MBR保险起见不被过早覆盖,就放到后面了)的地址内存空间。也就是0x8000 - 0x0400 = 0x7C00这就是这个数字的来历:DOS1.0要求最小的空间32KB 减去MBR自己的大小。这下就对了。
实验:尝试写一个简单的mbr.s
;Main Guide Program
;-------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax
mov sp, 0x7c00 ; 指引调用栈指针指向BIOS加载
;-------------------------------------------------
; INT 0x10 功能号是:0x06,也就是上卷窗口
;-------------------------------------------------
;输入
;AH = 0x06
;AL = 上卷行数(0表示全部上卷)
;BH = 上卷行属性
;(CL, CH) = 窗口左上角的位置
;(DL, DH) = 窗口的右下角位置
; no return
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ;左上角
mov dx, 0x184f ;右下角,VGA文本模式下全屏大小是80个字符呈上25行,下标从0开始,那就是0x 18 4f
int 0x10 ;触发0x10号中断
;---------获取光标位置------------------
;.cursor获取当前位置且在光标处打印字符
mov ah, 3
mov bh, 0
int 0x10
;-------------------------------------
;-----------打印字符串-----------------
mov ax, message
mov bp, ax ; es:bp是串的首地址,es跟cs此时一致
mov cx, 12 ;串的长度
mov ax, 0x1301 ;ah al两个8位寄存器被合并赋值,此时这样赋值时ah -> 0x13, al -> 0x01:显示字符串且光标跟随移动
mov bx, 0x2 ;bh存储的是显示的页号,此时显示的是0, bl则是说明字符属性,这里是黑底绿字
int 0x10 ;
;-------------------------------------
jmp $ ;反复跳转到这一行
message db "Hello,World"
times 510 - ($ - $$) db 0 ;将剩下的字节处全部赋值为0,留下两个字节给魔数提示
db 0x55, 0xaa
这就是效果。
完善MBR
啥是地址
地址只是数字,描述各种符号在地址中的位置。是源代码文件中符号偏移文件开头的距离。编译器所作的工作就是将符号转化为机器语言,给符号排位置。现在,我们来把符号当成有血有肉的物体,有的大有点小。你看
我们这里只是简单的把符号堆砌在这里,现在我们知道,符号的描述需要一定大小(信息论,只有足够的比特数才可以描述一定量的信息)。所以:
可以看到,想要找到某一个元素,只需要将将当前元素的首地址加上元素的大小,就可以了。
我们来看一段源代码:
mov ax, $$
mov ds, ax
mov ax, [var]
label: mov ax, $
jmp label
var dw 0x99
我们对之进行汇编,在进行反汇编:
00000000 B80000 mov ax,0x0
00000003 8ED8 mov ds,ax
00000005 A10D00 mov ax,[0xd]
00000008 B80800 mov ax,0x8
0000000B EBFB jmp short 0x8
0000000D 99 cwd
0000000E 00 db 0x00
第一个指的是当前指令的虚拟首地址,第二个就是我们ndisasm工具分割二进制流得到的:
0000000 00b8 8e00 a1d8 000d 08b8 eb00 99fb 0000 // 可以看到这儿是小端序
可以看到,同样是mov指令,但是编译出来的机器码天差地别,这是因为寻址方式不同,机器码自然不同的基本原理。对了,你会发现一个奇怪的一行
0000000D 99 cwd
0000000E 00 db 0x00
我们这里分明指出的是存储一个数0x99,却被反编译成cwd(扩展至双字节)
啥是section
什么是section?答案是,将程序被划分出来的若干个块,每个块就是一个section。我们这样就可以区分代码段和数据段,从而更好的维护我们的代码。(CPU不懂的,这些信息完全面向程序员)。
比如说,我们下面就来改造一下我们上面的程序,逻辑会更加清晰:
section code
mov ax, $$
mov ax, section.data.start
mov ax, [var1]
mov ax, [var2]
label: jmp label
section data
var1 dd 0x4
var2 dd 0x99
反汇编一下:
00000000 B80000 mov ax,0x0
00000003 B81000 mov ax,0x10
00000006 A11000 mov ax,[0x10]
00000009 A11400 mov ax,[0x14]
0000000C EBFE jmp short 0xc
0000000E 0000 add [bx+si],al
00000010 0400 add al,0x0
00000012 0000 add [bx+si],al
00000014 99 cwd
00000015 0000 add [bx+si],al
00000017 00 db 0x00
你看,没啥区别。
啥是vstart
我们来看看什么是vstart,他是指的:赋予一个虚拟起首地址。当然,这里先设一个重要的认识:跟x86开启分页后虚拟地址是两码事情。
我们汇编上面所有的符号相对地址都是针对vstart而言的——诸如看看下面
section code vstart=0x7c00
mov ax, $$
mov ax, section.code.start
mov ax, section.data.start
mov ax, $
mov ax, [var1]
mov ax, [var2]
jmp $
section data vstart=0x900
var1 dd 0x4
var2 dw 0x99
00000000 B8007C mov ax,0x7c00
00000003 B80000 mov ax,0x0
00000006 B81400 mov ax,0x14
00000009 B8097C mov ax,0x7c09
0000000C A10009 mov ax,[0x900]
0000000F A10409 mov ax,[0x904]
00000012 EBFE jmp short 0x12
00000014 0400 add al,0x0
00000016 0000 add [bx+si],al
00000018 99 cwd
00000019 00 db 0x00
可以看到,第一列的地址才是真实地址。可以看到,当我们使用$$时,解析出来的是 vstart的虚拟地址,但是实际上当我们使用section.code.start的时候,使用的是真正的地址。这样,我们实际上实现了一个重定位技术
地址访问策略就是根据程序给出的地址访问东西。这个地址需要提前准备好东西。这是因为,他最后的最后是需要访问实际的物理地址的。
.vstart和org作用是一样的,只是告诉编译器我一定会被加载器加载到内存的某个固定的位置。所以就让编译器给这个程序的起始地址为这个固定的位置。
因为mbr一定会被加载到0x7c00,所以指定vstart为0x7c00,接下来的所有指令地址都是以这个地址为基址,然后偏移。
总而言之,vstart就是告知后续数据相对于起始的偏移时的基址是多少,不会改变他的真实位置(后面才会编排)
CPU实模式
实模式是8086CPU的寻址方式,寄存器大小,指令用法等的一个抽象概括。下面为了更好的解释这个概念,我们来看看CPU到底是怎么工作的。
CPU自己可以划分为三个基本部分——控制单元,运算单元和存储单元
控制单元是CPU的控制中心,他操控CPU怎么干活。它是由IR(指令寄存器),ID(指令译码器)和OC(操作控制器)组成。程序被加载到内存时,我们的指令实际上也就在那里,CPU的指针指令寄存器(IP)依次指向指令后将之送到IR上准备执行。但是对于CPU来讲那只是一串01,不知道是这么具体工作的。这个时候,就需要ID来解析之。一般的指令格式如下:
前缀 + 操作码 + 寻址方式,操作数类型 + 立即数 +偏移量
注意到:前缀是辅助CPU工作的东西,比如说rep,段超越前缀等。操作码就是大家常见的mov,jmp等。数据呢,他们在存储单元。这里的存储单元指的是CPU内部的L1,L2缓存以及寄存器。待处理的数据就存放在这些存储单元。我们为什么需要在CPU设立L1,L2缓存呢?答案是缓存基本上都是由的是SRAM,性能比DRAM好,缺点是体积大,所以,我们将之设计为层级的方式。缓解CPU和内存的矛盾。
寄存器分为两类
- 一类是程序员可见的寄存器,比如说通用寄存器,段寄存器等。
- 程序不可见寄存器指的是那些程序员不仅而已直接使用也不可以直接访问的寄存器。
下面谈谈实模式下的寄存器,他是一个物理存储原件。可以存储数据。
寄存器分为两种:内部使用的寄存器:GDTR(全局描述符寄存器),IDTR(中断描述符表寄存器),LDTR(局部描述符表寄存器),TR(任务寄存器)CR0-3(控制寄存器),IP(指令指针寄存器),flags(标志寄存器),DR0-7调试寄存器。另一种那就是可见的,这里不赘述了
其中全局描述符表寄存器 GDTR, 通过 Igdt 指令为其指定全局描述符表的地址及偏移量。中断描述符表寄存器 IDTR通过 1idt指令为其指定中断描述符表的地址。局部描述符表寄存器 LDTR用gdt指令为其指定局部描述符表ldt任务寄存器TR,用 ltr 指令为其指定一个任务状态段tss。flags 寄存器,系统提供了 pushf和popf指令, 分别用于将 flags 寄存器的内容压入栈, 将栈中内容弹到 flags 寄存器ldt和tss 都位于 gdt中。
在CPU内部寄存器都是十六位的,但是如何才能表示20位地址呢?通过将16位段基址左移4位变成20位(形成段地址),后4位由段内偏移得到。超出范围就进行回卷操作(跟循环队列相似)。段寄存器:
段寄存器的作用就是指定一片内存的起始地址,故也称为段基址寄存器,尽管段基址在实模式下要乘以 16 ,在保护模式下只是个选择子(保护模式中会讲),但其作用就是指定一片内存的起始地址,而段内偏移地址,顾名思义,就是仅仅相对于此起始地址的偏移量,
由于要指定的是内存中的一段区域的起始地址,所以称之为段基址寄存器,也称段寄存器,无论是在实模式下,还是保护模式下,它们都是 16 位宽。(这是为了更好地兼容嘛?)
代码段:简而言之就是把所有指令都连续排放在一起,形成了一个全部都是指令的区域,里面存储的是指令的操作码及寻址方式等。该区域可以在硬盘上的文件中,也可以是被加载后的内存中,总之是一段指令区域。它们内部都是紧凑挨着的,内容形式完全一样,只是存放的介质不一样而己。代码段寄存器 CS 就是用来指向内存中这段指令区域的起始地址。
数据段:和代码段类似,只是这段区域中的内容不是指令,而是纯粹的数据,也就是说里面存储的是程序运行所需要的数据,属于指令的操作数。数据段寄存器 DS 便是用来指向此数据区域的起始地址。
栈段:其概念只在内存中,硬盘文件中可真没有。一般的栈段是由操作系统分配指定的,所以是属于被加载到内存后才有的。栈段寄存器 SS 就是用来指向此区域的起始地址。
另外三个附加段寄存器是为了方便大家,在 16 位 CPU 中,只有一个附加段寄存器——ES。而 FS,GS 附加段寄存器是在 32 位 CPU 中增加的。我们使用的是 32 位 CPU,并不是说 32 位 CPU 在实模式下的 16 位环境中就不能用 FS,GS 寄存器,32 位的 CPU 兼容 16 位 CPU 的特性。(兼容就是除了有自己额外新增的特性,还包括所有前辈的特性,只强不弱)
IP 寄存器是不可见寄存器,CS 寄存器是可见寄存器。(也就是说,程序员可以操作 CS 寄存器,而不能操作 IP 寄存器,因此程序员无法直接改变程序计数器 PC 来直接改变 CPU 的执行地址,只能通过像 jmp 这样的指令来跳转。)
复习🍊:
x86体系的CPU中:程序计数器 PC 并不是单一的某种寄存器,它是一种寄存器组合,指的段寄存器 cs 和指令寄存器 ip;有专门改变执行流的指令,如 jmp、call、int、ret ,这些指令可以同时修 cs 和 ip,它们在硬件级别上实现了原子操作。
ARM体系的CPU中:程序计数器有个专门的寄存器,名字就叫 PC,想要改变程序流程,直接对该寄存器赋值便可,可以用 mov 指令来修改程序流。(这个 PC 不知道是怎么实现的)
指令在内存中,访问内存就要用 “段:段内偏移” 的形式,所以 CS 寄存器用来存代码段段基址,IP 寄存器用来存储代码段段内偏移地址,同 CS 寄存器一样都是 16 位宽。并不是这两个寄存器真的决定了 CPU 的航向,只是 CPU 的航向被存入了这两个寄存器之中。(在 x86 体系架构中,是内存就应该用 “段基址:段内偏移地址” 的机制来访问,是地址就该有地方存放)
在执行当前指令的同时,在不跨段的情况下,CPU 以“ 当前 IP 寄存器中的值+当前执行指令的机器码长度” 的和作为新的代码段内偏移地址,将其存入 IP 寄存器,再到该新地址处读取指令井执行。 如果下一条指令需要跨段访问,还要加载新的段基址到 CS 寄存器。
flags 寄存器是计算机的窗口,展示了 CPU 内部各项设置、指标,任何一个指令的执行,其执行过程的细节,对计算机造成了哪些影响,都在 flags 寄存器中通过一些标志位反映出来。
通用寄存器:
无论是实模式,还是保护模式,通用寄存器都是有 8 个,分别是 AX BX CX DX SI DI BP SP。
拿 AX 寄存器举例,根据图 3-6 可知, AX 寄存器是由 AH 寄存器和 AL 寄存器组成的,它们都是 8 位寄存器。由于某种原因,16 位 AX 寄存器不够用了,将其扩展 (Extend) 为 32 位,在 AX 原有的基础上,增加 16 位作为高 16 位便是扩展的 AX,即 EAX。所以 EAX 归根结底也是由 AL,AH 组成的,AL,AH 值变了会直接影响 EAX。
通用是说每个寄存器的功能不单一,可以有多种用途,不像段寄存器 SS 那样只能用来放栈段基址,通用寄存器可以用来保存任何数据。虽说通用,但还是约定了它们的惯用法,除了通用的用途外每个寄存器肩负特定的功用。
cx 寄存器用作循环的次数控制,bx 寄存器用于存储起始地址。这样,在为一些通用的函数传递参数时会方便很多;另外,一些指令已经固定用一些特定的寄存器作为参数了。
各通用寄存器特定的功能描述如下:
CPU 中本来是没有实模式这一称呼的,是因为有了保护模式后,为了与老的模式区别开来,所以称老的模式为实模式。实模式的 “实” 体现在:程序中用到的地址都是真实的物理地址,"段基址:段内偏移 “ 产生的逻辑地址就是物理地址,也就是程序员看到的完全是真实的内存。8086 之前的 CPU 对内存的访问比 8086 还要“实诚”,它们没有段的概念,程序中要访问内存,需要把地址写死,也就是所谓的 “硬编码”。由于 “硬编码” 的各种缺点,Intel 早期的工程师发明了“段”,即 CPU 访问内存用 “段+偏移” 的形式。它就是首次在 8086 上出现的,自那之后的 CPU 都是用这类思想访问内存,只是在形式上有小改动。为了支持段机制,CPU 中新增了段寄存器,如 cs 、ds、es 等。
查了一下为什么叫 8086🍊
这个起源可以从8085说起,这个名字的字面意思是:80年代生产的8位5伏电压处理器。后来,对这个进行升级,就简单的在个位上加1,变成了8086,也就出现了那个非常著名的x86鼻祖,于是就延用下来了。其实,后来的32位和64位系统,全名叫做x86-32 和 x86-64。但是,为了方便区分,就变成了x86和x64。
总结一下:8085是8位,8086是16位,x86-32是32位,x86-64是64位。其中,x86就是x86-32的简称,x64就是x86-64的简称。
8086 的地址总线是 20 位宽,也就是其寻址范围是 2^20 次方= IMB 。但其内部寄存器都是 16 位的,若用单一寄存器来寻址的话,只能访问到 2^16 次方等于 64KB 的空间。(地址线位宽和寄存器位宽是没有必然联系)
为了让 16 位的寄存器寻址能够访问 20 位的地址空间 (注意,这里说的是通过寄存器寻址,因为只有通过 16 位的寄存器去寻址才会受到 16 位的限制,立即数寻址不会受到限制),通过先把 16 位的段基址左移 4 位后变成 20 位,再加段内偏移地址,这样便形成了 20 位地址。
段基址为 0xFFFF0,偏移地址应该小于等于 F 就对啦,而这个偏移地址却是 0xFFFF,超出了 0xFFF0 的空间,也就是多出来 64K-16 字节,这部分内存就是传说中的高端内存区( High Memory Area, HMA )。这部分内存是不存在的,也不用处理,8086 只能接收 20 位长的地址(A0~A19 地址线),所以由于超过了 20 位而产生的进位,就会给丢掉,其作用相当于把地址对 IMB 取模了。这是地址回卷的效果,即超过最大范围后,从 0 重新开始计数。
实模式下 CPU 内存寻址方式
为了 CPU 设计更容易,CPU 访问数据的形式也需要提前“约定”好,这就是所谓的寻址方式。8086 的寻址方式,从大方向来看可以分为三大类:寄存器寻址、立即数寻址、内存寻址。在内存寻址中又分为:直接寻址、基址寻址、变址寻址、基址变址寻址。
从名字上看,寻址就是寻找地址,寻找谁的地址?CPU 眼里只有二进制数,所以这是 CPU 在寻找“数” 的地址。这个“数”可以源操作数,也可以是目的操作数 (顺便提一句,Intel 汇编语言语法是“指令目的操作数,源操作数”。简而言之,寻址就是找到“数”的所在地,从哪来,往哪去。
寄存器寻址:最直接的寻址方式就是寄存器寻址,它是指“数”在寄存器中,直接从寄存器中拿数据就行了。只要牵扯到寄存器的操作,无论其是源操作数,还是目的操作数,都是寄存器寻址。若指令中有源操作数是立即数,也属于立即数寻址。
立即数寻址:立即数就是常数。操作数 “直接” 存在指令中,直接拿过来,立即就能用了,为了突显“立即就能用”的高效率,此数便称为立即数,CPU 免去了找数的过程。
内存寻址:操作数在内存中的寻址方式称为内存寻址。由于访问内存是用 “段基址:偏内偏移地址” 的形式,特别强调一下,此形式只用在内存访问中。默认情况下数据段寄存器是 DS,即段基址已经有了,只要再给出段内偏移地址就可以访问内存了,最终起决定作用的、有效的是段内偏移地址,所以段内偏移地址称为有效地址。
直接寻址:直接寻址,就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉 CPU,取此地址中的值作为操作数。这里给出的数字是段内偏移地址,默认的段地址是 DS,若使用了段跨越前缀,那么段基址就在该前缀的段寄存器中。(书中有例子,更容易理解)
注⚠️:立即数寻址中的数字是直接拿来就用作操作数了,直接寻址中的数字是用来进一步寻址的。
基址寻址:就是在操作数中用 bx 寄存器或 bp 寄存器作为地址的起始,地址的变化以它为基础。在实模式下,限制了只能用 bx,bp 作为基址寄存器,bx 寄存器的默认段寄存器是 DS,而 bp 寄存器的默认段寄存器是 SS。(bp、sp 都是栈的有效地址,有效地址就是指偏移地址)。
实模式下,CPU 字长是 16(字长反映 CPU 一次运算的数字位数,而实模式下都是 16 位的数字),而栈的操作是以字长为单位的,因此实模式下的 push 指令默认情况下是压入 2 字节的数据,pop 指令默认是弹出 2 字节的数据。
其工作原理都可分为两步:(栈是向低地址方向发展的)
假如执行 push ax:
1 sub sp, 2 先将 sp 中的值减去2
2 mov [sp], ax 再将 ax 中的值 mov 到新的 ss:sp 指向的内存
假如执行 pop ax:
1 mov ax, [sp] 先将 ss:sp 指向的值 mov 到 ax
2 add sp, 2 再将 sp 的指针 +2
访问栈有两种方式,一种是把栈当成“栈”来使用,也就是用 push,pop 指令操作栈,但这样我们只能访问到栈顶,即 sp 指向的地址,没有办法直接访问到栈底和栈顶之间的数据。
很多时候,我们需要读写栈中的数据,即需要把栈当成普通数据段那样访问。处理器为了让开发人员方便控制栈中数据,提供了这种把栈当成数据段来访问的方式,可以用寄存器 bp 来给出栈中偏 移量,所以 bp 默认的段寄存器就是 SS,这样就可通过 SS: bp 的方式把栈当成普通的数据段来访问了。
变址寻址:变址寻址其实和基址寻址类似,只是寄存器由 bx,bp 换成了 si,di。si 是指源索引寄存器 (source index),di 是指目的索引寄存器 (destination index),两个寄存器的默认段寄存器也是 ds。变址寻址主要是用于字符搬运方面的指令,这两个寄存器在很多指令中都要成对使用。(配合基址寻址,用来实现基址变址寻址)
基址变址寻址:从名字上看,这是基址寻址和变址寻址的结合,即基址寄存器 bx 或 bp 加一个变址寄存器 si 或 di。
一种寻址方式对应一种电路实现,增加一种寻址方式,会增加硬件电路的复杂性,所以寻址方式是有限的。
🍊上层形式万千的变化,归根结底就是这么几类硬件电路,而它是有限的。
硬件是如何实现栈的?
栈在我们计算机系统的工作十分重要。这里,我们假定数据结构大家已经学过,我们来看看硬件层面上栈是如何做到的。
栈使用一片内存空间 + SP指针维护的。我们的访问方法就是“段基址SS的值*16 + 栈指针SP(段内偏移地址)”访问,由于是硬件实现的栈,硬件提供了push和pop,SP就是栈的出口和入口,元素的进入和出去都是使用SP指针维护的。push指令就是SP-字长,目的是为了防止栈顶的元素被覆盖,所得到的差存入SP,这样我们就看到栈就向下移动一个元素大小位置。这样就可以去写入数据了,pop则是直接上移栈指针。(为什么是入栈下移出栈上移,答案是栈是向下生长的)
实模式下的ret和call
复习一下,CS:IP寄存器存储的是地址,其中CS存段基址,IP存偏移量。但是,我们一般不能直接去改变这两个寄存器的值,而是通过CPU提供的跳转指令来完成这些工作。我们下面将会提到ret和call两个指令。
call值得是跳转执行新代码,跳转后他会留下一个返回地址,这个返回地址就是给ret指令准备的。注意,retf和callf对应,ret和call对应,原因是他们弹出和压入的地址字节长度不一样,f表达的就是far,代表跳转的地址字节长度是4个字节。反之则是2个字节。
这点跟jmp不一样,jmp指的是直接跳转去新代码而不会回头。
call调用有四种方式
- 16位(2字节)实模式相对近调用
强调两点,一是近,二是相对。为什么是近?因为两个字节,near,总是取出两个字节用来call和ret。注意,最后转化出来的操作数并不是实际物理地址,而是目标和现在所处的差:
call near proc
举个例子,proc被解析出来是0x1234,则实际上的操作数是0x1234 - call所处的地址 - call自身长度3.
call near proc
jmp $
addr dd 4
near:
mov ax, 0x1234
ret
我们使用xdd指令来看看解析的结果
Usage:
xxd [options] [infile [outfile]]
or
xxd -r [-s [-]offset] [-c cols] [-ps] [infile [outfile]]
Options:
-a toggle autoskip: A single '*' replaces nul-lines. Default off.
-b binary digit dump (incompatible with -ps,-i,-r). Default hex.
-C capitalize variable names in C include file style (-i).
-c cols format <cols> octets per line. Default 16 (-i: 12, -ps: 30).
-E show characters in EBCDIC. Default ASCII.
-e little-endian dump (incompatible with -ps,-i,-r).
-g bytes number of octets per group in normal output. Default 2 (-e: 4).
-h print this summary.
-i output in C include file style.
-l len stop after <len> octets.
-o off add <off> to the displayed file position.
-ps output in postscript plain hexdump style.
-r reverse operation: convert (or patch) hexdump into binary.
-r -s off revert with <off> added to file positions found in hexdump.
-d show offset in decimal instead of hex.
-s [+][-]seek start at <seek> bytes abs. (or +: rel.) infile offset.
-u use upper case hex letters.
-v show version: "xxd 2021-10-22 by Juergen Weigert et al.".
e806 00eb fe04 0000 00b8 3412 c3 ..........4..
- 16位实模式间接绝对近调用
间接指的是我们并没有直接给出地址而是通过去寄存器或者是内存中去找值。绝对则是指的是绝对地址。
数据类型伪指令分为byte, word, dword, qward可以认为是强制类型转换:
section call_test vstart=0x900
mov word [addr], near_proc
call [addr]
mov ax, near_proc
call ax
jmp $
addr dd 4
near_proc:
mov ax, 0x1234
ret
- 16位实模式直接绝对远调用
直接指的是操作数直接给出,也就是立即数,远就意味着要跨段了
指令就是call far指令
call far 段基址(立即数):段内偏移地址(立即数)
section call_test vstart = 0x900
call 0:far_proc
jmp $
far_proc:
mov ax, 0x1234
ret
形式: call far 段基址(立即数):段偏移(立即数)
这个图片中是程序执行顺序
- 16位实模式间接绝对远调用
和3的区别:段基址和段偏移地址存放在内存中
形式: call far [bx] || call far [0x1234] (ds为段基址)
call是去内存中找要跳转的位置,内存前(低)两个字节谁段偏移地址,后(高)两个字节是段基址。比如call far [0x1234] ,由于没有段跨越前缀,则默认的段基址*16后再与0x1234相加,得到的和为物理地址,以该物理地址为起始地址的前2字节是段偏移地址,以(该物理地址+2)为起始的地址为段基址。(因为cs和ip都是新的,所以就的cs和ip都要压入栈中)
addr dw far_proc,0 addr是变量名称,dw是数据类型,far_proc,0两个都是dw类型,addr其实是一个数组,里面存放了两个dw类型的变量,前面的far_proc 是段偏移地址,0是段基址。call的化去addr里面访问,那么访问到的就是这两个数据。
实模式下的jmp
- 16位实模式相对短转移
短:因为使用short修饰,转移的操作数只有1个字节,所以叫短转移
相对:地址是相对位置,和上面的call 相对短转移是一样的。就拿上面那张图看,不是有jmp -2 么,这个就是相对的地址。(同时表明操作数是有符号的,所以才有-2)
jmp只有两个字节,操作码一个字节,操作数一个字节。操作数代表的位置转移可以是-127——128
注意,CPU是不认这个地址差的,CPU只认绝对的地址。所以这个jmp 0x1234这个0x1234被编译器编译成了地址差,还要被转换到绝对地址,才能传送到CPU中。
这里第3行定义了127个字节的数据。这个就是jmp短转移的操作数。因为第4行地址-第2行地址-2,就是127字节
第一行,在地址0x900处的指令是jmp +127操作数是127,其转移的真实地址是0x981,前面说过,操作数是不能给CPU直接用的,CPU要将其转换成绝对地址,所以此地址是这样得到的:0x900+2+127=0x981
因为jmp短转移的操作数范围是-127——-128,如果操作数不在这个范围内,那么会在编译阶段报错。
解决的方法就是将操作数的范围增大,突破1字节的有符号数表示范围就行了。
1)将第2行jmp后面的short去掉,改成near
2)第2行jmp后面什么都不写,让nasm编译器自己判断
- 16位实模式相对近转移
近转移:用near修饰,操作数有两个字节,所以转移的长度可以增加。
操作码占一个字节,是0xe9,操作数占两个字节,所以这个指令占3字节。
- 16位实模式间接绝对近转移
间接:使用寄存器或者内存,里面存放的是要转移的地址
绝对:给出绝对地址
近转移:near
形式:jmp near ax, jmp near [addr]
section call_test vstart=0x900
mov word [addr], start
jmp near [addr]
times 128 db 0
addr dw 0
start:
mov ax, 0x1234
jmp $
- 16位实模式直接绝对远转移
直接:给出物理地址,即段基址和偏移地址,CPU可以直接用
绝对:提供绝对地址
远:跨段需求
形式:jmp 立即数形式的段基址:立即数形式的偏移地址,例如:jmp 0:0x1234
- 16位实模式间接绝对远转移
因为操作数有两个:段基址,段偏移,所以肯定是不能放寄存器里面了,所以要放在内存中。
形式:jmp far 内存地址
section call_test vstart=0x900
jmp far [addr]
times 128 db 0
addr dw start, 0
start:
mov ax, 0x1234
jmp $
有条件转移
flags寄存器是16位宽,保护模式下扩展成32位的eflags寄存器。
0 cf:符号位,进位,借位为1,可以判断无符号加减的溢出,有符号不能判断。
2 pf:奇偶位,低8位1个数为偶,pf=1,否则为0
1,3,5,15没有专门标志位
有条件转移是一个指令簇,格式为jxx,条件满足跳转。但是jxx 目标地址只能是段内偏移地址。实模式下,编译器自动分辨是短转移还是近转移。保护模式下,32位偏移地址可以访问4GB内存,不分转移方式。
所谓条件转移,条件就是判断上一条指令的结果是否满足某方面或某些方面,能够影响标志位的指令才能条件指令作为条件。
Eflag寄存器标志位
Eflag标志位.png
CF Carray Flag
:,CF为1,表示进位或借位PF Parity Flag
:奇偶位,用来标记低八位中1的个数AF Auxiliary carry Flag
:辅助进位标志,用来记录运算结果低四位的进借位情况。ZF Zero Flag
:零标志位。若计算结果位0,此标志为1SF Sign Flag
:符号标志位。运算结果为负,则SF位为1TF Trap Flag
:陷阱标志位。TF为1,CPU进入单步运行模式。debug单步调试时,原理是让TF置一IF Interrupt Flag
:中断标志位。IF为1,表示中断开启,CPU可以响应外部可屏蔽中断。DF Direction Flag
:方向标志位。用于字符串操作指令中,当DF为1时,指令中的操作数地址会自动减少一个单位,当DF为0时,指令中的操作数地址会自动增加一个单位。其中提到的这个单位的大小,取决于用什么指令。OF Overflow Flag
:溢出标志位。表示计算结果是否超过了数据类型可表示的大小。IOPL Input Output Privilege Level
:用于特权级概念的CPU。占用两位表示4中特权级NT Nest Task
:任务嵌套标志。8088支持多任务,一个任务就是一个进程。一个任务中又嵌套调用了另一个任务时,此NT为1RF Resume Flag
:恢复标志位。该标志位用于程序调试,指示是否接受调试故障,它需要和调试寄存器一起使用VM Virtual 8086 Model
:虚拟8086模式,这是实模式向保护模式过渡时的产物。此位为1,可以在保护模式下运行实模式的程序。在保护模式下运行实模式程序,都要为其虚拟一个实模式环境,故称为虚拟模式。AC Alignment Check
:对齐检查。程序中的数据或指令其内存地址是否是偶数,是否是16,32的整数倍,没有余数。VIF Virtual Interrupt Flag
:虚拟中断标志位,虚拟模式下的中断标志。VIP Virtual Interrupt Pending
:虚拟中断挂起标志位。在多任务的情况下,为操作系统提供虚拟中断挂起信息。ID Identification
:识别标志位。系统经常需要判断CPU型号,若ID为1,表示支持CPU id 指令
直接干显卡去
CPU通过IO接口跟外界通信
在现在,硬件设备大爆发的今天,让CPU很难直接和外界设备进行通信。所以,加层,一个抽象层。一个IO接口设备。他来协调外界设备和CPU之间的通信。方法是:
- 设定数据缓存,解决CPU于与外设之间的速度不匹配问题
CPU和外设速度上的差异可以使用缓冲区来解决。
- 设置信号电平转换电路
CPU和外设信号的差异可以使用转换电路完成
- 设置数据格式转换
可以使用数模转换器转化到外设上进行驱动等等
- 设置时序控制电路来同步CPU和外部设备
硬件的工作也按照某一个时序,他们像计算机一样也有自己的晶振时序一样。我们的CPU和设备需要协调信号来进行通信!
- 提供地址编码
CPU同多个硬件打交道,每个硬件反馈的信息很多,所以一个IO接口需要包含多个端口。
CPU通过总线来跟IO接口交互。我们的数据就在这总线上高速飞驰。当多个IO设备想要和CPU进行通信的时候,会有一个芯片负责与之仲裁:那就是南桥芯片(输入输出控制中心 Input / Output Control Hub -> ICH)用于处理低俗设备和CPU的通信。北桥芯片则是处理高速IO设备,一些板子已经集成在了CPU当中。
微观上,我们就是通过寄存器的方式,让外部设备同CPU进行通信,端口,也就是告知如何访问外设的寄存器,也就要以操纵寄存器的方式进行。有两个专门的指令in out来处理:
in al, dx ;al,ax存储端口数据,dx则是存储端口号
in ax, dx
固定用法!只要使用in指令,就必须用dx代表源操作数,也就是端口,而使用al还是ax则是决定了我们的寄存器的宽度是8位的还是16位的。
相反的,我们还有out指令:
out dx, al
out dx, ax
out Imm, al,
out Imm, ax
可以看到位置反过来了,并且,我们可以使用立即操作数来表示端口,而不必须使用dx,这与in不同!
显存,显卡,显式器
显存是显卡内存的意思,显卡有两种常见的工作模式——图像模式和字符模式。显卡是通过我们一个约定的协议来得知我们需求。也就是说:我们使用一个编码来告诉显卡应该显示什么。举个例子,把0x41写进显卡内存,他知道他要打印A了。为了统一,我们就是用ASCII编码,来统一编码的格式:
十进制代码 | 十六进制代码 | MCS 字符或缩写 | DEC 多国字符名 | |
---|---|---|---|---|
ASCII 控制字符 1 | ||||
0 | 0 | NUL | 空字符 | |
1 | 1 | SOH | 标题起始 (Ctrl/A) | |
2 | 2 | STX | 文本起始 (Ctrl/B) | |
3 | 3 | ETX | 文本结束 (Ctrl/C) | |
4 | 4 | EOT | 传输结束 (Ctrl/D) | |
5 | 5 | ENQ | 询问 (Ctrl/E) | |
6 | 6 | ACK | 认可 (Ctrl/F) | |
7 | 7 | BEL | 铃 (Ctrl/G) | |
8 | 8 | BS | 退格 (Ctrl/H) | |
9 | 9 | HT | 水平制表栏 (Ctrl/I) | |
10 | 0A | LF | 换行 (Ctrl/J) | |
11 | 0B | VT | 垂直制表栏 (Ctrl/K) | |
12 | 0C | FF | 换页 (Ctrl/L) | |
13 | 0D | CR | 回车 (Ctrl/M) | |
14 | 0E | SO | 移出 (Ctrl/N) | |
15 | 0F | SI | 移入 (Ctrl/O) | |
16 | 10 | DLE | 数据链接丢失 (Ctrl/P) | |
17 | 11 | DC1 | 设备控制 1 (Ctrl/Q) | |
18 | 12 | DC2 | 设备控制 2 (Ctrl/R) | |
19 | 13 | DC3 | 设备控制 3 (Ctrl/S) | |
20 | 14 | DC4 | 设备控制 4 (Ctrl/T) | |
21 | 15 | NAK | 否定接受 (Ctrl/U) | |
22 | 16 | SYN | 同步闲置符 (Ctrl/V) | |
23 | 17 | ETB | 传输块结束 (Ctrl/W) | |
24 | 18 | CAN | 取消 (Ctrl/X) | |
25 | 19 | EM | 媒体结束 (Ctrl/Y) | |
26 | 1A | SUB | 替换 (Ctrl/Z) | |
27 | 1B | ESC | 换码符 | |
28 | 1C | FS | 文件分隔符 | |
29 | 1D | GS | 组分隔符 | |
30 | 1E | RS | 记录分隔符 | |
31 | 1F | US | 单位分隔符 | |
ASCII 特殊和数字字符 | ||||
32 | 20 | SP | 空格 | |
33 | 21 | ! | 感叹号 | |
34 | 22 | “ | 引号 (双引号) | |
35 | 23 | # | 数字符号 | |
36 | 24 | $ | 美元符 | |
37 | 25 | % | 百分号 | |
38 | 26 | & | 和号 | |
39 | 27 | ‘ | 省略号 (单引号) | |
40 | 28 | ( | 左圆括号 | |
41 | 29 | ) | 右圆括号 | |
42 | 2A | * | 星号 | |
43 | 2B | + | 加号 | |
44 | 2C | , | 逗号 | |
45 | 2D | — | 连字号或减号 | |
46 | 2E | . | 句点或小数点 | |
47 | 2F | / | 斜杠 | |
48 | 30 | 0 | 零 | |
49 | 31 | 1 | 1 | |
50 | 32 | 2 | 2 | |
51 | 33 | 3 | 3 | |
52 | 34 | 4 | 4 | |
53 | 35 | 5 | 5 | |
54 | 36 | 6 | 6 | |
55 | 37 | 7 | 7 | |
56 | 38 | 8 | 8 | |
57 | 39 | 9 | 9 | |
58 | 3A | : | 冒号 | |
59 | 3B | ; | 分号 | |
60 | 3C | < | 小于 | |
61 | 3D | = | 等于 | |
62 | 3E | > | 大于 | |
63 | 3F | ? | 问号 | |
ASCII 字母字符 | ||||
64 | 40 | @ | 商业 at 符号 | |
65 | 41 | A | 大写字母 A | |
66 | 42 | B | 大写字母 B | |
67 | 43 | C | 大写字母 C | |
68 | 44 | D | 大写字母 D | |
69 | 45 | E | 大写字母 E | |
70 | 46 | F | 大写字母 F | |
71 | 47 | G | 大写字母 G | |
72 | 48 | H | 大写字母 H | |
73 | 49 | I | 大写字母 I | |
74 | 4A | J | 大写字母 J | |
75 | 4B | K | 大写字母 K | |
76 | 4C | L | 大写字母 L | |
77 | 4D | M | 大写字母 M | |
78 | 4E | N | 大写字母 N | |
79 | 4F | O | 大写字母 O | |
80 | 50 | P | 大写字母 P | |
81 | 51 | Q | 大写字母 Q | |
82 | 52 | R | 大写字母 R | |
83 | 53 | S | 大写字母 S | |
84 | 54 | T | 大写字母 T | |
85 | 55 | U | 大写字母 U | |
86 | 56 | V | 大写字母 V | |
87 | 57 | W | 大写字母 W | |
88 | 58 | X | 大写字母 X | |
89 | 59 | Y | 大写字母 Y | |
90 | 5A | Z | 大写字母 Z | |
91 | 5B | [ | 左中括号 | |
92 | 5C | \ | 反斜杠 | |
93 | 5D | ] | 右中括号 | |
94 | 5E | ^ | 音调符号 | |
95 | 5F | _ | 下划线 | |
96 | 60 | ` | 重音符 | |
97 | 61 | a | 小写字母 a | |
98 | 62 | b | 小写字母 b | |
99 | 63 | c | 小写字母 c | |
100 | 64 | d | 小写字母 d | |
101 | 65 | e | 小写字母 e | |
102 | 66 | f | 小写字母 f | |
103 | 67 | g | 小写字母 g | |
104 | 68 | h | 小写字母 h | |
105 | 69 | i | 小写字母 i | |
106 | 6A | j | 小写字母 j | |
107 | 6B | k | 小写字母 k | |
108 | 6C | l | 小写字母 l | |
109 | 6D | m | 小写字母 m | |
110 | 6E | n | 小写字母 n | |
111 | 6F | o | 小写字母 o | |
112 | 70 | p | 小写字母 p | |
113 | 71 | q | 小写字母 q | |
114 | 72 | r | 小写字母 r | |
115 | 73 | s | 小写字母 s | |
116 | 74 | t | 小写字母 t | |
117 | 75 | u | 小写字母 u | |
118 | 76 | v | 小写字母 v | |
119 | 77 | w | 小写字母 w | |
120 | 78 | x | 小写字母 x | |
121 | 79 | y | 小写字母 y | |
122 | 7A | z | 小写字母 z | |
123 | 7B | { | 左大括号 | |
124 | 7C | \ | 垂直线 | |
125 | 7D | } | 右大括号 (ALTMODE) | |
126 | 7E | ~ | 代字号 (ALTMODE) | |
127 | 7F | DEL | 擦掉 (DELETE) | |
控制字符 | ||||
128 | 80 | [保留] | ||
129 | 81 | [保留] | ||
130 | 82 | [保留] | ||
131 | 83 | [保留] | ||
132 | 84 | IND | 索引 | |
133 | 85 | NEL | 下一行 | |
134 | 86 | SSA | 被选区域起始 | |
135 | 87 | ESA | 被选区域结束 | |
136 | 88 | HTS | 水平制表符集 | |
137 | 89 | HTJ | 对齐的水平制表符集 | |
138 | 8A | VTS | 垂直制表符集 | |
139 | 8B | PLD | 部分行向下 | |
140 | 8C | PLU | 部分行向上 | |
141 | 8D | RI | 反向索引 | |
142 | 8E | SS2 | 单移 2 | |
143 | 8F | SS3 | 单移 3 | |
144 | 90 | DCS | 设备控制字符串 | |
145 | 91 | PU1 | 专用 1 | |
146 | 92 | PU2 | 专用 2 | |
147 | 93 | STS | 设置传输状态 | |
148 | 94 | CCH | 取消字符 | |
149 | 95 | MW | 消息等待 | |
150 | 96 | SPA | 保护区起始 | |
151 | 97 | EPA | 保护区结束 | |
152 | 98 | [保留] | ||
153 | 99 | [保留] | ||
154 | 9A | [保留] | ||
155 | 9B | CSI | 控制序列引导符 | |
156 | 9C | ST | 字符串终止符 | |
157 | 9D | OSC | 操作系统命令 | |
158 | 9E | PM | 秘密消息 | |
159 | 9F | APC | 应用程序 | |
其他字符 | ||||
160 | A0 | [保留] 2 | ||
161 | A1 | ¡ | 反向感叹号 | |
162 | A2 | ¢ | 分币符 | |
163 | A3 | £ | 英磅符 | |
164 | A4 | [保留] 2 | ||
165 | A5 | ¥ | 人民币符 | |
166 | A6 | [保留] 2 | ||
167 | A7 | § | 章节符 | |
168 | A8 | ¤ | 通用货币符号 2 | |
169 | A9 | © | 版权符号 | |
170 | AA | ª | 阴性顺序指示符 | |
171 | AB | « | 左角引号 | |
172 | AC | [保留] 2 | ||
173 | AD | [保留] 2 | ||
174 | AE | [保留] 2 | ||
175 | AF | [保留] 2 | ||
176 | B0 | ° | 温度符 | |
177 | B1 | ± | 加/减号 | |
178 | B2 | ² | 上标 2 | |
179 | B3 | ³ | 上标 3 | |
180 | B4 | [保留] 2 | ||
181 | B5 | µ | 微符 | |
182 | B6 | ¶ | 段落符,pilcrow | |
183 | B7 | · | 中点 | |
184 | B8 | [保留] 2 | ||
185 | B9 | ¹ | 上标 1 | |
186 | BA | º | 阳性顺序指示符 | |
187 | BB | » | 右角引号 | |
188 | BC | ¼ | 分数四分之一 | |
189 | BD | ½ | 分数二分之一 | |
190 | BE | [保留] 2 | ||
191 | BF | ¿ | 反向问号 | |
192 | C0 | À | 带重音符的大写字母 A | |
193 | C1 | Á | 带尖锐重音的大写字母 A | |
194 | C2 | Â | 带音调符号的大写字母 A | |
195 | C3 | Ã | 带代字号的大写字母 A | |
196 | C4 | Ä | 带元音变音 (分音符号) 的大写字母 A | |
197 | C5 | Å | 带铃声的大写字母 A | |
198 | C6 | Æ | 大写字母 AE 双重元音 | |
199 | C7 | Ç | 带变音符号的大写字母 C | |
200 | C8 | È | 带重音符的大写字母 E | |
201 | C9 | É | 带尖锐重音的大写字母 E | |
202 | CA | Ê | 带音调符号的大写字母 E | |
203 | CB | Ë | 带元音变音 (分音符号) 的大写字母 E | |
204 | CC | Ì | 带重音符的大写字母 I | |
205 | CD | Í | 带尖锐重音的大写字母 I | |
206 | CE | Î | 带音调符号的大写字母 I | |
207 | CF | Ï | 带元音变音 (分音符号) 的大写字母 I | |
208 | D0 | [保留] 2 | ||
209 | D1 | Ñ | 带代字号的大写字母 N | |
210 | D2 | Ò | 带重音符的大写字母 O | |
211 | D3 | Ó | 带尖锐重音的大写字母 O | |
212 | D4 | Ô | 带音调符号的大写字母 O | |
213 | D5 | Õ | 带代字号的大写字母 O | |
214 | D6 | Ö | 带元音变音 (分音符号) 的大写字母 O | |
215 | D7 | OE | 大写字母 OE 连字 2 | |
216 | D8 | Ø | 带斜杠的大写字母 O | |
217 | D9 | Ù | 带重音符的大写字母 U | |
218 | DA | Ú | 带尖锐重音的大写字母 U | |
219 | DB | Û | 带音调符号的大写字母 U | |
220 | DC | Ü | 带元音变音 (分音符号) 的大写字母 U | |
221 | DD | Y | 带元音变音 (分音符号) 的大写字母 Y | |
222 | DE | [保留] 2 | ||
223 | DF | ß | 德语高调小写字母 s | |
224 | E0 | à | 带重音符的小写字母 a | |
225 | E1 | á | 带尖锐重音的小写字母 a | |
226 | E2 | â | 带音调符号的小写字母 a | |
227 | E3 | ã | 带代字号的小写字母 a | |
228 | E4 | ä | 带元音变音 (分音符号) 的小写字母 a | |
229 | E5 | å | 带铃声的小写字母 a | |
230 | E6 | æ | 小写字母 ae 双重元音 | |
231 | E7 | ç | 带变音符号的小写字母 c | |
232 | E8 | è | 带重音符的小写字母 e | |
233 | E9 | é | 带尖锐重音的小写字母 e | |
234 | EA | ê | 带音调符号的小写字母 e | |
235 | EB | ë | 带元音变音 (分音符号) 的小写字母 e | |
236 | EC | ì | 带重音符的小写字母 i | |
237 | ED | í | 带尖锐重音的小写字母 i | |
238 | EE | î | 带音调符号的小写字母 i | |
239 | EF | ï | 带元音变音 (分音符号) 的小写字母 i | |
240 | F0 | [保留] 2 | ||
241 | F1 | ñ | 带代字号的小写字母 n | |
242 | F2 | ò | 带重音符的小写字母 o | |
243 | F3 | ó | 带尖锐重音的小写字母 o | |
244 | F4 | ô | 带音调符号的小写字母 o | |
245 | F5 | õ | 带代字号的小写字母 o | |
246 | F6 | ö | 带元音变音 (分音符号) 的小写字母 o | |
247 | F7 | oe | 小写字母 oe 连字 2 | |
248 | F8 | ø | 带斜杠的小写字母 o | |
249 | F9 | ù | 带重音符的小写字母 u | |
250 | FA | ú | 带尖锐重音的小写字母 u | |
251 | FB | û | 带音调符号的小写字母 u | |
252 | FC | ü | 带元音变音 (分音符号) 的小写字母 u | |
253 | FD | ÿ | 带元音变音 (分音符号) 的小写字母 y 2 | |
254 | FE | [保留] 2 | ||
255 | FF | [保留] 2 |
我们下面来看看,我们要往哪里写去:
起始 | 结束 | 大小 | 用途 |
---|---|---|---|
C0000 | C7FFF | 32KB | 显示适配器BIOS |
B8000 | BFFFF | 32KB | 用于文本模式显示适配器 |
B0000 | B7FFF | 32KB | 用于黑白显示适配器 |
A0000 | AFFFF | 64KB | 用于彩色显示适配器 |
我们先来看看文本模式:注意,我们可以决定文本上可以显示字符的个数,比如说80x25, 40x25等。我们一般默认80x25,2000个字符,然后,一位存放字符,另一位存放的是属性,那就占4000B
属性字节则是说明其颜色的:
显卡怎么玩:
SECTION MBR vstart=0x7c00 ;起始地质编译在0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800 ; ax为文本信号起始区
mov gs,ax ; gs = ax 充当段基址的作用
;ah = 0x06 al = 0x00 想要调用int 0x06的BIOS提供的中断对应的函数 即向上移动即完成清屏功能
;cx dx 分别存储左上角与右下角的左边 详情看int 0x06函数调用
mov ax,0600h
mov bx,0700h
mov cx,0
mov dx,184fh
;调用BIOS中断
int 0x10
;新增功能 直接操作显存部分
;预设输出LOVE6 OS
mov byte [gs:0x00],'L' ;低位字节储存ascii字符 小端储存内存顺序相反
mov byte [gs:0x01],0xA4 ;背景储存在第二个字节 含字符与背景属性
mov byte [gs:0x02],'O'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'V'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'E'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'6'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0A],' '
mov byte [gs:0x0B],0xA4
mov byte [gs:0x0C],'O'
mov byte [gs:0x0D],0xA4
mov byte [gs:0x0E],'S'
mov byte [gs:0x0F],0xA4
jmp $ ;于此处死循环
times 510 - ($ - $$) db 0
db 0x55,0xaa
bochs调试指令速查
Debugger control 类
- q|quit|exit,这三个命令任意一个都能退出调试状态,关闭虚拟机,一般用q最简单
- set是指令族,通常用set设置寄存器的值
- 1) 例如set reg = val 。可以设置的寄存器包括通用寄存器和段寄存器。
2)也可以设置每次停止执行时,是否反汇编指令:set u on|off - show是指令族,有很多子功能,常用就下面3个
1)show mod
每次cpu变换模式时就提示,模式是指保护模式、实模式,
2)show int
每次有中断时就提示,同时显示三种中断类型,“softint”、“extint”和“iret”,可以单独显示某类中断,如执行show softint只显示软件主动触发的中断show extint则只显示来自外部设备的中断,show iret只显示iretd 指令有关的信息
3)show call
每次有函数调用发生时就会提示 - traceon|off 如果此项设为on,每次执行一条指令,bochs都会将反汇编代码打印到控制台,这样在单步调试时免得看源码了。
- u | disasm [/num] [start] [end]
将物理地址start到end之间的代码反汇编,如果不指定地址,则反汇编EIP指向的内存,num指定反汇编的指令数。 - setsize = 16|32|64 在使用反汇编命令时,用来告诉调试器段的大小。
- ctrl+c 中断执行,回到bochs控制台
Execution control 类
- c|cont|continue,这三个命令都意为向下持续执行,若没有断点则一直运行下去
- s|step [count] 执行count条指令,若不指定默认count=1,若遇到函数调用,则会进入函数去执行
- p|n|next 执行1条指令,若待执行的指令是函数调用,不管函数内有多少指令,把整个函数当作一个整体来执行
Breakpoint management 类
以地址打断点:
- vb|vbreak [seg:off] 以虚拟地址添加断点,程序执行到此虚拟地址停下来,注意虚拟地址是“段:段内偏移”的形式。vb比较常用
- lb|lbreak [addr] 以线性地址添加断点,程序执行到此线性地址停下来。lb比较常用
- pb|pbreak|b|break [addr] 以物理地址添加断点,程序执行到此物理地址停下来。b比较常用
以指令数打断点:
- sb [delta] delta表示增量,意味再执行delta条指令程序就中断
- sba [time] CPU从运行开始,执行第time条指令时中断,从0开始的指令数
以读写IO打断点:
- wath r|read [phy_addr] 设置读断点,如果物理地址phy_addr有读操作则停止运行
- wath w|write [phy_addr] 设置写断点,如果物理地址phy_addr有写操作则停止运行
- watch 显示所有读写断点
- unwatch 清除所有断点
- unwatch [phy_addr] 清除在此地址上的读写断点
- bpd|bpe [n] 禁用断点(break point disable) / 启用断点(break point enable),n是断点号,可以用blist命令先检查出来
- d|del|delete [n] 删除某断点,n是断点号,可以用blist命令先查出来
CPU and memory contents 类
x/nuf [line_addr] 显示线性地址的内容。n、u、f是三个参数,都是可选的,如果没有指定,则默认n=1,u=4,f=x
n:显示的单元数
u:每个显示单元的大小
b 1字节
h 2字节
w 4字节
g 8字节
f:显示格式
x 十六进制显示
d 十进制显示
u 无符号十进制显示
o 按照八进制显示
t 按照二进制显示
c 按照字符显示
s 按照ASCLLz显示
i 按照instr显示xp /nuf [phy_addr] 显示物理地址 phy_addr 处的内容,注意和 x 的区别,x 是线性地址。
setpmem [phy_addr] [size] [val] 设置以物理内存 phy_addr 为起始,连续 size 个字节的内容为 val。在某些情况下不易调试时,可以在程序中通过某个地址的值来判断分支,需要用 setpmem 来配合。size 最多只能设置 4 个字节宽度的数据
r|reg|regs|registers 任意四个命令之一便可以显示 8 个通用寄存器的值+eflags 寄存器+eip 寄存器。
ptime 显示 Bochs 自启动之后,总执行指令数。
print-stack [num] 显示堆栈,num 默认为 16,表示打印的栈条目数。输出的栈内容是栈顶在上,低地址在上,高地址在下。这和栈的实际扩展方向相反,这一点请注意。
?|calc 内置的计算器。
info 是个指令族,执行 help info 时可查看其所有支持的子命令,如下:
1)info pb|pbreak|b|break 查看断点信息,等同于 blist。
2)info CPU 显示 CPU 所有寄存器的值,包括不可见寄存器。
3)info fpu 显示 FPU 状态。
4)info idt 显示中断向量表 IDT。
5)info gdt [num]显示全局描述符表 GDT,如果加了 num,只显示 gdt中第 num 项描述符。
6)info ldt 显示局部描述符表 LDT。
7)info tss 显示任务状态段 TSS。
8)info ivt [num]显示中断向量表 IVT。和 gdt 一样,如果指定了 num,则只会显示第 num 项的中断向量。如果想知道 BIOS 在中断向量表中建立了哪些中断,执行此命令就可以看info flags|eflags 显示状态寄存器,其实在用 r 命令显示寄存器值时也会输出 eflags 的状态,还会输出通用寄存器的值,我通常会用 r 来看。
sreg 显示所有段寄存器的值。
dreg 显示所有调试寄存器的值。
creg 显示所有控制寄存器的值。
info tab 显示页表中线性地址到物理地址的映射。
page line_addr 显示线性地址到物理地址间的映射。
硬盘介绍
将磁盘整个盘面划分为多个同心环,以圆心画扇型,扇型与每个同心环相交的弧状区域作为最基本的数据存储单元。这个同心环就称为磁道,而同心环上的弧状区域是扇型的一部分,称为扇区。不同盘面的磁道组成的管状区域就称为柱面。
扇区的编号从1开始。盘面和磁道的编号从0开始。扇区有自己的“头部”,包含扇区自身的信息:磁头号、磁道号和扇区号。
针对硬盘的 IO 接口时硬盘控制器。
3.5.3 硬盘控制器端口
端口分为两组:
- Command Block registers。用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态
- Control Block registers。用于控制硬盘工作状态
端口时按照通道给出的,要想操作某通道上的某块硬盘,需要单独指定。
data 寄存器的作用是读取或写入数据,宽度是16位(其它硬盘控制器寄存器都是8位)。在读硬盘时,硬盘准备好数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,把数据输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
Error 和 Feature 寄存器都是 8 位宽度。
Sector count 寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。最大值是255,若指定为0,表示要操作256个扇区。
逻辑块地址(Logical Block Address),扇区从 0 开始依次递增编号,不用考虑扇区所在的物理结构。
LBA 有两种:
- LBA28。用 28 位比特来描述一个扇区的地址,最大支持128GB
- LBA48。用 48 位比特来描述一个扇区的地址,最大支持128PB
LBA 寄存器有 LBA low、LBA mid、LBA high 三个。LBA low 寄存器用来存储 28 位地址的第 0 ~ 7 位,LBA mid 寄存器用来存储第 8 ~ 15 位,LBA high 寄存器存储第 16 ~ 23 位。
device 寄存器的低 4 位用来存储 LBA 地址的第 24 ~ 27 位。第 4 位用来指定通道上的主盘或从盘,0 代表主盘,1 代表从盘。第 6 位用来设置是否启用 LBA 方式,1 代表启用 LBA 模式,0 代表启用 CHS 模式。第 5 和第 7 位固定为 1,称为 MBS 位。
在读硬盘时,Status 寄存器给出硬盘的状态信息。第 0 位是 ERR 位,如果此位为 1,表示命令出错。第 3 位是 data request 位,如果此位为 1,表示硬盘已经把数据准备好了,主机可以把数据读出来。第 6 位是 DRDY,表示硬盘就绪,可以执行一些命令。第 7 位是 BSY 位,表示硬盘是否繁忙,如果为 1 表示硬盘正忙,此寄存器中其他位都无效。在写硬盘时,Command 寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。
将要使用的命令:
- identify:0xEC,硬盘识别
- read sector:0x20,读扇区
- write sector:0x30,写扇区
3.5.4 常用的硬盘操作方法
command 寄存器最后写,一旦 command 寄存器被写入,硬盘就开始干活。
建议操作步骤:
- 先选择通道,往该通道的 sector count 寄存器写入待操作的扇区数
- 往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位
- 往 device 寄存器中写入 LBA 地址的 24 ~ 27 位,并置第 6 位 1,使其为 LBA 模式,设置第 4 位,选择操作的硬盘(主盘或从盘)
- 往该通道上的 command 寄存器写入操作命令
- 读取该通道上的 status 寄存器,判断硬盘工作是否完成
- 如果以上步骤是读硬盘,进入下一个步骤。否则完工
- 将硬盘数据读出
常用的数据传送方式:
- 无条件传送。数据源设备随时准备好数据,CPU随时拿
- 查询传送。传输前需要先去检测设备的状态
- 中断传送。数据源准备好后,发中断通知 CPU 来拿数据
- 直接存储器存取方式(DMA)。需要 DMA 控制器
- I/O 处理机传送方式。一种专用于处理 I/O 的处理器
; 主引导程序
; --------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00 ; 把起始地址编译为 0x7c00
mov ax, cs ; cs 代码段寄存器
mov ds, ax ; dx 数据段寄存器
mov es, ax ; es 附加段寄存器
mov ss, ax ; ss 堆栈段寄存器
mov fs, ax ; fs 80386 后添加的寄存器,无全称
mov sp, 0x7c00 ; sp 堆栈指针寄存器
mov ax, 0xb800
mov gs, ax ; gs 80386 后添加的寄存器,无全称
; 往 gs 中存入显存段基址
; 文本模式显存的起始地址是 0xb8000
; 清屏
; --------------------------------------------------
; INT 0x10 功能号: 0x06 功能描述:上卷窗口
; --------------------------------------------------
; 输入:
; AH 功能号 = 0x06
; AL = 上卷的行数(如果为0,表示全部)
; BH = 上卷行属性
; (CL, CH) = 窗口左上角的 (X, Y) 位置
; (DL, DH) = 窗口右下角的 (X, Y) 位置
; 无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0
mov dx, 0x184f ; 右下角: (80, 25)
; VGA 文本模式种,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24, 0x4f=79
int 0x10 ; int 0x10
mov eax, LOADER_START_SECTOR ; loader 的起始扇区 LBA 地址
mov bx, LOADER_BASE_ADDR ; 写入的地址
mov cx, 4 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR + 0x300
; 读取硬盘 n 个扇区
rd_disk_m_16:
; eax = LBA 扇区号
; bx = 将数据写入的内存地址
; cx = 读入的扇区数
mov esi, eax ; 备份 eax
mov di, cx ; 备份 cx
; 1. 设置要读取的扇区数
mov dx, 0x1f2
mov al, cl
out dx, al ; 读取的扇区数
mov eax, esi ; 恢复 ax
; 2. 将 LBA 地址存入 0x1f3 ~ 0x1f6
; LBA 地址 7 ~ 0 位写入端口 0x1f3
mov dx, 0x1f3
out dx, al
; LBA 地址 15 ~ 8 位写入端口 0x1f4
mov cl, 8
shr eax, cl
mov dx, 0x1f4
out dx, al
; LBA 地址 23 ~ 16 位写入端口 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
shr eax, cl
and al, 0x0f ; LBA 地址的第 24 ~ 17 位
or al, 0xe0 ; 设置 7 ~ 4 位为 1110,表示 LBA 模式
mov dx, 0x1f6
out dx, al
; 3. 向 0x1f7 端口写入读命令,0x20
mov dx, 0x1f7
mov al, 0x20
out dx, al
; 4. 检测硬盘状态
.not_ready:
; 从 Status 寄存器读硬盘状态
nop
in al, dx
and al, 0x88 ; 第 3 位为 1 表示硬盘控制器已准备好数据传输
; 第 7 位为 1 表示硬盘忙
cmp al, 0x08
jnz .not_ready ; 若硬盘未准备好,继续等
; 5. 从 0x1f0 端口读数据
mov ax, di ; di = 要读入的扇区数
mov dx, 256 ; 一个扇区有 512 个字节,ax 寄存器宽度是 2 字节
; 一共需要读 di * 512 / 2 = di * 256 次
mul dx ; ax = ax * dx
mov cx, ax ; cx = 读硬盘次数
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [bx], ax ; bx=loader将被写到的内存地址
add bx, 2 ; ax 的宽度是两字节,内存地址需要往后移两个字节
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55, 0xaa
// boot.inc
; loader 和 kernel
LOADER_BASE_ADDR equ 0x900 ; loader 被 MBR 写到的内存地址
LOADER_START_SECTOR equ 0x2 ; loader 所在硬盘 LBA 扇区
保护模式入门
我们结束阐述实模式,开始玩玩保护模式!首先我们引出一个问题:为什么需要保护模式呢?
- 操作系统和用户程序同等级别
- 直接操纵的就是真实的物理地址,换而言之,我们的逻辑地址就是物理地址
- 用户可以随意修改段基址,从而任意的访问所有内存
这是安全问题,下面讲讲效率:
- 每当访问超过64KB的内存就要切换段基址
- 单批处理。也就是一次只可以运行一个程序
- 只有20地址线,最大可用内存不过1M。
扩展的寄存器
现在,我们将开启32位时代,首先做的就是扩展我们的寄存器。之前,为了访问32位的数据,我们将之扩展为32位的寄存器,每一个寄存器的名称上我们都加上了一个前缀e:
这样设计是为了兼容先前的8086,偏移地址还是跟实模式一致,但是多加了信息检查,这就需要我们存储大量的检查标准和信息,我们就使用了一个数据结构:叫做GDT(全局描述表),来存放这些信息,每一个表项都是64个字节(64B),他在内存中,所以整一个GDTR指向他就好了。
寻址扩展
在16位下,我们只可以使用bx, bp作为基址寄存器,si, di作为变址寄存器。他们都是有固定使命的。而且,立即操作数(偏移寻址时的立即数)值可以是4个字节大小。这很不方便。所以现在,我们可以更加灵活的编址了:也就是使用扩展了的寄存器,并且允许对变址寄存器乘上一个比例因子(左移1, 2 ,3位)
mov eax, [eax + edx*8 + 0x12345678]
当然,esp不可以用作变址寄存器。但是可以作为基址寄存器。
运行模式反转
我们在编写代码的时候,CPU并不知道我们的代码视在16位实模式下运行还是32位保护模式下运行,我们需要手动指定。很简单
[bits 16] ; 16位
[bits 32] ; 32位
我们来实际看看,两个环境下的代码是如何切换的:
[bits 32]
00000000 B8 78563412 mov eax,0x12345678
00000005 BA 78563412 mov edx,0x12345678
如果我们交叉写呢?
00000000 B83412 mov ax,0x1234
00000003 66B834120000 mov eax,0x1234
00000009 66BA3412BA34 mov edx,0x34ba1234
0000000F 1200 adc al,[bx+si]
00000011 00 db 0x00
可以看到,我们的反汇编器可以识别之,并且会给机器码上标识66,标识的是执行当前的代码需要反转一次状态:可以是16位切换到32位模式下,前面添加了前缀码66.反之亦然。对于寻址模式的反转,我们会在机器码上添加前缀0x67。
GDT
全局描述符号表是一个在保护模式下内存段登记表。为了更好的回忆,我们首先来看看段描述符。一个描述符是64位大小的,我买让我们依次来看看:
首先,是段基址,由于保护模式下地址总线有32根,段基址就需要有32位。段界限字如其名,表示的是段边界的扩展最值,扩展方向可以向上,也可以向下。这对于数据段和代码段,向上扩展,对于栈则是向下扩展。段界限使用20位描述,它是一个数,最终折算成具体界限还是需要看粒度是1字节(1MB)还是4KB(4GB)。这里,段描述符存储了一个位G来说明粒度,1表示4KB,0表示1字节。
下面,还有type字段(S表示的是是否为系统段,0则为系统段,1则是数据段),A表示的是是否可以使用,C表示的是一致性代码,R表示的是是否可读,W表示是否可写,X表示的是代码是否可以执行,E指明扩展方向(1表示向下扩展),DPL字段(描述的当前描述符的特权级),2位可以表示4个字节。P表示的是段是否存在于内存,这是一个标记位。AVL表示的是可用,不过这里是保留的,操作系统可以使用之。DB位则是说明操作数大小的。举个例子:对于代码段,这里用D,D为1则是说明地址是32位的,否则是16位的。对于栈则是B,说明操作数的大小
GDT,LDT和选择子
GDT的位置用一个GDTR寄存器来保存位置,这个寄存器48位大小,前32位存储位置,后16位描述GDT界限,也就是说,他可以指向65536/8=8192个段和门。当我们打开GDT后,段基址寄存器失去意义,段基址被存在表里了,现在他存储的是选择子,低2位放RPL(请求特权级),TI存放的是这个选择子在GDT还是LDT,高十三位就是索引值部分。刚好8192个,对上GDT了。
现在,我们就可以直接使用选择子指向的段描述符,现在就可以直接取地址并接起来就是我们要的地址了。
A20地址线门是为了兼容上个世纪的使用地址回环的技巧的程序方便使用的,这里不阐述了。
下面,我们来看看CR0寄存器,我们聚焦于第一位,也就是PE位,这一位打开了,我们就会进入保护模式了。
我们如何体现保护的呢,答案是使用多出来的一些判断位。向段寄存器加载的时候,需要看高十三位是不是在GDT或者是LDT的数目之内,有点像查看有没有越界访问。先看TI,决定去哪里拿,在看拿到的是啥,也就是看描述符里这片内存的属性。
CS加载代码段,它必须要求数据具有可执行性。
其他四个段寄存器则必须要求代码至少是可读或者是可写的。对于SS寄存器则必须要求可写,余下三个必须要求可读(不然怎么读取)
对于数据段,代码段的访问保护,则是查看代码段和数据段访问处尾端有没有越过段界限下
对于栈段,则是查看有没有低于最大用户栈空间和有没有有没有高于段界限,是就没事,不是则会抛出异常
我们下面来实践一段代码:那就是让我们的操作系统成功的进入保护模式并且整点活出来!
为了操作方便,我们先定义一串宏
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
; gdt create
GDT_BASE: dd 0x0000_0000
dd 0x0000_0000
CODE_DESCRIPTOR dd 0x0000_FFFF
dd DESCRIBTOR_CODE_HIGH4
DATA_STACK_DESCRIPTOR: dd 0x0000_FFFF
dd DESCRIBTOR_DATA_HIGH4
VIDEO_DESCRIPTOR: dd 0x8000_0007; limit = 0xbffff - 0xb8000/4K = 7; text_mode specific
dd DESCRIBTOR_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE;到这里了GDT多长
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0; 60 decriptors; 填充4字节的
; 预备一些选择子
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0; [(CODE_DESC - GDT_BASE) /8 ]<<3+ TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
; gdt pointer
GDT_PTR dw GDT_LIMIT
dd GDT_BASE
loader_msg db '2 loader in real mode!'
loader_start:
;--------------------------------------------------------------------------
;INT 10; | 0x13 functional | print string!
;--------------------------------------------------------------------------
; AH = 13H
; BH = page code
; BL = attribute
; CX = string length
;(DH, DL):POS
; ES:BP string addr
; AL: ways of output
mov sp, LOADER_BASE_ADDR
mov bp, loader_msg
mov cx, 22
mov ax, 0x1301
mov bx, 0x001f
mov dx, 0x1800
int 0x10
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
; Open A20
; Load GDT
; CR0 set first bet Page Enable 1
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
in al, 0x92
or al, 0000_0010B
out 0x92, al
lgdt [GDT_PTR]
mov eax, cr0
or eax, 0x0000_00001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start;刷新流水线
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:0xB0], 'P' ; 放上一个P足矣!
jmp $
引用的boot.inc是
; --------------------LOADER and KERNEL---------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;---------------------Global Describtor Table---------
; 4k set 1 as per sector multiply 4K
DESCRIBTOR_G_4K equ 1000_0000_0000_0000_0000_0000b
; set 1 as to indicate the operator is 32bits instead of 16 bits
DESCRIBTOR_D_32 equ 100_0000_0000_0000_0000_0000b
; No, not 64 bits but 32!
DESCRIBTOR_L equ 00_0000_0000_0000_0000_0000b
; Currently we don't use the AVL bits, as we don't have to make it meaningful!
DESCRIBTOR_AVL equ 0_0000_0000_0000_0000_0000b
; the second segments limitations in high 32 bits
DESCRIBTOR_LIMIT_CODE_2 equ 1111_0000_0000_0000_0000b
; else for the data if the descriptor indicates the data seg
DESCRIBTOR_LIMIT_DATA_2 equ DESCRIBTOR_LIMIT_CODE_2
; for Video(显存) one:
DESCRIBTOR_LIMIT_VIDEO_2 equ 000_0000_0000_0000_0000b
; P for the present, of course it must be present :)
DESCRIBTOR_P equ 1000_0000_0000_0000b
; Following macro sets the Privilage Level of the kernel
DESCRIBTOR_DPL_0 equ 000_0000_0000_0000b
DESCRIBTOR_DPL_1 equ 010_0000_0000_0000b
DESCRIBTOR_DPL_2 equ 100_0000_0000_0000b
DESCRIBTOR_DPL_3 equ 110_0000_0000_0000b
; S stands for the basic type of the descriptor
; 1 for the user des, 0 for the system des
DESCRIBTOR_S_CODE equ 1_0000_0000_0000b
DESCRIBTOR_S_DATA equ DESCRIBTOR_S_CODE
DESCRIBTOR_S_SYS equ 0_0000_0000_0000b
; set the type now! remember it is the X C R A for CODE X E W A for data
; for code it is executable, not the consistence, unable to read , a is for \
; having read, we haven't read it yet, set 0
DESCRIBTOR_TYPE_CODE equ 1000_0000_0000b
; for the data. not executable, extended upper and writable
DESCRIBTOR_TYPE_DATA equ 0010_0000_0000b
; make a sum and get the HIGH32bits
; 代码段高位
DESCRIBTOR_CODE_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_CODE_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + DESCRIBTOR_TYPE_CODE + 0x00
;数据段高位
DESCRIBTOR_DATA_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_DATA_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + DESCRIBTOR_TYPE_DATA + 0x00
; 显卡高位
DESCRIBTOR_VIDEO_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_VIDEO_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + DESCRIBTOR_TYPE_DATA + 0x0B
; --------------------------Sector-------------------------
; Request Privilage Level from 0 to 3
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
; What kind of the Table? Global One or Local One
TI_GDT equ 000b
TI_LDT equ 100b
这里则是存储了大量的比特位操作。可以注释理解
配合上mbr.S
; Main Guide
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax
; --------CLEAR SCREEN-----------------
; INT 10, functional 0x06
; -------------------------------------
mov ax, 0600
mov bx, 0700
mov cx, 0
mov dx, 184f
int 10h
; outPutBytes
mov byte [gs:0x00], 'W'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'l'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'c'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], 'o'
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'm'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'e'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], ' '
mov byte [gs:0x0f], 0xA4
mov byte [gs:0x10], 'P'
mov byte [gs:0x11], 0xA4
mov byte [gs:0x12], 'r'
mov byte [gs:0x13], 0xA4
mov byte [gs:0x14], 'e'
mov byte [gs:0x15], 0xA4
mov byte [gs:0x16], 's'
mov byte [gs:0x17], 0xA4
mov byte [gs:0x18], 'e'
mov byte [gs:0x19], 0xA4
mov byte [gs:0x1a], 'r'
mov byte [gs:0x1b], 0xA4
mov byte [gs:0x1c], 'v'
mov byte [gs:0x1d], 0xA4
mov byte [gs:0x1e], 'e'
mov byte [gs:0x1f], 0xA4
mov byte [gs:0x20], 'd'
mov byte [gs:0x21], 0xA4
mov byte [gs:0x22], ' '
mov byte [gs:0x23], 0xA4
mov byte [gs:0x24], 'M'
mov byte [gs:0x25], 0xA4
mov byte [gs:0x26], 'o'
mov byte [gs:0x27], 0xA4
mov byte [gs:0x28], 'd'
mov byte [gs:0x29], 0xA4
mov byte [gs:0x2a], 'u'
mov byte [gs:0x2b], 0xA4
mov byte [gs:0x2c], 'l'
mov byte [gs:0x2d], 0xA4
mov byte [gs:0x2e], 'e'
mov byte [gs:0x2f], 0xA4
mov byte [gs:0x30], '!'
mov byte [gs:0x31], 0xA4
mov eax, LOADER_START_SECTOR;起始扇区地址
mov bx, LOADER_BASE_ADDR; 写入的地址
mov cx, 4;待读入的扇区个数
call rd_disk_m_16
jmp LOADER_BASE_ADDR
;---------------------------
;LOAD DISK
;---------------------------
rd_disk_m_16:
;eax LBA ADDR; BX where shall we write into CX : amounts of disks
mov esi, eax
mov di, cx; Save the important index
; First:set the read amount
mov dx, 0x1f2
mov al, cl
out dx, al
mov eax, esi
;LBA 0-7 bits to 0x1f3
mov dx, 0x1f3
out dx, al
; LBA 8-15 to 0x1f4
mov cl, 8
shr eax, cl ; shr means shift right cl bits
mov dx, 0x1f4
out dx, al
; LBA 16-23
shr eax, cl
mov dx, 0x1f5
out dx, al
shr eax, cl
and al, 0x0f
or al, 0xe0; 1110
mov dx, 0x1f6
out dx, al
mov dx, 0x1f7
mov al, 0x20;输入写指令
out dx, al
;check the state
.not_ready:
nop
in al, dx
and al, 0x88; 1000 1000检查第三位是不是1,是1表示准备结束
cmp al, 0x08
jnz .not_ready
; ready to ready from 0x1f0
mov ax, di
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .go_on_read
ret
times 510 - ($ - $$) db 0
db 0x55, 0xaa
然后,先将MBR写到第一个扇区,再将Loader写道后面的扇区:
dd if=mbr.bin of=a.img count=1 bs=512 seek=0 conv=notrunc
dd if=loader.bin of=a.img count=2 bs=512 seek=2 conv=notrunc
bochs -f bashsrc.ab
这样就可以了:
保护模式进阶
获取物理内存的容量
本质上调用BIOS的0x15实现
EAX=0xE820; 遍历主机全部内存
AX = 0xE801; 检测低15MB和16至4GB的内存
AH=0x88
这一方法在实模式下才可以使用,Linux自己也是在进入保护模式之前完成对内存容量的检查了,然后写近内存方便之后的工作拿取
0x15下的0xE820
这一方法是迭代的查询每一个种类的内存分配,直到所有的类型都检查完毕。所以返回的信息更加丰富。需要一个格式结构来组织这些数据——那就是ARDS
字节偏移量 | 属性名称 | 描述 |
---|---|---|
0 | BaseAddrLow | 基地址的低32位 |
4 | BaseAddrHigh | 基地址的高32位 |
8 | LengthLow | 内存长度的低32位,单位是字节 |
12 | LengthHigh | 内存长度的高32位,单位是字节 |
16 | Type | 种类 |
Type的值分为可以被操作系统直接使用和保留。我们来仔细看看如何使用之, 下面是寄存器的输入输出起到的作用:
寄存器 | 作用 |
---|---|
eax | 功能码,当输入e820h时能够探测内存 |
ebx | 主要用于指向内存区域,第一次调用时ebx=0,被称为continuation value |
es:di | 用于指令执行后,在指向的内存写入描述内存区域的数据结构ARDS(Address Range Descriptor Structure) |
ecx | 用于限制指令填充的ARDS的大小,实际上大多数情况这个是无效的,无论ecx设置为多少,BIOS始终会填充20字节的ARDS |
edx | 0534D4150h(‘SMAP’),输入时在edx,输出时将会在eax中 |
下面是输出时各个寄存器(标志)的结果:
寄存器 | 结果 |
---|---|
CF | 当没有发生错误时,CF=0,否则CF=1 |
eax | 0534D4150h(‘SMAP’) |
ebx | 指向下一个内存区域,而不是调用之前的内存区域,当ebx=0且CF=0时,表示当前是最后一个内存区域。 |
es:di | 和调用之前一样,如果要保存多个ARDS,需要手动修改es:di |
ecx | 返回写入的ARDS的大小 |
所以,简单的讲就是需要我们这样做:
1)填写好“调用前输入所列出的寄存器”
2)执行中断int 0x15
3)在CF位是0下,输出对应的寄存器就会有我们想要的结果
0xE801
向ax写入输入号后发其调用即可
输出:
寄存器 | 描述 |
---|---|
cf | 0为出错,1出错了 |
ax | KB为单位,只显示15MB一下的内存,最大就是0x3c00 |
bx | 64KB为单位,内存空间16MB-4GB连续的单位数量 |
cx | 跟ax一样 |
dx | 跟bx一样 |
这个的功能就会较为简单,我们只需要向AX寄存器写入0xE801后执行中断,在判断CF位是0后取数即可。但是这种方法需要注意,他总是返回少1MB的内存。
80286是拥有24位地址线,其寻址空间是16MB。当时有一些ISA设备要用到地址15MB以上的内存做为缓冲区,也就是此缓冲区为1MB大小,所以硬件系统就把这部分内存保留下来,操作系统不可以用此段内存空间。保留的这部分内存区域就像不可以访问的黑洞,这就成了内存空洞memory hole。现在虽然很少很少能碰到这些老ISA设备了,但为了兼容,这部分空间还是保留下来,只不过是通过bios选项的方式由用户自己选择是否开启。bios厂商不同,一般的菜单选项名称也不相同,不过大概意思都差不多。比如咱们开机进入bios界面后,会有类似这样的选项:
memory hole at address 15m-16m
将此选项设为enable或disable便开启或关闭对这类扩展ISA设备的支持。话说,起初定义这个0xe801子功能,就是为了支持扩展ISA服务。现在来回答这个问题。如果检测到的内存容量大于等于16MB,bios 0x15中断返回的结果中,AX*1024必然是小于等于15MB,而BX*64*1024肯定大于0。所以,内存容量分成两部分展示,只要符合这两个结果,就能检查出内存空洞。当然如果物理内存在16MB以下,此方法就不灵了,但检测到的内存依然会小于实际内存1MB。所以实际的物理内存大小,在检测结果的基础上一定要加上1MB。
0x88
这是最后一个,也是最简单的一个,
方法一样,上班写代码,我们依次来看看如何调用:
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
; gdt create
GDT_BASE: dd 0x0000_0000
dd 0x0000_0000
CODE_DESCRIPTOR dd 0x0000_FFFF
dd DESCRIBTOR_CODE_HIGH4
DATA_STACK_DESCRIPTOR: dd 0x0000_FFFF
dd DESCRIBTOR_DATA_HIGH4
VIDEO_DESCRIPTOR: dd 0x8000_0007; limit = 0xbffff - 0xb8000/4K = 7; text_mode specific
dd DESCRIBTOR_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 59 dq 0; 60 decriptors ;DQ 定义的变量为4字型(8字节)Define Quadra Word
times 5 db 0
total_mem_bytes dd 0 ;保存内存中的总量大小,而我们默认bin vstart = 0x900 + 0x200 = 0xb00
; why 0x200 = 512? 60 x 8(discriptor) + 32btye(Current GDT Size: 0 for ununsable, code, video and data)
GDT_PTR dw GDT_LIMIT; GDT_Reg 48 bits == 1 w + 1 dw
dd GDT_BASE
; tol_mem_bytes 4 + gdt_ptr 6 + buf 244 + ards_nr 2 = 256
ards_buf times 244 db 0 ; Auto Filled the bytes
ards_nr dw 0 ; record the number of ARDS
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
; gdt pointer
loader_start:
; int 15h eax = 0000_E820, edx = 534D4150h "SMAP" as the signature to check
xor ebx, ebx
mov edx, 0x534D4150
mov di, ards_buf
.e820_mem_get_loop:
mov eax, 0x0000_E820; caller
mov ecx, 20 ;ARDS is 20 bytes
int 0x15 ; call
jc .e820_mem_failed_try_e801 ; cf is 1 to signal the error, jmp to the second method
add di, cx
inc word [ards_nr] ; ards_nr++
cmp ebx, 0 ; is 0 means finish all and ready return
jnz .e820_mem_get_loop
; find max
mov cx, [ards_nr]
mov ebx, ards_buf
xor edx, edx; clear edx to store the largest
.find_max_mem_area:
mov eax, [ebx];base add low
add eax, [ebx + 8];length_low
add ebx, 20; next
cmp edx, eax
; we always store largest in edx
jge .next_ards
mov edx, eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
; --- E801H --------
.e820_mem_failed_try_e801:
mov eax, 0xe801
int 0x15
jc .e801_failed_try_88
mov cx, 0x400
mul cx
shl edx, 16
and eax, 0x0000FFFF
or edx, eax
add edx, 0x10_0000; ax = 15MB
mov esi, edx; store
; 16MB+
xor eax, eax
mov ax, bx
mov ecx, 0x10000 ; 64KB
mul ecx ; High in edx, low in eax
add esi, eax
mov edx, esi
jmp .mem_get_ok
; ----0x88---
.e801_failed_try_88:
mov ah, 0x88
int 0x15
jc .error_hlt
and eax, 0x0000_FFFF
mov cx, 0x400; change it to byte
mul cx
shl edx, 16; dx left move to higher place
or edx, eax; then we get dx | ax as 32bits
add edx, 0x100000; add the bonus 1MB
.mem_get_ok:
mov [total_mem_bytes], edx
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
; Open A20
; Load GDT
; CR0 set first bet Page Enable 1
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
in al, 0x92
or al, 0000_0010B
out 0x92, al
lgdt [GDT_PTR]
mov eax, cr0
or eax, 0x0000_00001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start
.error_hlt:
hlt
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:0xB0], 'P'
jmp $
我们查看一下,在0xb00位置处:
xp 0xb00
10进制下就是32MB,成功!
开启分页机制
我们下面就要开启分页机制了,打开分页后,我们的CPU就不会将简单的线性地址直接认为是物理地址了。现在,我们将连续的线性地址与任意的物理内存相互关联(就是说,我们需要将线性地址经过转化才可以是物理地址)
分页机制就干两件事情:
- 将线性地址通过映射得到物理地址
- 将大小相等的页替换大小不等的段
现在,这个线性地址有了新的名称:虚拟地址。那么,我们又会是怎样的映射呢?答案是,以4KB作为粒度,这样分出来1M个页,用20个位表示即可,而剩下的12位恰好描述了4KB(2的12次方)。然而,一级页表还是大小可观,所以我们采用的是二级页表,每一个页表的物理地址都用页目录项的方式存储(Page Directory Entry)。采用了二级页表,我们就需要给虚拟地址这样分:前10位寻一级页表,中间10位给二级页表,最后12位是Offset。
页表也有属性,下面一一介绍:
位 | 说明 |
---|---|
P | Present表示的是在不在物理内存 |
RW | Read Write权限 |
US | User还是Supervisor,权限大小 |
PWT | 页级通写位,1表示的是在高速缓存 |
PCD | 页级高速缓存禁止位 |
A | 访问位(可以用来记录内存的访问频率) |
D | 是否有效 |
PAT | 页属性位表 |
Global | 在不在TLB中,在的话可以直接的从TLB中取出地址直接转换,是用来加速的 |
开启分页做好三件事情:
- 准备好页目录表和页表
- 将页表地址写入cr3寄存器
- cr0的PG置1
我们先给boot.inc添加点新代码:这是一些关于页表的设置问题
; -----------------------Page Table ---------------
PAGE_DIR_TABLE_POS equ 0x1000000
; -----------------------Page Table Related Attribute
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 01b
PG_US_S equ 000b
PG_US_U equ 100b
下面,我们继续补充代码:
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
; gdt create
GDT_BASE: dd 0x0000_0000
dd 0x0000_0000
CODE_DESCRIPTOR dd 0x0000_FFFF
dd DESCRIBTOR_CODE_HIGH4
DATA_STACK_DESCRIPTOR: dd 0x0000_FFFF
dd DESCRIBTOR_DATA_HIGH4
VIDEO_DESCRIPTOR: dd 0x8000_0007; limit = 0xbffff - 0xb8000/4K = 7; text_mode specific
dd DESCRIBTOR_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0; 60 decriptors ;DQ 定义的变量为4字型(8字节)Define Quadra Word
total_mem_bytes dd 0 ;保存内存中的总量大小,而我们默认bin vstart = 0x900 + 0x200 = 0xb00
; why 0x200 = 512? 60 x 8(discriptor) + 32btye(Current GDT Size: 0 for ununsable, code, video and data)
GDT_PTR dw GDT_LIMIT; GDT_Reg 48 bits == 1 w + 1 dw
dd GDT_BASE
; tol_mem_bytes 4 + gdt_ptr 6 + buf 244 + ards_nr 2 = 256
ards_buf times 244 db 0 ; Auto Filled the bytes
ards_nr dw 0 ; record the number of ARDS
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
; gdt pointer
loader_start:
; int 15h eax = 0000_E820, edx = 534D4150h "SMAP" as the signature to check
xor ebx, ebx
mov edx, 0x534D4150
mov di, ards_buf
.e820_mem_get_loop:
mov eax, 0x0000_E820; caller
mov ecx, 20 ;ARDS is 20 bytes
int 0x15 ; call
jc .e820_mem_failed_try_e801 ; cf is 1 to signal the error, jmp to the second method
add di, cx
inc word [ards_nr] ; ards_nr++
cmp ebx, 0 ; is 0 means finish all and ready return
jnz .e820_mem_get_loop
; find max
mov cx, [ards_nr]
mov ebx, ards_buf
xor edx, edx; clear edx to store the largest
.find_max_mem_area:
mov eax, [ebx];base add low
add eax, [ebx + 8];length_low
add ebx, 20; next
cmp edx, eax
; we always store largest in edx
jge .next_ards
mov edx, eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
; --- E801H --------
.e820_mem_failed_try_e801:
mov eax, 0xe801
int 0x15
jc .e801_failed_try_88
mov cx, 0x400
mul cx
shl edx, 16
and eax, 0x0000FFFF
or edx, eax
add edx, 0x10_0000; ax = 15MB
mov esi, edx; store
; 16MB+
xor eax, eax
mov ax, bx
mov ecx, 0x10000 ; 64KB
mul ecx ; High in edx, low in eax
add esi, eax
mov edx, esi
jmp .mem_get_ok
; ----0x88---
.e801_failed_try_88:
mov ah, 0x88
int 0x15
jc .error_hlt
and eax, 0x0000_FFFF
mov cx, 0x400; change it to byte
mul cx
shl edx, 16; dx left move to higher place
or edx, eax; then we get dx | ax as 32bits
add edx, 0x100000; add the bonus 1MB
.mem_get_ok:
mov [total_mem_bytes], edx
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
; Open A20
; Load GDT
; CR0 set first bet Page Enable 1
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
in al, 0x92
or al, 0000_0010B
out 0x92, al
lgdt [GDT_PTR]
mov eax, cr0
or eax, 0x0000_00001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start
.error_hlt:
hlt
[bits 32]
p_mode_start:
; 初始化GDT
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
call setup_page
sgdt [GDT_PTR]
mov ebx, [GDT_PTR + 2]
or dword [ebx + 0x18 + 4], 0xc000_0000
add dword [GDT_PTR + 2], 0xc000_0000
add esp, 0xc0000000
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
mov eax, cr0
or eax, 0x8000_0000
mov cr0, eax
lgdt [GDT_PTR]
mov byte [gs:160], 'V'
jmp $
; -------------------setup page table ---------
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir: ; 先把页表空间清0
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
;PDE
.create_pde: ; Create Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000;First PDE's attr
mov ebx, eax ; ready for PTE
; kernel one and usr one
or eax, PG_US_U | PG_RW_W | PG_P
; init the basic attr
mov [PAGE_DIR_TABLE_POS + 0x0], eax
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; kernel
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax; final one points to itself
; create PTE
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi*4], edx
add edx, 4096
inc esi
loop .create_pte
; kernel pte
mov eax, PAGE_DIR_TABLE_POS
mov eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
放鞭炮!
TLB
TLB是translation lookaside buffer的简称。首先,我们知道MMU的作用是把虚拟地址转换成物理地址。虚拟地址和物理地址的映射关系存储在页表中,而现在页表又是分级的。64位系统一般都是3~5级。常见的配置是4级页表,就以4级页表为例说明。分别是PGD、PUD、PMD、PTE四级页表。在硬件上会有一个叫做页表基地址寄存器,它存储PGD页表的首地址。MMU就是根据页表基地址寄存器从PGD页表一路查到PTE,最终找到物理地址(PTE页表中存储物理地址)。这就像在地图上显示你的家在哪一样,我为了找到你家的地址,先确定你是中国,再确定你是某个省,继续往下某个市,最后找到你家是一样的原理。一级一级找下去。这个过程你也看到了,非常繁琐。如果第一次查到你家的具体位置,我如果记下来你的姓名和你家的地址。下次查找时,是不是只需要跟我说你的姓名是什么,我就直接能够告诉你地址,而不需要一级一级查找。四级页表查找过程需要四次内存访问。延时可想而知,非常影响性能。
加载内核
芜湖!终于可以C语言写代码了!下面,我们单刀直入,直接看看我们的main.c:
int main(void)
{
while(1);
return 0;
}
让我们查看一下:
gcc -c -o main.c main.o
file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
这是可重定位文件,这是大家都熟悉的(relocatable)
我们使用nm指令查看符号定位情况,幸好,他足够简单
0000000000000000 T main
没有被重定位,那么地址自然是0!我们编译完成,就需要链接:
ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
-e手动指定了我们的程序入口在哪个地方。假使我们不加-e main
ld main.o -Ttext 0xc0001500 -o kernel.bin
ld: 警告: 无法找到项目符号 _start; 缺省为 00000000c0001500
嘶,我们这样呢?
int _start(void)
{
while(1);
return 0;
}
重新编译链接,没问题了!
回忆一下,在此之前,我们是怎么调用程序的,BIOS初始化之后,将第0扇区的MBR加载到0x7c00并且跳到那里执行,mbr再去调用loader,loader的地址是0x900。可以看到这些程序的地址都是固定的,并且调用方和被调用方需要约定好地址,存储在哪。这种方式是很不灵活的,我们可以提供一种较为灵活地方式来加载各种可执行程序吗?有,这种方式就是文件头+文件体。
文件头和文件体的运用还是不少的,比如我们学习计算机网络时,从应用层到链路层每一层都会添加一个头部用来描述这一层用的是什么协议,大小是多少等等。头部像是书上的目录,而文件体就像书的每一章内容,通过书上目录显示的每一章的页码,我们就能找到对应章的位置。
个人理解,文件头+文件体的方式并不是说不需要指定入口地址,它只不过是将入口地址等信息放到一个统一的模块去存储,从某种意义上说,其实和加载MBR和Loader没啥区别,都是需要得到入口地址,而这个入口地址无非是一个放在了一个常数变量里(宏定义),一个放在了一个类似表的东西(文件头),只不过我们约定好了一种文件格式,我们都总能根据格式规范找到入口地址,不再需要自己去额外定义常数,这会更加统一和灵活一些,就像C语言定义了一个结构体,结构体里定义好了有哪些成员,我们预先知道了这个结构体有什么成员,在哪个位置,到这个位置取出变量即可,文件头就和结构体类似,假设文件头告诉你它包含文件体大小和入口地址两个成员,而入口地址在文件体的第二个成员的位置,你去那里找就好了,这样的话任何可执行文件都可以按这种方式找到这个入口地址,不再需要定义变量这种不灵活的方式了。另外值得注意的一点是,程序头部是编译器生成的,那么入口地址自然也是编译器去赋值的 ,当然赋值多少要看编译器怎么实现,我们可以让编译器生成目标文件时指定入口地址,也可以让编译器自己去决定入口地址。
那么对于程序来说,程序头和程序体需要包含什么呢?程序头最基本需要包含程序体的大小和程序的入口地址,程序体大小限制了程序体的边界,程序入口地址方便让我们跳到指定的地址执行代码;程序体自然包括程序代码和其他程序数据了。如下图:
基本的思想就是这样,但具体到实际的话,会有很多细节上的东西,比如如何识别这是个可执行文件,一个程序包含许多节(链接后会划分成段),如何在头部指定这些节或段的具体位置等等。
那么操作系统显然是预先知道这个约定的,但具体怎么规划或实现在各个操作系统都不太一样,我们可以说每个操作系统都有自己的文件格式,即自己文件头和文件体的规划。如Window下可执行文件格式为PE,Linux下采用的可执行文件格式是ELF,Mac系统下的可执行文件格式为Mach-O。说个题外话,一开始我还想再Mac系统下做接下来的实验的,不过一路坐下来发现mac系统和linux系统下文件格式有很大的不同,学习成本有点高,不过有兴趣的同学可以试试,接下来说的还是以Linux的文件格式来加载我们的程序。
Linux下的可执行文件格式为ELF,即Executable and Linkable Format,可执行链接格式。与ELF相关的文件类型有三种,是我们需要区分一下的,如下图:
我们将待重定位文件即没有编址好的文件称为目标文件,将动态链接库称为共享目标文件,将编译链接后的文件称为可执行文件。但以ELF规范的命名方式,这三种文件都称之为ELF目标文件。为了避免混淆,之后我们说目标文件是这三种类型的文件,不单单指待重定位的文件。
段和节
之前我们有涉及过段和节的概念,节,之前有说过,就是人为将代码划分成几个模块,便于程序员理解模块的功能,节的声明并不会影响地址的编排。段不是内存的段,而是文件概念上的段,内存里真正运行的是段而不是节,所以ELF有个专门的字段叫程序头表,它描述段的存储信息,以“程序”头表称呼代表段才是程序真正执行的部分。节和段又是什么关系呢,在链接器将重定位文件合并为可执行文件时,多个节最终会合并成段,虽然节的声明并不会改变地址编排,但节到段的过程可能会改变节中指令的地址,毕竟节不是程序真正执行的部分,下面会结合例子讲讲这个过程。
本书对目标文件的节和段的描述不算太具体,以下结合《深入理解计算机系统》的链接章节进行补充。
可重定位目标文件
在可重定位目标文件中只有节,没有段,一个典型的ELF可重定位目标文件的结构如下图:
把《深入理解计算机系统》对每个节的描述复制过来 ,让大家看看:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关(switch)语句的跳转表。
- .data:已初始化的全局C变量。局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中。
- .bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率在:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
- .symtab:一个符号表(symbol table),它存放在程序中被定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的表目。
- .rel.text:当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。
- .rel.data:被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。
- .debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会得到这张表。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时,才会得到这张表。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
上面我们不是生成了main.o待重定位目标文件吗,通过readelf工具看看里面有什么节:
readelf -S main.o
输出结果如下:
可执行目标文件
一个典型的ELF可执行文件结构如下图:
此时的可执行文件已经将各个节合并成段了,我们也用readelf工具查看一下kernel.bin段和节的情况。
readelf -a kernel.bin
通过上面的命令得到ELF目标文件的全部信息。
通过上图,我们可以发现可执行目标文件只剩下几个section,重定位目标文件里的一些section不见了,分别是.data,.rel.eh_frame,.note.GNU-stack,.bss节。.rel.eh_frame好理解,毕竟只用作重定位,帮助被引用的符号找到正确的地址,而.data,.bss,.note.GNU-stack,个人觉得是因为它们的大小为0,到可执行文件就没必要存在了。
最终可执行文件将节合并为两个段,将.text,.eh_frame合并为一个段LOAD,可以称之为代码段,因为包含.text可执行的代码,还有一个段GNU_STACK不知道是什么用途,而且它大小为0就不管了。还有一些节并没有合并成段,原因是它们并不是程序执行的部分,没必要加载到内存里面,所以看不到有段包含它们。
我们再把注意力放到节和段的编址上,着重看.text和.eh_frame,因为它们最终组成了代码段,我们发现.text和.eh_frame的地址变了,它们的相对距离也变了;在可重定位文件中,.text和.eh_frame是分开的,而在可执行文件中,.text和.eh_frame是紧挨着的,所以合并成段后,段所包含的节地址是连续的。
相同名称的节也会合并在一起
具有相同名字的节最终会合并在一起。
为了帮助理解节最终合并成段的过程,我下面编写一段小代码test.S:
section .data
db 0x1,0x2
section .text
add esp,4
section .data
db 0x3,0x4
section .text
add esp,5
编译该文件:
nasm test.S
再反汇编生成的文件:
ndisasm test
输出如下:
由于是直接利用nasm工具将汇编代码转化成可执行代码的,我们并不知道哪些节合并成什么段,但我们可以通过地址去猜测哪些节合并在一起了。
根据上面的输出,所有.data的节合并在一起了,所有.text的节合并在一起了,而且地址是连续的。还有就是.text和.data同时存在时,会把.text优先放到前面,因为按约定俗成的叫法,.text是可执行的部分,汇编器nasm优先把.text放在前面。如果把.text和.data改成.text1和.data1,这样就是用户自己定义的节,结果是.data1会放在前面,所以如果不存在.text和.data这类约定的节,相同名称的节合并成段时会按照这些节之中第一个的位置来编排地址。
至此我们对段和节,以及它们直接的关系都有比较直观的理解了。
ELF布局
理解完节和段之后,我们再从ELF整个布局理解一下,操作系统是怎么将ELF文件加载到内存里面的。
ELF的布局如上图所示,ELF的布局在链接阶段和运行阶段并不太一样,主要是因为节最终会合并成段,不过我们发现ELF头是共同存在的。
ELF头
ELF头部结构如下图,可以去linux系统下/usr/include/elf.h找到这个定义:
ELF头的每个成员含义可以在注释里面看到,由于我们需要获取程序段的代码,我们需要把注意力放在和Program header相关的成员上,在后面代码编写上我们主要用到e_phentsize、e_phoff、e_phnum,分别代表程序头表的大小、距离文件开头的偏移、个数。
每个成员的字节大小可以根据上图得到,这有什么作用呢?由于我们获取文件段还是要用汇编代码编写的,所以我们需要知道这些成员距离文件开头的偏移位置,e_phentsize、e_phoff、e_phnum距离文件开头的位置分别为42,28,44。
程序头表
虽然说了节和段是什么东西,但还没讲程序头表是什么,程序头表是程序头的数组,程序头表的程序头地址是连续的,不是离散的。程序头是什么,程序头是描述段的数据结构。
下面看一下程序头的结构是怎样的。
每个成员的含义可以在注释看到,后面我们主要用到p_type、p_filesz、p_offset、p_vaddr,分别代表段的类型、文件内的大小、距离文件开头的偏移、在内存中的地址。
p_type取值与含义如下:
加载段到内存
综合上面一节,我们可以得出加载段到内存的流程:
①得到程序头的大小
②得到第一个程序头的偏移量
③得到程序头的个数
开始复制段:
④判断段类型是否是忽略,是的话不复制,跳到⑦,否则继续
⑤得到段在文件的偏移量、段的大小、在内存的地址
⑥将段复制到内存里
⑦判断是否全部段都复制好了,不是的话,跳到下一个程序头,跳到④,否则复制完成
TIPS:
这里需要注意的是:之后我们的C语言代码需要 使用gcc 4.4进行编译,需要我们去下载gcc 4.4,方法如下:
添加deb源:
deb http://dk.archive.ubuntu.com/ubuntu/ trusty main universe deb http://dk.archive.ubuntu.com/ubuntu/ trusty-updates main universe
先update
sudo apt-get install g++-4.4
会出现公匙问题,这里对应的添加即可
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
再次更新,一切照常!
sudo apt-get install g++-4.4
安装结束后,我们需要使用
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 60
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.4 40
来添加gcc切换组
现在,我们只需要:
sudo update-alternatives --config gcc
输入编号就可以自动切换链接了!
下面我们给上代码,这里,我们的操作系统开始真正的拥有雏形。现在,我们分分区:
将我们先前打磨的boot.inc,loader.S放到位置boot下:
将我们的main.c放到kernel下:
修改一下我们的编译脚本和加载脚本:
Compile.sh
nasm -I boot/include/ boot/loader.S -o boot/loader.bin
gcc -m32 -c -o kernel/main.o kernel/main.c
ld -m elf_i386 kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
Load.sh
dd if=./boot/mbr.bin of=a.img count=1 bs=512 seek=0 conv=notrunc
dd if=./boot/loader.bin of=a.img count=4 bs=512 seek=2 conv=notrunc
dd if=./kernel/kernel.bin of=a.img count=200 bs=512 seek=9 conv=notrunc
bochs -f bashsrc.ab
我们最后看一眼boot.inc
; --------------------LOADER and KERNEL---------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;---------------------Global Describtor Table---------
; 4k set 1 as per sector multiply 4K
DESCRIBTOR_G_4K equ 1000_0000_0000_0000_0000_0000b
; set 1 as to indicate the operator is 32bits instead of 16 bits
DESCRIBTOR_D_32 equ 100_0000_0000_0000_0000_0000b
; No, not 64 bits but 32!
DESCRIBTOR_L equ 00_0000_0000_0000_0000_0000b
; Currently we don't use the AVL bits, as we don't have to make it meaningful!
DESCRIBTOR_AVL equ 0_0000_0000_0000_0000_0000b
; the second segments limitations in high 32 bits
DESCRIBTOR_LIMIT_CODE_2 equ 1111_0000_0000_0000_0000b
; else for the data if the descriptor indicates the data seg
DESCRIBTOR_LIMIT_DATA_2 equ DESCRIBTOR_LIMIT_CODE_2
; for Video(显存) one:
DESCRIBTOR_LIMIT_VIDEO_2 equ 000_0000_0000_0000_0000b
; P for the present, of course it must be present :)
DESCRIBTOR_P equ 1000_0000_0000_0000b
; Following macro sets the Privilage Level of the kernel
DESCRIBTOR_DPL_0 equ 000_0000_0000_0000b
DESCRIBTOR_DPL_1 equ 010_0000_0000_0000b
DESCRIBTOR_DPL_2 equ 100_0000_0000_0000b
DESCRIBTOR_DPL_3 equ 110_0000_0000_0000b
; S stands for the basic type of the descriptor
; 1 for the user des, 0 for the system des
DESCRIBTOR_S_CODE equ 1_0000_0000_0000b
DESCRIBTOR_S_DATA equ DESCRIBTOR_S_CODE
DESCRIBTOR_S_SYS equ 0_0000_0000_0000b
; set the type now! remember it is the X C R A for CODE X E W A for data
; for code it is executable, not the consistence, unable to read , a is for \
; having read, we haven't read it yet, set 0
DESCRIBTOR_TYPE_CODE equ 1000_0000_0000b
; for the data. not executable, extended upper and writable
DESCRIBTOR_TYPE_DATA equ 0010_0000_0000b
; make a sum and get the HIGH32bits
DESCRIBTOR_CODE_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_CODE_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + \
DESCRIBTOR_TYPE_CODE + 0x00
DESCRIBTOR_DATA_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_DATA_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + \
DESCRIBTOR_TYPE_DATA + 0x00
DESCRIBTOR_VIDEO_HIGH4 equ (0x00 << 24) + DESCRIBTOR_G_4K + \
DESCRIBTOR_D_32 + DESCRIBTOR_L + \
DESCRIBTOR_AVL + DESCRIBTOR_LIMIT_VIDEO_2 + \
DESCRIBTOR_P + DESCRIBTOR_DPL_0 + DESCRIBTOR_S_CODE + \
DESCRIBTOR_TYPE_DATA + 0x0B
; --------------------------Sector-------------------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
; -----------------------Page Table ---------------
PAGE_DIR_TABLE_POS equ 0x1000000
; -----------------------Page Table Related Attribute
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 01b
PG_US_S equ 000b
PG_US_U equ 100b
; -------------------Kernel Related -----------------
KERNEL_BIN_SECTOR equ 0x9
KERNEL_BIN_BASE_ADDR equ 0x70000
KERNEL_ENTRY_ADDR equ 0xc000_1500
PT_NULL equ 0x0
(新加的就在最后五行上)
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
; gdt create
GDT_BASE: dd 0x0000_0000
dd 0x0000_0000
CODE_DESCRIPTOR dd 0x0000_FFFF
dd DESCRIBTOR_CODE_HIGH4
DATA_STACK_DESCRIPTOR: dd 0x0000_FFFF
dd DESCRIBTOR_DATA_HIGH4
VIDEO_DESCRIPTOR: dd 0x8000_0007; limit = 0xbffff - 0xb8000/4K = 7; text_mode specific
dd DESCRIBTOR_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0; 60 decriptors ;DQ 定义的变量为4字型(8字节)Define Quadra Word
total_mem_bytes dd 0 ;保存内存中的总量大小,而我们默认bin vstart = 0x900 + 0x200 = 0xb00
; why 0x200 = 512? 60 x 8(discriptor) + 32btye(Current GDT Size: 0 for ununsable, code, video and data)
GDT_PTR dw GDT_LIMIT; GDT_Reg 48 bits == 1 w + 1 dw
dd GDT_BASE
; tol_mem_bytes 4 + gdt_ptr 6 + buf 244 + ards_nr 2 = 256
ards_buf times 244 db 0 ; Auto Filled the bytes
ards_nr dw 0 ; record the number of ARDS
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
; gdt pointer
loader_start:
; int 15h eax = 0000_E820, edx = 534D4150h "SMAP" as the signature to check
xor ebx, ebx
mov edx, 0x534D4150
mov di, ards_buf
.e820_mem_get_loop:
mov eax, 0x0000_E820; caller
mov ecx, 20 ;ARDS is 20 bytes
int 0x15 ; call
jc .e820_mem_failed_try_e801 ; cf is 1 to signal the error, jmp to the second method
add di, cx
inc word [ards_nr] ; ards_nr++
cmp ebx, 0 ; is 0 means finish all and ready return
jnz .e820_mem_get_loop
; find max
mov cx, [ards_nr]
mov ebx, ards_buf
xor edx, edx; clear edx to store the largest
.find_max_mem_area:
mov eax, [ebx];base add low
add eax, [ebx + 8];length_low
add ebx, 20; next
cmp edx, eax
; we always store largest in edx
jge .next_ards
mov edx, eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
; --- E801H --------
.e820_mem_failed_try_e801:
mov eax, 0xe801
int 0x15
jc .e801_failed_try_88
mov cx, 0x400
mul cx
shl edx, 16
and eax, 0x0000FFFF
or edx, eax
add edx, 0x10_0000; ax = 15MB
mov esi, edx; store
; 16MB+
xor eax, eax
mov ax, bx
mov ecx, 0x10000 ; 64KB
mul ecx ; High in edx, low in eax
add esi, eax
mov edx, esi
jmp .mem_get_ok
; ----0x88---
.e801_failed_try_88:
mov ah, 0x88
int 0x15
jc .error_hlt
and eax, 0x0000_FFFF
mov cx, 0x400; change it to byte
mul cx
shl edx, 16; dx left move to higher place
or edx, eax; then we get dx | ax as 32bits
add edx, 0x100000; add the bonus 1MB
.mem_get_ok:
mov [total_mem_bytes], edx
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
; Open A20
; Load GDT
; CR0 set first bet Page Enable 1
; ----------------LOAD_TO_PRESERVED_MODE--------------------------------
in al, 0x92
or al, 0000_0010B
out 0x92, al
lgdt [GDT_PTR]
mov eax, cr0
or eax, 0x0000_00001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start
.error_hlt:
hlt
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP
mov eax, KERNEL_BIN_SECTOR
mov ebx, KERNEL_BIN_BASE_ADDR
mov ecx,200
call rd_disk_m_32
; -----------PAGE--------------
call setup_page
sgdt [GDT_PTR]
mov ebx, [GDT_PTR + 2]
or dword [ebx + 0x18 + 4], 0xc000_0000
add dword [GDT_PTR + 2], 0xc000_0000
add esp, 0xc0000000
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
mov eax, cr0
or eax, 0x8000_0000
mov cr0, eax
mov eax, SELECTOR_VIDEO
mov gs, ax
lgdt [GDT_PTR]
mov byte [gs:160], 'V'
jmp SELECTOR_CODE:enter_kernel
; ----------------Enter kernel
enter_kernel:
call kernel_init
mov esp, 0xc009_f000
jmp KERNEL_ENTRY_ADDR
; -------------------setup page table ---------
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir: ; 先把页表空间清0
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
;PDE
.create_pde: ; Create Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000;First PDE's attr
mov ebx, eax ; ready for PTE
; kernel one and usr one
or eax, PG_US_U | PG_RW_W | PG_P
; init the basic attr
mov [PAGE_DIR_TABLE_POS + 0x0], eax
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; kernel
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax; final one points to itself
; create PTE
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi*4], edx
add edx, 4096
inc esi
loop .create_pte
; kernel pte
mov eax, PAGE_DIR_TABLE_POS
mov eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
kernel_init:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
mov ebx,[KERNEL_BIN_BASE_ADDR+28]
add ebx,KERNEL_BIN_BASE_ADDR ;ebx当前位置为程序段表
mov dx,[KERNEL_BIN_BASE_ADDR+42] ;获取程序段表每个条目描述符字节大小
mov cx,[KERNEL_BIN_BASE_ADDR+44] ;一共有几个段
.get_each_segment:
cmp dword [ebx+0],PT_NULL
je .PTNULL ;空即跳转即可 不进行mem_cpy
mov eax,[ebx+8]
cmp eax,0xc0001500
jb .PTNULL
push dword [ebx+16] ;ebx+16在存储的数是filesz 可以翻到Loader刚开始
mov eax,[ebx+4]
add eax,KERNEL_BIN_BASE_ADDR
push eax ;p_offset 在文件中的偏移位置 源位置
push dword [ebx+8] ;目标位置
call mem_cpy
add esp,12 ;把三个参数把栈扔出去 等于恢复栈指针
.PTNULL:
add ebx,edx ;edx是一个描述符字节大小
loop .get_each_segment ;继续进行外层循环
ret
mem_cpy:
cld ;向高地址自动加数字 cld std 向低地址自动移动
push ebp ;保存ebp 因为访问的时候通过ebp 良好的编程习惯保存相关寄存器
mov ebp,esp
push ecx ;外层循环还要用 必须保存 外层eax存储着还有几个段
;分析一下为什么是 8 因为进入的时候又重新push了ebp 所以相对应的都需要+4
;并且进入函数时 还Push了函数返回地址 所以就那么多了
mov edi,[ebp+8] ;目的指针 edi存储的是目的位置 4+4
mov esi,[ebp+12] ;源指针 源位置 8+4
mov ecx,[ebp+16] ;与Movsb好兄弟 互相搭配 12+4
rep movsb ;一个一个字节复制
pop ecx
pop ebp
ret
;------------------------ rd_disk_m_32
rd_disk_m_32:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据
;;;;;;;;;;;;;;;;;;;;;
;1 写入待操作磁盘数
;;;;;;;;;;;;;;;;;;;;;
mov esi,eax ; !!! 备份eax
mov di,cx ; !!! 备份cx
mov dx,0x1F2 ; 0x1F2为Sector Count 端口号 送到dx寄存器中
mov al,cl ; !!! 忘了只能由ax al传递数据
out dx,al ; !!! 这里修改了 原out dx,cl
mov eax,esi ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了
;;;;;;;;;;;;;;;;;;;;;
;2 写入LBA 24位寄存器 确认扇区
;;;;;;;;;;;;;;;;;;;;;
mov cl,0x8 ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中
mov dx,0x1F3 ; LBA low
out dx,al
mov dx,0x1F4 ; LBA mid
shr eax,cl ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15
out dx,al
mov dx,0x1F5
shr eax,cl
out dx,al
;;;;;;;;;;;;;;;;;;;;;
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;;;;;;;;;;;;;;;;;;;;;
; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事
; 把除了最后四位的其他位置设置成0
shr eax,cl
and al,0x0f
or al,0xe0 ;!!! 把第四-七位设置成0111 转换为LBA模式
mov dx,0x1F6 ; 参照硬盘控制器端口表 Device
out dx,al
;;;;;;;;;;;;;;;;;;;;;
;4 向Command写操作 Status和Command一个寄存器
;;;;;;;;;;;;;;;;;;;;;
mov dx,0x1F7 ; Status寄存器端口号
mov ax,0x20 ; 0x20是读命令
out dx,al
;;;;;;;;;;;;;;;;;;;;;
;5 向Status查看是否准备好惹
;;;;;;;;;;;;;;;;;;;;;
;设置不断读取重复 如果不为1则一直循环
.not_ready:
nop ; !!! 空跳转指令 在循环中达到延时目的
in al,dx ; 把寄存器中的信息返还出来
and al,0x88 ; !!! 0100 0100 0x88
cmp al,0x08
jne .not_ready ; !!! jump not equal == 0
;;;;;;;;;;;;;;;;;;;;;
;6 读取数据
;;;;;;;;;;;;;;;;;;;;;
mov ax,di ;把 di 储存的cx 取出来
mov dx,256
mul dx ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dx
mov cx,ax ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环
mov dx,0x1F0
.go_read_loop:
in ax,dx ;两字节dx 一次读两字
mov [ebx],ax
add ebx,2
loop .go_read_loop
ret ;与call 配对返回原来的位置 跳转到call下一条指令
这里是loader.S,我们上机一跑,没有挂,就是成功了!完事!
特权
简介
什么是特权?顾名思义,如果特权级别高的,就能拥有更强大的能力,能访问低特权不能访问的数据。操作系统将特权分为四层,分别为0、1、2、3,0是最高特权级别,3是最低特权级别。特权级别为0一般时由操作系统占有,特权级别1、2是由驱动程序和虚拟机占有,而用户程序一般都在特权级别3的级别下,也就是最低特权级别,能拥有的能力是最低的。
TSS
TSS是一种描述任务状态的数据结构,每一个任务都会有这样的一个结构。为什么要说这个结构,是因为在特权级别转移的时候会用到这个结构,但TSS不仅仅用于特权级别转移,既然它是描述任务状态的,那它必然也包含任务的其它信息。
那什么是任务呢?在没有操作系统内核的情况下,任务就是进程,一段独立的程序运行起来就是一个任务;但在有操作系统的情况下,任务就不仅仅包括用户的进程了,它还包含内核程序,用户进程处于特权级别3,内核程序处于特权级别0,即一个任务是有可能从特权级别3的用户程序跳到特权级别0的内核程序的,反之亦然,这时候是需要借助TSS完成这个过程的。
先来看看TSS的整体结构:
我们关注一下TSS结构偏移4到偏移27的三个栈指针,包括ss和esp。一个任务最多有4个栈,每个特权级别会用到一个独立的栈,但并不是每个任务都会有4个栈,因为不是每个特权级别都会用到。为什么每个特权级别的栈是独立的,而不是共用一个栈呢?这是因为如果共用一个栈,容易导致交叉引用引起混乱,并且一个栈也比较容易导致溢出。
那既然至多有4个栈,那为啥TSS只有三个栈指针信息呢?esp0和ss0、esp1和ss1、esp2和ss2分别代表特权级别0、1、2的栈指针信息,唯独没有特权级别3的栈指针。
要理解这个问题,我们首先得知道为什么要用到TSS的这三个栈指针信息。
分两种情况,一种是从低特权级别转移到高特权级别,一种是由高特权级别转移到低特权级别。
①低特权级别转移到高特权级别时,栈也要由低转到高(省略的是特权级别这几个字,下面也是),所以处理器就会在TSS结构里面找到高特权级别的栈信息,并转移到高特权级别的栈上。这种情况下,只需要知道高特权级别的栈信息,所以作为最低特权级别的3的栈信息是不需要在TSS记录的。
②高特权级别转移到低特权级别时,高转低之前一般都有低转高的过程。这种情况下是不会用到TSS的,这是因为低转高时已经将低的栈指针压进高的栈里面了,所以只需要从高的栈里面获取低的栈指针即可返回到低的栈上。
另一个值得注意的点是,低转高时,由于高特权级别下执行指令时不免会对高特权级别的栈进行操作,此时高的esp就会变更,但是TSS里面的esp并不会跟着变更,如果要变更,也只有操作系统直接对TSS操作才会变更。
当前特权级别
首先,需要明确一点,当前处于什么特权是相对于处理器而言的,例如,我们可以说如果处理器在执行内核代码,处理器就处于特权级别0。
其次,需要知道特权下的访问者是谁,访问者是指令,对于处理器来说,只有指令是被执行的。
接着,再想想,我们怎样才知道当前的特权级别等级(CPL)是什么,在哪里查看,或者说是多少。既然处理器无时无刻不在执行指令,而且指令为访问者,我们用当前执行的指令的特权等级作为当前特权级别等级,但一条指令并不会指定它自己的特权级别,指令肯定是在某个代码段里面,而代码段是有自己的特权级别等级的,所以CPU用当前执行的代码段的特权级别作为当前特权级别等级。
还记不记得段描述符的结构,不记得的话,看一下下面这图:
段描述符的DPL就是描述某个段的特权级别,每个段都有自己的特权级别。结合上面所说的,当前执行代码段的DPL=CPL。
那么CPL需要有个地方保存,这样子处理器才能知道自己是处在什么特权级别。而这个地方就是选择子,准确来说的选择子中的RPL部分。
选择子在哪里存储的?段寄存器。代码段寄存器是CS,所以CS中的RPL=CPL,我们写作CS.RPL=CPL,综上,当前代码段DPL=CPL=CS.RPL。
一般原则
一般而言,低特权级别是不能访问高特权级别的资源,但高特权级别能访问低特权级别的资源。现在看来只有段描述符是有特权级别的,当然后面也有像门结构描述符也有DPL,下面只谈论“段”。
所以我们分别讨论一下代码段和数据段(这里除代码段以外都称为数据段):
这里以三个例子来讨论一般规则。
①如果受访者是数据段,且特权级别为2,处于哪些特权等级的访问者可以访问?
遵循一般原则,只有高于或等于受访者的特权等级才能访问。所以只有处于特权级别0、1、2的访问才能对数据段进行访问。即数值上访问者的特权级别小于等于受访者的特权级别。
②如果受访者是非一致性代码段,且特权级别为2,处于哪些特权级别的访问者可以访问?
首先比受访者特权级别低的访问者肯定是不能直接访问了,这得遵循一般原则。所以我们不能通过jmp或call等指令跳到或调用特权等级较高的代码。
然后比受访者特权级别高的访问者可不可以访问呢?就是访问者能访问比自己特权级别低的代码段吗?不能,书上的解释是:低特权等级能做的事,高特权等级也能做,所以高特权等级没有必要跳到低特权等级里执行,访问者没有必要自降等级去做它本来能做的事情。凡是都有例外,唯一一种从高特权等级跳到低特权等级的情况是:从中断服务程序返回到用户态。
综上只有平级的访问者才能访问非一致性代码段,即特权级别为2才能访问。
③如果受访者是一致性代码段,且特权级别为2,处于哪些特权级别的访问者可以访问?
一致性代码段能让比自己特权级别低的访问者访问,所以特权级别3,2的访问者可以访问。注意访问者转移到一致性代码段后,转移后的特权级别还是和转移前一样,这是一致性代码段得到特点,一致性代码段不以自己的特权级别等级为主。
特权级别检查发生在访问受访者的一瞬间,之后访问这个受访者段里的内容都不需要再检查了。
门结构
上面说到低特权级别不能访问高特权级别,但总有一些情况是需要访问到比自己高的特权级别等级,比如需要请求操作系统内核从外设里获取数据。处理器提供了一种”门结构”的机制,只有通过门结构才能让处理器转移到高特权级别等级。门结构是在内存里面存储的,所以也是由操作系统设置的,之所以说是处理器的机制,是因为处理器就是这样设计的,操作系统需要遵循处理器的这种机制,来实现门结构的效果,说白了,对于处理器来说,操作系统只是它的应用。
门结构在操作系统有四种:任务门、调用门、中断门、陷阱门。它们像描述段的段描述符那样,门结构都有自己的描述符。那么它们存储在那里呢?任务门描述符存储在GDT,LDT,IDT,IDT是中断描述符表,之后会说。GDT,LDT是全局描述符表和局部描述符表。调用门描述符存储在GDT,LDT。中断门描述符和陷阱们描述符都存储在IDT里面。任务门可以通过call和jmp指令调用,用任务门选择子作为参数。调用门可以通过call和jmp指令调用,用调用门选择子作为参数。中断门通过int指令发出中断。陷阱门通过int3指令发出中断,一般是在编译器调试时用,不做过多关注。
为了下面更好的讲述RPL,这里讲一下调用门,调用门是指定一段程序入口地址的门结构,调用门的结构如下图所示:
调用门的结构和段描述符的结构类似,都有P、DPL、S、TYPE,因为调用门需要描述一段程序,所以需要用到调用例程的选择子和偏移量,还有参数个数。
调用门的执行流程
我们说到调用门是描述一段例程的结构,可以让低特权级别转移到高特权级别等级,那么具体的使用流程是怎么样的呢?
具体流程如下:
①通过call 调用门选择子开始调用。
②根据选择子的索引在GDT或者LDT里面找到调用门描述符,从调用门描述符里得到了例程所在的段描述符选择子和段内偏移地址。
③再根据段描述符选择子在GDT或LDT找到该段的基址。
④将段基址和段内偏移地址相加得到最终的例程 入口地址。
⑤跳到该入口地址执行。
调用门的栈的变化
从低特权级别等级转移到高特权级别等级,还涉及栈的转移,上面说了,每个特权级别都有独立的栈,所以栈也要跟着转移,那么这个过程是怎样的呢?
流程:
①call调用门选择子之前先压入参数。
②判断例程所在的段的DPL是否和CPL相同,不同则发生特权级别转移。
③若发生特权级别转移,在TSS查找转移后的特权级别的栈指针(包括ss和esp)。处理器临时找个地方保存当前特权级别的栈指针,再转移到新的特权级别的栈。
④在新栈中压入旧的栈指针(③所保存的),再根据调用门中的参数个数,复制低特权级别栈中的参数到新栈。
⑤再压入cs和eip。
若不发生特权级别转移,直接由②跳到⑤。
那么执行完之后,如何返回到低特权级别时的状态呢?通过retf指令将ss、esp、cs、ip弹到寄存器里面,从而恢复现场。另外retf的用法是 retf+参数个数,这样子retf指令才知道中间应该跳过多少个参数。
调用门的特权检查
这里结合使用调用门的流程来讲讲其中的特权级别检查:
①通过call 调用门选择子开始调用。
②根据选择子的索引在GDT或者LDT里面找到调用门描述符,判断CPL数值上是否小于等于调用门描述符的DPL,设为DPL_GATE,即数值上CPL<=DPL_GATE。可以看出调用门描述符的DPL是第一道门槛,起码访问者特权级别要高于受访者“门描述符”的特权级别,才能通过特权级别检查,这遵循一般原则。
③从调用门描述符里得到了例程所在的段描述符选择子和段内偏移地址。
④再根据段描述符选择子在GDT或LDT找到该段描述符,通过这个段描述符得到该段的特权级别等级DPL_CODE,由于门结构的特点,第二个约束就是数值上CPL>=DPL_CODE,就是说转移后的特权等级要高于转移前的特权等级。
④将段描述符的段基址和调用门的段内偏移地址相加得到最终的例程 入口地址。
⑤跳到该入口地址执行。
综上数值上DPL_CODE<=CPL<=DPL_GATE。
RPL
根据上面谈及的一般原则,当访问者访问受访者时,访问者的特权级别数值上要小于等于受访者的特权级别DPL,所以CPL<=DPL。
这里将访问者看作当前执行的指令,所以访问者的特权级别等级时CPL。
如果我们单纯靠CPL和DPL来进行特权级别检查的话会有什么问题呢?
考虑这样一种情况,用户程序想获取外设里的数据,比如通过网卡获取的网络数据,用户程序不能直接对外设进行访问,所以只能通过操作系统内核来获取。寻求内核的帮助需要用到门结构,因为涉及到特权级别的转移,这里假设用户是通过调用门转移到内核。用户程序向内核提交了一个缓冲区的选择子,缓冲区的段内偏移地址,写入数据大小三个参数。
用户提交参数后,并且通过了DPL_CODE<=CPL<=DPL_GATE的校验,转移到内核去执行调用门指向的例程,这时候CPL为0,处理器拥有最高的权限,可以对内存里的任何地方进行读写,用户提交缓冲区的选择子指向用户态下的数据段,此数据段DPL为3,由于CPL<=DPL,所以处理器能够通过特权校验并对缓冲区写入。
那么再想想,如果用户通过某种途径知道了内核环境下的数据段,并且提交了一个指向内核缓冲区的选择子,试图破坏内核环境,结果会怎么样?
由于此时处理器处于最高特权级CPL=0,内核缓冲区的DPL=0,即CPL<=DPL校验通过,所以处理器也是能对内核缓冲区进行读写操作,所以可能造成了破环内核环境的后果,这就很可怕了。
出现这问题的原因是显然易见的。内核并不知道请求资源者的真正的能力是多少,当通过调用门转移到内核的时候,特权级别已经是最高了,内核成为了请求资源者的代理,能够对任何缓冲区进行读写,对于缓冲区来说,它认为访问者是内核,特权检查必定能通过,所以缓冲区肯定会放任内核去对自己进行操作。但实际上缓冲区的真正请求者是用户程序而不是内核。所以我们有必要让受访者知道真正请求资源的是谁。
RPL出现了。由于需要通过选择子去索引段描述符,RPL放在选择子再合适不过。
RPL代表选择子请求资源的能力,还记得选择子的结构吗,RPL就是在这里用到的。
一般原则补充
其实特权级别校验不单单有DPL和CPL的参与,还有RPL的参与,这里对上面说到的一般原则进行补充。
①如果受访者是数据段,特权级别需要满足:数值上数据段的DPL大于等于CPL和数据段选择子的RPL,即CPL<=DPL且RPL<=DPL。
②如果受访者是非一致性代码段,特权级别需要满足:数值上代码段的DPL等于CPL等于代码段选择子的RPL,即CPL=DPL=RPL。
③如果受访者是一致性代码段你,特权级别需要满足:数值上代码段的DPL小于等于CPL和RPL,即CPL>=DPL且RPL>=DPL。
调用门特权检查补充
RPL同样需要在使用调用门时参与校验。
在访问调用门描述符时,需要指定调用门选择子,选择子的RPL要数值上小于等于门描述符的DPL_GATE,即RPL<=DPL_GATE,同样需要CPL<=DPL_GATE,之前讨论的。
在调用调用门指向的例程之前,门描述符选择子的RPL不需要参与校验,因为它只是用来索引门描述符,所以只需CPL>=DPL_CODE。
RPL防止越权读写资源
再回到刚才那个问题,操作系统是怎么利用RPL防止用户破坏内核环境的或者偷偷获取到内核数据的。用户通过调用门转移到内核服务程序执行,内核服务程序会将用户提交的选择子里的RPL变更为用户进程的CPL,这就防止了这个问题的出现。
假设用户伪造了一个RPL为0的选择子,并且这个选择子指向的时内核数据段。在执行内核服务程序时,这个选择子RPL被改成了3,此时CPL=0,RPL指向的数据段DPL=0,通过上面的讨论,得出虽然CPL<=DPL,但是RPL<=DPL并不满足,所以特权校验并不通过,自然就不会写进内核缓冲区了。
几种情况
在这里再讨论三种情况。
①不通过调用门,描述一下处于特权级别为3的用户程序向当前特权级别的缓冲区里写数据时,DPL、RPL、CPL的校验过程:
用户程序执行时CPL=3,缓冲区选择子RPL=3,缓冲区数据段DPL=3,CPL=RPL=DPL,校验通过。
②不通过调用门,描述一下处于特权级别3的用户程序尝试写入特权级别0的缓冲区中,DPL、RPL、CPL的校验过程:
用户程序执行时CPL=3,缓冲去选择子RPL=3,缓冲区数据段DPL=0,CPL>DPL、RPL>DPL,都不通过校验,特权检查失败。
③通过调用门,用户程序想获取外设里的数据,用户程序向内核提交了缓冲区的选择子,缓冲区的段内偏移地址,写入数据大小三个参数,DPL、RPL、CPL的校验过程:
这情况是对上面讨论的总结。
首先用户程序当前CPL=3,提交的缓冲区选择子RPL=3,指向的缓冲区所在数据段的DPL=3。
用户程序指定了一个调用门选择子,其RPL=3,调用门描述符的DPL_GATE=3(如果这里为2或2以下就通过不了校验了,所以一定为3),RPL<=DPL_GATE且CPL<=DPL_GATE,通过调用门描述符的特权检查。
再检查是否能转移到内核服务程序中,内核服务程序的DPL=0,所以CPL=3>=DPL,通过校验,跳到内核服务程序执行。
内核服务程序获取外设数据后需要对缓冲区进行写入,此时CPL=0,缓冲区选择子RPL=3,缓冲区所在数据段DPL=3,所以CPL<=DPL且RPL<=DPL,通过校验,能对缓冲区进行写入。
如果用户伪造了一个缓冲区选择子RPL=0且指向的缓冲区所在的数据段DPL=0,会怎么样。
内核服务程序会将这个选择子的RPL改为3,虽然CPL<=DPL但RPL>DPL,所以校验不通过,不能对缓冲区进行写入,入侵失败。
访问外设
以下为补充内容,体现特权在IO和指令的限制。
特权指令
特权级不仅仅体现在对数据和代码的访问,而且体现在对指令的限制。
有一些指令只能在特权级别为0时才能执行,这类指令叫特权指令,如lgdt、lidt、ltr、popf等,这些指令涉及到对内存的管理、中断等等,不应该由用户程序操作,处理器也只信任操作系统,所以放在特权级别为0下运行也十分合理。
还有一些指令是需要受到IOPL限制,这些指令是IO读写指令,如in、out、cli、sti,也被称为IO敏感指令。只有在当前特权级别大于等于IOPL才能执行IO指令。
IOPL
还记得eflags的结构吗:
IOPL是IO特权级,是用来限制访问IO指令的最低特权级别。每个任务都有eflags寄存器,所以每个任务都会有自己的IOPL。
除此之外IOPL还可以用来决定任务是否能够访问所有外设端口。当数值上CPL<=IOPL时,任务能够访问所有外设端口,那么当CPL>IOPL时是不是就不能访问外设端口呢?不是,处理器允许部分IO端口被任务访问,哪些端口允许访问是IO位图决定的。所以IOPL有点像防火墙,首先禁止所有访问,再打开想开放的端口。
像处于特权级别0的操作系统和处于特权级别1的驱动程序就能对所有端口进行访问。驱动程序就是通过in、out对硬件直接访问的程序。
那么像任务处于特权级别2和3的话,虽然操作系统不允许开放所有端口,但还是允许部分端口对任务开放,只要通过IO位图做好限制即可。
为什么处理器允许特权级别1、2、3能够对IO进行直接操作呢,原因是如果所有IO操作都通过内核的话,上下文切换的开销会有点大,这样做主要是为了提速。
IO位图
什么是位图,位图就是用位映射到某些资源上,IO位图就是由一个位映射到一个端口,如果这个位是1,则端口关闭,如果是0,代表端口打开。
IO位图存储在TSS里面:
TSS的前104个字节的结构是固定的,不固定的是IO位图的起始位置。怎么找到IO位图的起始位置呢?TSS中偏移102字节的地方就是保存着IO位图在TSS的偏移地址,占2个字节。IO位图的偏移地址取值范围是104~TSS段界限limit之间,如果偏移地址不再这个范围,即大于等于TSS段界限limit,则代表没有IO位图。IO位图起始一个位代表第0个端口,以此类推。Intel处理器最大支持65536个端口,所以位图大小65536/8=8192个字节。IO位图如果存在且映射所有端口,TSS的大小为IO位图偏移地址+8192+1,1代表TSS最后的0xff,下面会解释,如果IO位图不存在,TSS大小为104。
那么为什么最后有个0xFF呢?
每个端口只能读写一个字节的数据,但IO指令是可以都多个端口进行读写,如果对一个端口进行连续读写,那相当于以该端口号为起始的多个端口一并读写进来,如in ax, 0x234,in可以读取16位端口数据,即两个字节,假设0x234是16位端口,in ax,0x234就相当于in al,0x234和in ah, 0x235两个指令。
所以一个指令是有可能会读取多个端口的,处理器会在IO位图检查这些端口是否都打开了,连续的bit会有可能跨字节,比如0x234端口在一个字节的最后一位,0x235端口在一个字节的首位,处理器需要将两个字节都都进来。大部分情况跨字节检查不会有问题,但是如果这发生在IO位图的最后一个字节的话,读取两个字节,第二个字节就会越界。
因此0xff会有两个作用:
①处理器允许IO位图不映射所有端口,但要保证最后一个字节位0xff。假设映射到0-23的端口,我读取23端口,且读取两个字节的数据,即会读取两个端口23、24,那么0xff就表明24端口关闭,这样既不会越界,也不会不合理,因为没有映射到的端口就是关闭了;0xff即代表24-31号端口关闭,也防止越界,当然防止越界才是真正的目的,前者只是合理化的结果。
②如果IO位图映射所有端口,那么0xff就不代表任何端口,它就是作为位图的边界标记,防止越界。
函数调用与操作显卡
函数的调用约定
我们下面看看函数的调用约定问题。我们现在知道,函数调用在汇编层面就是先设置好寄存器的参数值之后压栈后调用函数,那么,压栈的过程中是怎么压栈的,需要我们思考。方法很多,这就延申出来下表了:
①:cdecl
参数从右往左依次压入栈中,由调用方负责弹出栈上的参数。这个约定与C语言的原型声明相一致,并且多用于可变参数函数的定义,比如 printf 函数。
②:stdcall
参数从右往左依次压入栈中,由被调用函数负责清理栈上的参数。这个约定多用于 Windows 操作系统中的 WinAPI 函数定义和调用。stdcall 可以减少程序的大小并且使得代码更容易优化。
③:fastcall
该约定可以支持寄存器传递参数来加快调用速度,通常前两个参数会被放置到寄存器 ecx 和 edx 中,其他参数依然按顺序入栈。由于寄存器有限,fastcall 只适用于参数少于三个的函数。
C语言遵循的是cdecl。
C与汇编混合编程
我们先看库函数和系统调用,都知道库函数是对系统调用的一个封装。实际上,我们是预先指定好系统中断需要的值,放在寄存器中,然后发起中断(0x80)。需要的值就有需要请求的系统调用的中断号,比如说可以看看我们write函数的系统调用
当然这是64位的,我们书写32位程序使用的是:
我们这样就可以看看如何实现汇编使用系统调用了:
section .data
STR_C_LIB: db "c lib says: hello world!", 0xa
STR_C_LIB_LEN equ $ - STR_C_LIB
STR_SYSCALL: db "syscall says: hello world!", 0xa
STR_SYSCALL_LEN equ $ - STR_SYSCALL
section .text
global _start
_start:
; ------- C Like Caller -------------
push STR_C_LIB_LEN
push STR_C_LIB
push 1
call simu_write
add esp, 12
; use syscall directly
mov eax, 4 ; write's syscall num is 4
mov ebx, 1
mov ecx, STR_SYSCALL
mov edx, STR_SYSCALL_LEN
int 0x80
;;;-----Exit then
mov eax, 1; 0x80 of 1 is exit
int 0x80
simu_write:
push ebp
mov ebp, esp
mov eax, 4
mov ebx, [ebp + 8]
mov ecx, [ebp + 12]
mov edx, [ebp + 16]
int 0x80
pop ebp
ret
两种方式,第一种就是C-like的参数压栈后调用,第二种就是直接干系统调用。
nasm -f elf system_call.S -o system_call.o
ld -m elf_i386 -o system_call.bin system_call.o
显卡的端口控制
显卡寄存器:
显卡端口非常多,但是计算机系统提供的寄存器寻址范围很少,只有0~65535个
所以显卡硬件也使用数据结构的方式来提供寄存器的访问,如图,上面4个寄存器被分为了2组,Address 寄存器和 Data 寄存器,前者存储寄存器组的索引,后者是该索引对应的寄存器
这里主要用到的寄存器组是CRT Controller Register,这里的端口地址取决于 Miscellaneous Output Register 寄存器中的 Input/Output Address Select 字段。
默认情况下, Miscellaneous Output Register寄存器的值为0x67, 其他字段不管, 咱们只关注这最重要的I/OAS位, 其值为1。 也就是说:
- CRT controller寄存器组的Address Register的端口地址为0x3D4,Data Register的端口地址0x3D5。
- Input Status # l Register寄存器的端口地址被设置为0x3DA。
- Feature Control register寄存器的写端口是0x3DA。
由于这里涉及到的显卡操作只用到了CRT Controller Registers分组中的寄存器,其他的就不管了:
这里的 0x0e
和0x0f
存储的是光标位置的高、低8位。
我们熟知自己要干啥之后,就可以开始写代码:
实现打印函数
首先定义标准类型:
// IN ./lib/stdint.h
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t
typedef signed short int int16_t
typedef signed int int32_t
typedef signed long long int64_t
typedef unsigned char uint8_t
typedef unsigned short int uint16_t
typedef unsigned int uint32_t
typedef unsigned long long uint64_t
#endif
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
section .data
put_int_buffer dq 0
[bits 32]
section .text
;--------------------put_char------------------
; put char into the screen
; --------------------------------------------
global put_char
put_char:
pushad
mov ax, SELECTOR_VIDEO
mov gs, ax
; --- get cur cursor pos LOW--
mov dx, 0x03d4 # 端口在
mov al, 0x0e # 看CRT表得到
out dx, al
mov dx, 0x03d5
in al, dx
mov ah, al
; --- get cur cursor pos HIGH--
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
mov bx, ax
mov ecx, [esp + 36]; pushad add 32 bits and then the ret addr is 36
cmp cl, 0xd
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8
jz .is_backspace
jmp .put_other
.is_backspace:
dec bx ; 回退一位
shl bx, 1; *2 ;
mov byte [gs:bx], 0x20 ;space
inc bx
mov byte [gs:bx], 0x07 ;space
shr bx, 1; /2
jmp .set_cursor
.put_other:
shl bx, 1
mov [gs:bx], cl ;写进字符本身
inc bx
mov byte [gs:bx], 0x07 ;设置属性
shr bx, 1; /2
inc bx
cmp bx, 2000 ; check if we need to set the cursor
jl .set_cursor
.is_line_feed:
.is_carriage_return:
xor dx, dx
mov ax, bx
mov si, 80
div si
sub bx, dx
.is_carriage_return_end:
add bx, 80
cmp bx, 2000 ;比较是否换行
.is_line_feed_end:
jl .set_cursor
.roll_screen:
cld
mov ecx, 960
mov esi, 0xc00b80a0
mov edi, 0xc00b8000
rep movsd
mov ebx, 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720
add ebx, 2
loop .cls
mov ebx, 1920
.set_cursor:
; HIGH 8
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
; LOW 8
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
; ------------------------------
; Put String into the screen
; ------------------------------
global put_str
put_str:
push ebx
push ecx
xor ecx, ecx
mov ebx, [esp + 12]
.goon:
mov cl, [ebx]
cmp cl, 0
jz .str_over
push ecx
call put_char
add esp, 4
inc ebx
jmp .goon
.str_over:
pop ecx
pop ebx
ret
global put_int
; ------------------------------
; Put uint8 into the screen
; ------------------------------
put_int:
pushad
mov ebp, esp
mov eax, [ebp + 4*9]
mov edx, eax
mov edi, 7
mov ecx, 8
mov ebx, put_int_buffer
.16based_4bits:
and edx, 0x0000_000F
cmp edx, 9
jg .is_A2F
add edx, '0'
jmp .store
.is_A2F:
sub edx, 10
add edx, 'A'
.store:
mov [ebx, edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
.ready_to_print:
inc edi
.skip_prefix_0:
cmp edi, 8
je .full0
.go_on_skip:
mov cl, [put_int_buffer + edi]
inc edi
cmp cl, '0'
je .skip_prefix_0
dec edi
jmp .put_each_num
.full0:
mov cl, '0'
.put_each_num:
push ecx
call put_char
add esp, 4
inc edi
mov cl, [put_int_buffer + edi]
cmp edi, 8
jl .put_each_num
popad
ret
建一个头文件:
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include"stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
void put_int(uint32_t num);
#endif
我们在main函数下写道:
#include"print.h"
int main(void)
{
put_str("\n\n\n\n\n\n\n");
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
put_char('\n');
put_str("Hello from CCSTDC_Kernel!\n");
put_int(0x12345678);
put_char('\n');
while(1);
}
├── a.img
├── AutoRun.sh
├── bashsrc.ab
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.bin
│ ├── loader.S
│ └── mbr.bin
├── Compile.sh
├── kernel
│ ├── kernel.bin
│ ├── main.c
│ └── main.o
├── lib
│ ├── kernel
│ │ ├── myprint.S
│ │ ├── print.h
│ │ └── print.o
│ └── stdint.h
└── Load.sh
这是我们现在的结构图
编译:
nasm -f elf ./lib/kernel/myprint.S -o ./lib/kernel/print.o
gcc -m32 -I ./lib/kernel/ -c -o ./kernel/main.o ./kernel/main.c
ld -m elf_i386 ./kernel/main.o ./lib/kernel/print.o -Ttext 0xc0001500 -e main -o ./kernel/kernel.bin
加载:
dd if=./boot/mbr.bin of=a.img count=1 bs=512 seek=0 conv=notrunc
dd if=./boot/loader.bin of=a.img count=4 bs=512 seek=2 conv=notrunc
dd if=./kernel/kernel.bin of=a.img count=200 bs=512 seek=9 conv=notrunc
bochs -f bashsrc.ab
执行结果就是这样。
内联汇编
1.基本内联汇编的格式
asm [volatile](" code")
采用;作为代码的分隔符,如果·一行写不下采用\继续下一行。
2.扩展内联汇编
例子:扩展内联汇编
#include<stdio.h>
int main()
{
int add_1=1,add_2=2;
int answer=0;
asm("addl %%ebx,%%eax":"=a"(answer):"a"(add_1),"b"(add_2):);
printf("%d",answer);
return 0;
}
这里可以看到对于寄存器采用了两个%%来表示。此外扩展内联汇编还提供了占位符来方便表示。
中断IDT
中断
我们这从来看看怎么玩中断表。可以理解为:
操作系统完成初始化工作之后就开始hold住,靠中断来为上层服务。
中断的分类
分为外部中断和内部中断。
外部中断
从外面来的中断,一般是一个硬件给出的中断,比如说我们将要使用的8259A,他会给CPU一个信号——有中断了,处理一下。这个中断发出的引脚是INTR,表示的是非致命的中断,而从NMI引脚出来的信号表示的是致命错误,这些中断往往不可屏蔽,因为它表明系统要挂了。
内部中断
也就是我们系统软件发起的中断,int 0x啥的就是内部终端,我们就是让系统先保护现场,然后处理中断,然后恢复现场。就是这样的简单。
可以看到,有些中断还是会压入错误码,这就需要我们后续的处理中需要整一下活。
IDT
中断描述符表
什么是中断描述符表?
中断描述符表是保护模式下用于存储中断处理程序的数据结构。CPU在接收到中断时,会根据中断向量在中断描述符表中检索对应的描述符。
中断描述符表中的描述符有哪些类型?
中断描述表中的主要包含以下类型:
- 任务门描述符
- 中断门描述符
- 陷阱门描述符
- 调用门描述符
任务门描述符结构如下:
任务门需要和任务状态段(TSS)配合使用,这是Intel处理器在硬件一级提供的任务切换机制。任务门可以存在于全局描述符GDT、局部描述符表LDT以及中断描述符表IDT中。
中断门描述符结构如下:
中断门包含中断处理程序所在的段选择子和段内偏移地址,当通过此方式进入中断后,标志寄存器eflags中的IF位自动置0,表示把中断关闭,避免中断嵌套。中断们只存在于中断描述符表IDT。
陷阱门描述符结构如下:
通过陷阱门进入中断,标志寄存器eflags的IF位不会自动置0,陷阱门只允许存在于IDT中。
调用门描述符结构如下:
调用门是用户进程用来进入0特权级的方式,其DPL为3。调用门可以在GDT和IDT中存在的,只能使用call和jmp指令调用。
中断描述符表存储的位置不固定。
中断描述符寄存器
如何找到中断描述符表?
CPU内部有个中断描述符寄存器IDTR,该寄存器的结构图如下图:
第0~15位是表界限,即IDT减1,可容纳8192个中段描述符;第16~47位时IDT的基地址。
通过lidt 48位内存数据指令便可将中断描述符表的信息加载到IDTR寄存器中。
中断细节
中断处理过程包含哪两部分?
CPU外部:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送给CPU
CPU内部:CPU执行该中断向量号的中断处理程序
如何在中断描述表中定位中断描述符?
每个中断描述符号占用8字节,所以使用中断向量号与8相乘,相当于得到偏移地址,然后从IDTR寄存器中取出中断描述符表的基址,将两个地址相加,便能定位到中断描述符的地址。
中断门处理器如何进行特权级检查?
对于内部中断来说,要求检查当前特权级CPL和中断门描述符DPL及门描述符对应的的代码段的DPL,在数值上满足以下关系:
目标代码段DPL < 当前特权级CPL < 门描述符DPL
对于外部中断,检查当前特权级CPL和目标代码段的DPL,在数值上满足以下关系:
目标代码段DPL < 当前特权级CPL
如何执行中断处理程序?
将门描述符中的目标代码段描述符选择子加载到代码段寄存器CS中,把门描述中中断处理程序的偏移地址加载到EIP,便开始执行中断程序。
如何确定使用新栈还是旧栈?
程序的运行需要栈,由于不同的特权级需要使用不同的栈,因此当涉及到特权级变化后,便需要开启新栈。
使用新栈时需要压入哪些信息?
- 旧栈环境下SS和ESP的值
- 标志寄存器EFLAGS的值
- 备份CS和EIP的值
- ERROR_CODE中断错误码
特权级不发生变化时,不需要压入旧栈环境下SS和ESP的值。
中断处理程序执行完成以后执行返回指令时,CPU会将上述值从栈中弹出,但是ERROR_CODE需要我们手动弹出。
为什么要保存CS和EIP的值?
当我们中断处理程序在返回时,检查备份的CS选择子,根据其RPL和DPL做特权级检查,如果通过,则需要更新寄存器CS和EIP,这样才可以恢复到中断之前的代码段。
该特权级检查的结果还决定了是否需要恢复SS和ESP的值,如果特权级没有发生变化,不需要恢复,因为中断处理程序和我们的应用程序是同一个栈,否则便需要恢复栈。
中断错误码
中断错误码只是用来指明中断发生在哪个段上,结构如下图:
EXT用来指明中断源来自处理器内部还是外部,1代表中断源是不可屏蔽中断或外部设备。
IDT表示选择子是否指向中断描述表,1表示执行IDT,否则指向GDT或LDT。
当IDT为1:TI为0表示从GDT中检索描述符,为1表示从LDT检索描述符。
选择子高13位就是用来索引描述符用的下标。
8259A详解
8259A的外部引线
采用28脚双列直插式封装。
- D0~D7:双向数据线,与系统的数据总线相连。编程时控制字,命令字由此写入;中断响应时,中断向量码由此送给CPU。
- /WR,/RD为读写控制信号,与系统总线的/IOW,/IOR相连接。
- /CS为片选信号,当/CS为低电平时,8259A被选中,CPU才能对它进行读写操作。此引脚连到系统的I/O译码器输出,由此确定8259A在系统I/O地址空间的基地址。
- A0是8259A内部寄存器的选择信号,它与/CS,/WR,/RD信号相配合,对不同的内部寄存器进行读写。使用中,通常接地址总线的某一位,例如A1,A0等。
- INT为8259A的中断请求输出信号,可以直接接到CPU的INTR输入端。
- /INTA为中断响应输入信号,在中断响应过程中CPU的中断信号由此端进入8259A。
- CAS0~CAS2为级联控制线。当多片8259A级联工作时,其中一片为主控芯片,其它均为从属芯片。对于主片的8259A,其CAS0到CAS2为输出,对于各从属芯片的CAS0到CAS2为输入。主片的CAS0到CAS2与从片相同引脚对应相连。当从片提出中断请求时,主片通过CAS0到CAS2送出相应的编码给从片,使得从片的中断被允许。
- /SP/EN为双功能引线,工作在缓冲模式时,为输出,控制缓冲器的传送方向。CPU–>8259A为高电平,数据从8259A–>CPU为低电平。工作在非缓冲方式时,为输入,指定8259A是否为主片从片。/SP=1为主片,/SP=0为从片。系统中只有一片8259A时,接高电平。
- IR0~IR7为中断请求输入信号,与外设中断请求线相连,上升沿或高电(可编程设定)时表示有中断请求到达。
8259A的内部结构
8259A的内部结构如图所示:
由中断请求寄存器IRR,中断服务寄存器ISR。中断屏蔽寄存器IMR,中断判优电路,数据总线缓冲器,读/写电路,控制逻辑和级联缓冲/比较器组成。
- 中断请求寄存器IRR:
保存从IR0-IR7来的中断请求信号。某一位为1表示相应引脚上有中断请求信号。该中断请求信号至少应保持到该请求被响应为止。中断响应后,该IR输入线上的请求信号应撤销,否则,在中断处理完结后,该IR线上的高电平可能会引起又一次中断服务。 - 中断服务寄存器ISR:
ISR用于保存所有正在服务的中断源。是8位的寄存器(IS0-IS7分别对应IR0-IR7)。在中断响应时,判优电路。把发出中断请求的中断源中优先级最高的中断源所对应的位置1,表示该中断请求正在处理中,ISR的某一位ISi置1可阻止与它同级及更低优先级的请求被响应,但不阻止比它优先级更高的中断请求被响应,即允许中断嵌套,所以,ISR中可能不只有一位被置1,当8259A收到中断结束(EOI)命令时,ISR相应位会被清除。对自动EOI操作,ISR寄存器中刚被置1的位在中断相应结束时自动复位。 - 中断屏蔽寄存器IMR:
IMR用于存放中断屏蔽字,它的每一位分别与IR7~IR0相对应。其中为1的位所对应的中断请求输入将被屏蔽,为0的位所对应的中断请求输入不受影响。 - 中断判优电路
中断判优电路监测从IRR,ISR,和IMR来的输入,并确定是否应向CPU发出中断请求。在中断响应时,它要确定ISR寄存器哪一位应置1,并将相应的中断类型码送给CPU。在EOI命令时,它要决定ISR寄存器哪一位应复位。
8259A的工作过程
当系统通电后,首先应对8259A初始化,也就是由CPU执行一段程序,向8259A写入若干控制字,指定它的工作方式。初始化完成后,8259A就准备好了,随时可以接受外设的中断请求信号,当外设发出请求后,8259A对外部中断请求的处理过程如下:
\5. 哪条IR线上的数据有效,IRR相应位置就置1。
\6. 至少有一条中断没有被屏蔽,都屏蔽了中断输入无意义,8259A由INT引脚向CPU发INTR信号。
\7. 恰好CPU处于开中断状态,完成当前指令后,用/INTR信号响应,告诉8259A收到了中断请求。
\8. 8259A接收到CPU发出的第一个/INTA脉冲后,把最高优先权的ISR置1,并使相应的IRR复位。
\9. 第二个中断响应周期中,CPU再输出一个/INTA脉冲,这时8259A就把刚才选定的中断源所对应的8位中断类型码放到数据总线上,CPU读取该中断类型码并乘以4,就是中断服务子程序在中断向量表中的入口地址。找到执行。
\10. 若8259A工作在自动中断结束AEOI方式,在第二个/INTA脉冲结束时,把中断源所对应的ISR中相应位复位。对于非自动中断结束方式,则由CPU在中断服务子程序结束时向8259A写入EOI命令,才能使ISR中的相应位复位。
8259A的工作方式
- 中断优先方式与中断嵌套
中断优先方式:
固定优先级:各中断请求中断优先级固定不变,8259A加电后就处于这种方式,默认IR0优先级最高,IR7优先级最低,可改变默认值。
循环优先级: 实际应用中,许多中断源的优先级别是一样的,如果固定优先级,低级别的中断源可能总得不到服务。使这些中断源轮流处于最高优先级。一个中断源得到中断服务后,优先级自动降到最低,原来它的下一级中断源变为最高级,依次排列。
中断嵌套:
允许更高优先级的中断打断当前的中断处理过程。
普通全嵌套方式: 8259A最常用的工作方式,CPU响应中断时,8259A把优先权最高的中断源在ISR中置1,把它的中断类型码送到数据总线,在此中断服务子程序结束之前,比它低的或同级的被屏蔽,只有优先权高的才被允许。
特殊全嵌套方式:与普通全嵌套的区别是:同级的中断请求8259A也会响应。从而实现中断处理过程能被同级别中断请求打断。一般用在8259A级联的系统中。在这种情况下,只有主片8259A允许编程为特殊全嵌套模式。这时,来自某一从片的中断请求正在处理时,主片除了对本片上优先级较高的引脚上的中断请求处理开放,同时对于从片较高优先级的请求也会开放。 这种模式下,中断结束时,通过软件检查是否为从片唯一中断,向从片发一正常结束命令EOI,然后读ISR内容。如果为0表示只有一个中断服务,这时再向主片发一个EOI命令,结束所有中断;否则,说明从片上有两个以上的中断,则不应再向主片发EOI命令,等待该从片中断服务全部结束后,再发EOI给主片。
2.中断结束处理方式
不管用哪种优先权方式工作,当一个中断请求IRi得到响应时,8259A都会将ISR中相应ISi置1.中断服务结束时清零。否则会引起总线竞争(我认为)。这个复位动作就是中断结束处理。这里的中断结束是指8259A结束中断的处理,而不是CPU结束执行中断服务子程序。
自动中断结束方式(AEOI): 第二个中断响应周期/INTA信号的后沿,8259A自动把ISR中的对应位消除,CPU在执行中断服务,ISR中没有保留正在执行中断服务的状态。只能用于没有中断嵌套的情况。
正常中断结束方式(EOI):配合全嵌套优先权方式使用,CPU向8259A发出EOI命令,8259A就会把ISR中置1的最高位复位。因为在全嵌套方式中ISR中置1的位对应的是最后一次被响应和处理的中断。复位操作相当于结束了当前正在处理的中断。
特殊中断结束方式(SEOI): 在程序中发一条特殊的中断结束命令,这个命令指出了要擦除ISR中的哪一位。
不管是EOI还是SEOI,在一个中断服务子程序结束时,对于级联使用的8259A都必须发两次中断结束命令,一次给主片,一次给从片。
3.屏蔽中断源的方式:
8259A的8个中断请求都可根据需要单独屏蔽,(IMR置0或1)
普通屏蔽方式: 很简单,IMR置1,对应IRi被屏蔽,中断请求不能从8259A送到CPU,置0,反之。
特殊屏蔽方式: (SMM)为了动态改变系统优先权结构,在IRi的处理过程中,希望除了IRi以外的所有中断请求均可被响应,首先设置特殊屏蔽方式,再编程将IRi屏蔽掉,使得这一位复位。提供了允许较低优先级中断源得到响应的特殊手段。但是打乱了原来正常的嵌套结构,被处理的程序不一定是最高优先级的事件。所以不能用正常的EOI命令来使ISR复位。但是退出SMM方式之后,仍可以用正常的EOI结束中断。
ps:这个我感觉有点像有关系走后门,可以为所欲为,但是破坏了原来的秩序。你对他还没有办法:)
4.中断触发方式:
边沿触发方式: 8259A的引脚IRi上出现上升沿表示有中断请求,高电平并不表示有中断请求。
电平触发方式: 8259A的引脚IRi上出现高电平表示有中断请求。在这种方式下,应该及时撤除高电平,否则可能引起不应该有的第二次中断。
无论是边沿触发还是电平触发,中断请求信号IR都应维持足够的宽度,就是在第一个中断响应信号/INTA结束之前IR都必须保持高电平。如果IR信号提前变为低电平,8259A就会自动假设这个中断请求来自引脚IR7.这样能有效防止由IR输入端上的严重噪声尖峰而产生的中断,如果IR7被使用,仍可通过读ISR状态来识别非正常的IR7中断。(正常的IR7中断会使ISR的IS7置位,而非正常的IR7中断不会)
5.级联工作方式:
中断源超过8个,无法用一片8259A来进行管理,这时候使用级联工作方式。指定一片为主片,其余为从片。主片的INT接到CPU上,其余的从片INT输出分别接到主控芯片的IR输入端。一片有八个中断输入端,所以最多有64个中断输入。
在级联系统中,每一片8259A,不管是主片还是从片,都有各自独立的初始化程序,以便设置各自的工作状态。中断结束要发两次EOI命令,分别使主片和从片完成中断结束操作。
中断响应过程中,若中断请求来自从片的IR,中断响应时,主片8259A会通过级联控制线CAS0~CAS2来通知相应从片的8259A,从片即可把中断向量码放到数据总线上。
级联方式下,可以采用前面说的特殊全嵌套方式,允许从片上优先级更高的IR产生中断。主片初始化为特殊全嵌套方式后,从片结束中断时,要用软件来检查ISR的内容,看看本片上有没有其它中断请求未被处理,如果没有,连发两个EOI,使主片,从片结束中断,如果还有其它中断,只发一个EOI命令。
(这个前面说过)
8259A的初始化编程
初始化编程不必多说,就是写入控制命令字,为我所用。
控制命令字分为:初始化命令字ICW, 操作命令字OCW
写入8259A后保存在内部的ICW和OCW寄存器中。
初始化编程:由CPU向 8259A送2~4个字节的初始化命令字ICW。在 8259A工作之前,必须写入初始化命令字
操作方式编程:由CPU向 8259A送3个字节的操作命令字OCW。以规定8259A的操作方式,可在初始化后的任何状态写入。
1.8259A内部寄存器寻址方法
靠/CS,A0,/RD,/WR和数据线D4,D3配合寻址。
2.8259A初始化顺序
对8259A进行写时,若I/O地址是奇数(A0=1),则写的对象包括四个寄存器(ICW2,ICW3,ICW4,ICW1)即一个I/O地址对应了4个寄存器。按照下图顺序写入。
3.8259A内部控制字
初始化命令字ICW
ICW1–初始化字,写入条件上上图,这时写入的数据被当成ICW1,意味着重新初始化8259A,同时:
清除ISR,IMR
中断优先级设置为初始状态,IR0最高,IR7最低
设定为普通屏蔽方式
采用非自动EOI中断结束方式
状态读出电路设置为读IRR
ICW1各功能如图所示
ICW2–中断向量码
ICW2为中断向量码寄存器,CPU响应中断时,放到数据总线。
初始化只需要确定T6~T3,低三位可以任意,8259A中断响应时自动填入。
ICW3–级联控制字
仅在多片级联时需要写入,主片与从片格式不同
主片标识码图上写的很明白,从片标识码与连接主片的IR序列号一致。
ICW4–中断结束方式字
紧跟ICW3写入同一I/O地址中
缓冲方式用于在级联工作时增大数据驱动能力,/SP/EN端作为输出端,输出一个允许信号。用来控制缓冲器的工作此时主片从片由D2位区分,主片=0,从片=1;非缓冲方式/SP/EN端为输入端,用以区分主片(高电平),从片(低电平)
操作命令字OCW
用来改变8259A 的中断控制方式,屏蔽中断源,以及读出8259A的工作状态(IRR,ISR,IMR),初始化完成后任意状态皆可写入,顺序也没有严格要求,但是对端口地址有规定:OCW1奇地址端口(A0=1),OCW2,OCW3必须为偶地址端口。
OCW1–中断屏蔽字
是否屏蔽中断,
OCW2–中断结束和优先级循环
对8259A发中断结束命令EOI,还可以控制中断优先级循环,与OCW3共用一个端口地址,有特征位D4D3=00,写入要求为偶地址。
R:优先级循环控制位:0固定优先级,IR7最低,IR0最高。1循环优先级。前面说了循环优先级
SL:特殊循环控制。SL=1,L2~L0对应的中断为最低优先级,SL=0时,对应的中断编码无效
EOI:中断结束命令,为1复位ISR相应位,,在ICW4的AEOI=0时,需要用OCW2来复位
OCW3–屏蔽方式和状态读出控制字
查询中断请求,CPU禁止中断或不想让8259A申请中断,CPU先写一个P=1的OCW3到8259A,再对同一地址读入。
I=1:表示本片8259A有中断请求,最高优先级的IR线由后三位编码组合给出
I=0;无中断请求
查询状态可重复进行,为了响应同时发生的中断。
读8259A状态
使用OCW3命令控制读出IRR,ISR,IMR的内容。
CPU先写RR RIS=10,再对同一地址读,为IRR内容
ISR同理
A0=1时读8259A,为IMR(不依赖OCW3)
编写程序,开干
我们梳理一下,就是首先需要写两个函数:pic_init函数和idt_init函数,我们首先使用汇编完成对中断处理的注册。
先介绍一下汇编概念下的宏:那就是
%macro 宏名称 参数个数
...
%endmacro
下面就是代码:
[bits 32]
%define ERROR_CODE nop ;若在相关异常中CPU已经自动压入了错误码,为保持栈中格式统一,这里不做操作
%define ZERO push 0 ;CPU没有压入错误码,为了统一栈中格式,手工压入0
extern put_str ;声明外部打印函数,上一章实现的
section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号
;所以一个终端类型一个中断处理程序
;自己知道自己的中断向量号是多少
%2
push intr_str
call put_str
add esp, 4 ;跳过参数
;如果是 从片 上进入的中断,除了往 从片发生EOI外,还要往主片发EOI
mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;向 从片发生
out 0x20, al ;向 主片发送
add esp,4 ;跨国error_code
iret ;从中断返回,32位下等同指令iretd
section .data ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了
dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组
%endmacro
VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ZERO
VECTOR 0x09, ZERO
VECTOR 0x0a, ZERO
VECTOR 0x0b, ZERO
VECTOR 0x0c, ZERO
VECTOR 0x0d, ZERO
VECTOR 0x0e, ZERO
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ZERO
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ZERO
VECTOR 0x19, ZERO
VECTOR 0x1a, ZERO
VECTOR 0x1b, ZERO
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO
VECTOR 0x21, ZERO
就是这样我们构成了一个中断向量描述表。
之后就是写C代码
interrupt.c
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 //主片
#define PIC_M_DATA 0x21
#define PIC_S_CTRL 0xA0 //从片
#define PIC_S_DATA 0xA1
#define IDT_DESC_CNT 0x21 //目前总共支持的中断数
//中断门描述符结构体
struct gate_desc{
uint16_t func_offset_low_word; //中断处理程序偏移量低16位
uint16_t selector; //中断处理程序目标段选择子
uint8_t dcount; //此项位双字计数字段,是门描述符第4字节,固定值
uint8_t attribute; //type属性 + S + DPL + P
uint16_t func_offset_high_word; //中断处理程序偏移量高16位
};
//静态函数声明,非必须
//intr_handler 实际上是void* ,将后两个参数写入第一个参数所指向的中断门描述符
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //idt本质上就是个中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //声明引用kernel.S里的中断处理函数入口数组
/* 初始化 可编程中断控制器 8259A */
static void pic_init(void) {
//初始化主片
outb(PIC_M_CTRL, 0x11); //ICW1: 0001 0001, 边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA, 0x20); //ICW2: 0010 0000, 其实中断向量号为0x20(0x20-0x27)
outb(PIC_M_DATA, 0x04); //ICW3: 0000 0100, IR2接从片
outb(PIC_M_DATA, 0x01); //ICW4: 0000 0001, 8086模式,正常EOI
//初始化从片
outb(PIC_S_CTRL, 0x11); //ICW1: 0001 0001,边沿触发, 级联8259, 需要ICW4
outb(PIC_S_DATA, 0x28); //ICW2:0010 1000,起始中断向量号为0x28(0x28-0x2f)
outb(PIC_S_DATA, 0x02); //ICW3: 0000 0010,设置连接到主片的IR2引脚
outb(PIC_S_DATA, 0x01); //ICW4, 0000 0001, 8086模式, 正常EOI
//打开主片上的IR0, 也就是目前只接受时钟产生的中断
//eflags 里的IF位 堆所有外部中断有效,但不能屏蔽某个外设的中断了
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);
put_str(" pic init done\n");
}
//创建中断门描述符
//参数:中断描述符,属性,中断处理函数
//功能:向中断描述符填充属性和地址
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF; //0000FFFF=1111 1111 1111 1111
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;
}
//初始化中断描述符表
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); //IDT_DESC_DPL0在global.h定义的
}
put_str(" idt_desc_init done\n");
}
//完成有关中断的所有初始化工作
void idt_init() {
put_str("idt_init_start\n");
idt_desc_init(); //初始化中断描述符表
pic_init(); //初始化8259A
//加载idt, idt=32位表基址+16位表界限
//通过lidt命令加载IDT,开启中断机制
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t) ((uint32_t)idt << 16)));
asm volatile("lidt %0" : : "m"(idt_operand));
put_str("idt_init_done\n");
}
global.h
#ifndef _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H
#include "stdint.h"
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)
/*-------------------IDT描述符-----------------*/
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32位的门
#define IDT_DESC_16_TYPE 0x6 //16位的门,用不到
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)
#endif
init.c 和 init.h
#ifndef __KERNEL_INIT_H
#define __kERNEL_INIT_H
void init_all(void);
#endif
//
#include "init.h"
#include "print.h"
#include "interrupt.h"
/*负责初始化所有模块*/
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
}
io.h
/************** 机器模式 ***************
b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。
w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。
HImode
“Half-Integer”模式,表示一个两字节的整数。
QImode
“Quarter-Integer”模式,表示一个一字节的整数。
*******************************************/
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"
/*向端口port写入一个字节*/
static inline void outb(uint16_t port, uint8_t data) {
/*********************************************************
对端口指定 N 表示0-255, d表示用dx存储端口号,
%b0表示对应al,%w1表示对应dx */
asm volatile ("outb %b0, %w1" : : "a"(data), "Nd"(port));
/******************************************************/
// 这里是 AT&T 语法的汇编语言,相当于: mov al. data
// mov dx, port
// out dx, al
}
/*将addr处其实的word_cnt 个字写入端口port */
static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
/*********************************************************
+ 表示此限制既做输入,又做输出,
outsw 是把 ds:esi 处的 16 位的内容写入 port 端口,
我们在设置段描述符时,已经将ds,es,ss段的选择子都设置为相同的值了, 此时不用担心数据错乱 */
asm volatile ("cld; rep outsw" : "+S"(addr), "+c"(word_cnt) : "d"(port));
/*********************************************************/
// 这里是 AT&T 语法的汇编语言,相当于: cld
// mov esi, addr
// mov ecx, word_cnt
// mov edx, port
}
/* 将从端口port 读入一个字节返回*/
static inline uint8_t inb(uint16_t port) {
uint8_t data;
asm volatile("inb %w1, %b0" : "=a"(data) : "Nd"(port));
return data;
}
/* 将从端口port 读入的word_cnt 个字写入addr */
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt){
//insw是将端口port处读入的16位内容写入es:edi 指向的内存
asm volatile("cld; rep insw" : "+D"(addr), "+c"(word_cnt) : "d"(port): "memory");
}
#endif
而后装入bochs即可。
我们下面做出一些改进:也就是把汇编的注册接口挪到C来,这样的话方便我们的操作
#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 //主片
#define PIC_M_DATA 0x21
#define PIC_S_CTRL 0xA0 //从片
#define PIC_S_DATA 0xA1
#define IDT_DESC_CNT 0x21 //目前总共支持的中断数
//中断门描述符结构体
struct gate_desc{
uint16_t func_offset_low_word; //中断处理程序偏移量低16位
uint16_t selector; //中断处理程序目标段选择子
uint8_t dcount; //此项位双字计数字段,是门描述符第4字节,固定值
uint8_t attribute; //type属性 + S + DPL + P
uint16_t func_offset_high_word; //中断处理程序偏移量高16位
};
//静态函数声明,非必须
//intr_handler 实际上是void* ,将后两个参数写入第一个参数所指向的中断门描述符
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //idt本质上就是个中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //声明引用kernel.S里的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //用于保存处理程序地址
//定义中断处理程序数组,在kernel.S中定义的intrXXentry
//只是中断处理程序入口,最终调用的是ide_table中的处理程序
;;;
/* 初始化 可编程中断控制器 8259A */
static void pic_init(void) {
//初始化主片
outb(PIC_M_CTRL, 0x11); //ICW1: 0001 0001, 边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA, 0x20); //ICW2: 0010 0000, 其实中断向量号为0x20(0x20-0x27)
outb(PIC_M_DATA, 0x04); //ICW3: 0000 0100, IR2接从片
outb(PIC_M_DATA, 0x01); //ICW4: 0000 0001, 8086模式,正常EOI
//初始化从片
outb(PIC_S_CTRL, 0x11); //ICW1: 0001 0001,边沿触发, 级联8259, 需要ICW4
outb(PIC_S_DATA, 0x28); //ICW2:0010 1000,起始中断向量号为0x28(0x28-0x2f)
outb(PIC_S_DATA, 0x02); //ICW3: 0000 0010,设置连接到主片的IR2引脚
outb(PIC_S_DATA, 0x01); //ICW4, 0000 0001, 8086模式, 正常EOI
//打开主片上的IR0, 也就是目前只接受时钟产生的中断
//eflags 里的IF位 堆所有外部中断有效,但不能屏蔽某个外设的中断了
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);
put_str(" pic init done\n");
}
//创建中断门描述符
//参数:中断描述符,属性,中断处理函数
//功能:向中断描述符填充属性和地址
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF; //0000FFFF=1111 1111 1111 1111
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;
}
//初始化中断描述符表
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); //IDT_DESC_DPL0在global.h定义的
}
put_str(" idt_desc_init done\n");
}
/*通用的中断处理函数,一般用在异常出现时的处理*/
static void general_intr_handler(uint8_t vec_nr){
if (vec_nr == 0x27 || vec_nr == 0x2f) {
//IRQ7 IRQ15会产生伪中断,此处无需处理
//0x2f 是从片8259A上的最后一个IR0引脚,保留项
return;
}
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}
/*完成一般中断处理函数注册及异常名称注册*/
static void exception_init(void){
int i;
for(i = 0; i < IDT_DESC_CNT; i++) {
//idt_table 数组中的函数是在进入中断后根据中断向量号调用的
//见kernel.S的call[idt_table + %1 * 4]
idt_table[i] = general_intr_handler; //默认为这个,后面会用register_handler 来注册具体处理函数
intr_name[i] = "unknown";
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device No七 Available Exception";
intr_name[8] = "JIDF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[l5]第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU F'loating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
//完成有关中断的所有初始化工作
void idt_init() {
put_str("idt_init_start\n");
idt_desc_init(); //初始化中断描述符表
exception_init(); //异常名初始化并注册通常的中断处理函数
pic_init(); //初始化8259A
//加载idt, idt=32位表基址+16位表界限
//通过lidt命令加载IDT,开启中断机制
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t) ((uint32_t)idt << 16)));
asm volatile("lidt %0" : : "m"(idt_operand));
put_str("idt_init_done\n");
}
以及这就需要我们修改接口:
kernel.S
[bits 32]
%define ERROR_CODE nop ;若在相关异常中CPU已经自动压入了错误码,为保持栈中格式统一,这里不做操作
%define ZERO push 0 ;CPU没有压入错误码,为了统一栈中格式,手工压入0
extern idt_table ;idt_table是C中注册的中断处理程序数组
section .data
; intr_entry_table位于data段, 之后会和宏中的data段组合在一起(注意: 宏中的text段与intr_entry_table不是同一个段)
global intr_entry_table
intr_entry_table:
;--------------- 宏 VECTOR 开始, 参数数目为2, 第一个参数为中断号, 第二个参数为该中断对 ERROR_CODE 的操作 -----------
%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号
;所以一个终端类型一个中断处理程序
;自己知道自己的中断向量号是多少
%2
;以下是保存上下文环境
push ds
push es
push fs
push gs
pushad
;如果是 从片 上进入的中断,除了往 从片发生EOI外,还要往主片发EOI
mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;向 从片发生
out 0x20, al ;向 主片发送
push %1 ;;不管idt_table中的目标程序是否需要参数,一律压入中断向量号
call [idt_table + %1*4] ;调用idt_table中的c版本中断处理函数
jmp intr_exit
section .data ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了
dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组
%endmacro
section .text
global intr_exit
intr_exit:
;以下是恢复上下文环境
add esp,4 ;跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp,4 ;跳过error_code
iretd
VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ERROR_CODE
VECTOR 0x09, ZERO
VECTOR 0x0a, ERROR_CODE
VECTOR 0x0b, ERROR_CODE
VECTOR 0x0c, ZERO
VECTOR 0x0d, ERROR_CODE
VECTOR 0x0e, ERROR_CODE
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ERROR_CODE
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ERROR_CODE
VECTOR 0x19, ZERO
VECTOR 0x1a, ERROR_CODE
VECTOR 0x1b, ERROR_CODE
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODE
VECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO
这样就可以了。
MakeFile 基本库书写 进程临门一脚
本章和第九章合并起来讲,这是因为第八章主要是为后面铺垫一些代码:
首先第一件事情就是:我们改用MakeFile来写代码!
Makefile的优点
- 管理代码的编译,决定该编译什么文件,编译顺序,以及是否需要重新编译;
- 节省编译时间。如果文件有更改,只需重新编译此文件即可,无需重新编译整个工程;
- 一劳永逸。Makefile通常只需编写一次,后期就不用过多更改。
命名规则
一般来说将Makefile命名为Makefile或makefile都可以,但很多源文件的名字是小写的,所以更多程序员采用的是Makefile的名字,因为这样可以将Makefile居前显示。
如果将Makefile命为其它名字,比如Makefile_demo,也是允许的,但使用的时候应该采用以下方式:
make -f Makefile_demo
基本规则
Makefile的基本规则为:
目标:依赖
(tab)规则
目标 —> 需要生成的目标文件 依赖 —> 生成该目标所需的一些文件 规则 —> 由依赖文件生成目标文件的手段 tab —> 每条规则必须以tab开头,使用空格不行
例如我们经常写的gcc test.c -o test,使用Makefile可以写成:
test: test.c gcc test.c -o test
其中,第一行中的test就是要生成的目标,test.c就是依赖,第二行就是由test.c生成test的规则。
Makefile中有时会有多个目标,但Makefile会将第一个目标定为终极目标。
工作原理
目标的生成: a. 检查规则中的依赖文件是否存在; b. 若依赖文件不存在,则寻找是否有规则用来生成该依赖文件
自动变量
常用自动变量:
Makefile提供了很多自动变量,但常用的为以下三个。这些自动变量只能在规则中的命令中使用,其它地方使用都不行。
$@ —> 规则中的目标
$< —> 规则中的第一个依赖条件
$^ —> 规则中的所有依赖条件
变量解析
用括号括起来再加个美元符,如:
FOO = $(OBJ)
伪目标声明:.PHONY:clean 等,这样我们输入make + 伪目标就可以执行对应的指令(有点像shell)
Kernel Assert
我们下面在内核层面实现Assert断言:
debug.c
#include "debug.h"
#include "print.h"
#include "interrupt.h"
void panic_spin(char* filename,int line,const char* func,const char* condition)
{
intr_disable(); //我的理解是中断关闭防止中断后cpu处理其他进程被调换
put_str("\n\n\n\\**********ERROR\\**********\\\n");
put_str("Filename: ");put_str(filename);put_char('\n');
put_str("Line: "); put_int(line); put_char('\n');
put_str("Func: ");put_str((char*)func);put_char('\n');
put_str("Condition: ");put_str((char*)condition);put_char('\n');
put_str("\\**********ERROR\\**********\\\n");
while(1);
}
debug.h
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename,int line,const char* func,const char* condition);
// __FILE__, __LINE__, __func__, __VA_ARGS__都是内置的宏
#define PANIC(...) panic_spin (__FILE__,__LINE__,__func__,__VA_ARGS__)
#ifdef NDEBUG
#define ASSERT(CONDITION) ((void)0) // 空声明,这样就可以回避一些警告
#else
#define ASSERT(CONDITION) \
if(CONDITION){} \
else{ PANIC(#CONDITION); }
#endif
c#endif
interrupt.c 新增部分
enum intr_status intr_enable()
{
if(intr_get_status() != INTR_ON)
{
asm volatile("sti");
return INTR_OFF;
}
return INTR_ON;
}
enum intr_status intr_disable()
{
if(intr_get_status() != INTR_OFF)
{
asm volatile("cli");
return INTR_ON;
}
return INTR_OFF;
}
enum intr_status intr_set_status(enum intr_status status)
{
return (status & INTR_ON) ? intr_enable() : intr_disable();
}
enum intr_status intr_get_status()
{
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (eflags & EFLAGS_IF) ? INTR_ON : INTR_OFF;
}
interrupt.h
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
enum intr_status
{
INTR_ON,
INTR_OFF
};
enum intr_status intr_enable();
enum intr_status intr_disable();
enum intr_status intr_set_status(enum intr_status status);
enum intr_status intr_get_status();
#endif
main.c
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void) {
put_str("I am kernel\n");
init_all();
ASSERT(2 == 3);
while(1);
}
string操作函数
这个我搓过了,这里简单连同位图(就是用比特表示资源的状态)一笔带过
string.c
#include "string.h"
#include "debug.h"
#include "global.h"
void memset(void* dst_,uint8_t value,uint32_t size)
{
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*) dst_;
while((size--) > 0)
*(dst++) = value;
return;
}
void memcpy(void* dst_,const void* src_,uint32_t size)
{
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t* dst = dst_;
const uint8_t* src = src_;
while((size--) > 0)
*(dst++) = *(src++);
return;
}
int memcmp(const void* a_,const void* b_, uint32_t size)
{
const char* a = a_;
const char* b = b_;
ASSERT(a != NULL || b != NULL);
while((size--) > 0)
{
if(*a != *b) return (*a > *b) ? 1 : -1;
++a,++b;
}
return 0;
}
char* strcpy(char* dsc_,const char* src_)
{
ASSERT(dsc_ != NULL && src_ != NULL);
char* dsc = dsc_;
while((*(dsc_++) = *(src_++) ));
return dsc;
}
uint32_t strlen(const char* str)
{
ASSERT(str != NULL);
const char* ptr = str;
while(*(ptr++));
return (ptr - str - 1); //例如一个字 1 '\0' ptr会指向'\0'后面一位
}
int8_t strcmp(const char* a,const char* b)
{
ASSERT(a != NULL && b != NULL);
while(*a && *a == *b)
{
a++,b++;
}
return (*a < *b) ? -1 : (*a > *b) ; //这个表达式太猛了 用活了
}
char* strchr(const char* str,const char ch)
{
ASSERT(str != NULL);
while(*str)
{
if(*str == ch) return (char*)str;
++str;
}
return NULL;
}
char* strrchr(const char* str,const uint8_t ch)
{
ASSERT(str != NULL);
char* last_chrptr = NULL;
while(*str != 0)
{
if(ch == *str) last_chrptr = (char*)str;
str++;
}
return last_chrptr;
}
char* strcat(char* dsc_,const char* src_)
{
ASSERT(dsc_ != NULL && src_ != NULL);
char* str = dsc_;
while(*(str++));
str--;
while(*(str++) = *(src_++));
return dsc_;
}
char* strchrs(const char* str,uint8_t ch)
{
ASSERT(str != NULL);
uint32_t ch_cnt = 0;
while(*str)
{
if(*str == ch) ++ch_cnt;
++str;
}
return ch_cnt;
}
string.h
#ifndef __LIB_STRING_H
#define __LIB_STRING_H
#include "stdint.h"
#define NULL 0
void memset(void* dst_,uint8_t value,uint32_t size);
void memcpy(void* dst_,const void* src_,uint32_t size);
int memcmp(const void* a_,const void* b_, uint32_t size);
char* strcpy(char* dsc_,const char* src_);
uint32_t strlen(const char* str);
int8_t strcmp(const char* a,const char* b);
char* strchr(const char* str,const char ch);
char* strrchr(const char* str,const uint8_t ch);
char* strcat(char* dsc_,const char* src_);
char* strchrs(const char* str,uint8_t ch);
#endif
bitmap.c
#include "bitmap.h" //函数定义
#include "global.h"
#include "string.h" //memset函数要用
#include "interrupt.h"
#include "print.h"
#include "debug.h"
#define BITMAP_MASK 1
void bitmap_init(struct bitmap* btmp)
{
memset(btmp->bits,0,btmp->btmp_bytes_len);
return;
}
bool bitmap_scan_test(struct bitmap* btmp,uint32_t bit_idx) //一个8位的数 bit_idx/8 找数组下标 %得索引下的具体位置
{
uint32_t byte_idx = bit_idx/8;
uint32_t byte_pos = bit_idx%8;
return (btmp->bits[byte_idx] & (BITMAP_MASK << byte_pos));
}
/*
这个函数写很多的原因 是因为刚开始先用一个字节的快速扫描
看是否每个字节中存在 位为0的位置 然后紧接着再看连续位 挺有意思 自己先写写看
*/
int bitmap_scan(struct bitmap* btmp,uint32_t cnt)
{
ASSERT(cnt >= 1);
uint32_t first_find_idx = 0;
//解释一下0xff 一共8位 0xff = 11111111b
while(first_find_idx < btmp->btmp_bytes_len && btmp->bits[first_find_idx] == 0xff)
++first_find_idx;
if(first_find_idx == btmp->btmp_bytes_len) return -1;
uint32_t find_pos = 0;
while((btmp->bits[first_find_idx] & (BITMAP_MASK << find_pos)))
++find_pos;
if(cnt == 1) return find_pos + 8*first_find_idx;
uint32_t ret_pos = find_pos + 8*first_find_idx + 1,tempcnt = 1,endpos = (btmp->btmp_bytes_len)*8;
while(ret_pos < endpos)
{
if(!bitmap_scan_test(btmp,ret_pos)) ++tempcnt;
else tempcnt = 0;
if(tempcnt == cnt)
return ret_pos - tempcnt + 1;
++ret_pos;
}
return -1;
}
void bitmap_set(struct bitmap* btmp,uint32_t bit_idx,int8_t value)
{
ASSERT(value == 1 || value == 0);
uint32_t byte_idx = bit_idx/8;
uint32_t byte_pos = bit_idx%8;
if(value) btmp->bits[byte_idx] |= (BITMAP_MASK << byte_pos);
else btmp->bits[byte_idx] &= ~(BITMAP_MASK << byte_pos);
return;
}
bitmap.h
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#include "stdint.h"
#define BITMAP_MASK 1
typedef int bool;
struct bitmap
{
uint32_t btmp_bytes_len;
uint8_t* bits;
};
void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp,uint32_t bit_idx);
int bitmap_scan(struct bitmap* btmp,uint32_t cnt);
void bitmap_set(struct bitmap* btmp,uint32_t bit_idx,int8_t value);
#endif
内存池
我们知道,现在我们在保护模式之下,我们根据段选择子取出的是虚拟地址,我们需要对这些像是水池的内存进行管理,他们当中有一部分用来跑内核,另一部分跑用户进程!所以,我们的下面的目标就是去实现内存的管理
memory.h
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
struct virtual_addr
{
struct bitmap vaddr_bitmap;
uint32_t vaddr_start;
};
enum pool_flags
{
PF_KERNEL = 1,
PF_USER = 2
};
#define PG_P_1 1
#define PG_P_0 0
#define PG_RW_R 0
#define PG_RW_W 2
#define PG_US_S 0
#define PG_US_U 4
extern struct pool kernel_pool,user_pool;
void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt);
uint32_t* pte_ptr(uint32_t vaddr); // Page Table Entry
uint32_t* pde_ptr(uint32_t vaddr); // Page Directory Entry
void* palloc(struct pool* m_pool); // Page Allocation
void page_table_add(void* _vaddr,void* _page_phyaddr);
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt);
void* get_kernel_pages(uint32_t pg_cnt);
void mem_pool_init(uint32_t all_mem);
void mem_init(void);
#endif
memory.c
#include "memory.h"
#include "stdint.h"
#include "print.h"
#include "bitmap.h"
#include "debug.h"
#include "string.h"
#define PG_SIZE 4096
#define MEM_BITMAP_BASE 0Xc009a000 //位图开始存放的位置
#define K_HEAP_START 0xc0100000 //内核栈起始位置
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
struct pool
{
struct bitmap pool_bitmap; //位图来管理内存使用
uint32_t phy_addr_start; //内存池开始的起始地址
uint32_t pool_size; //池容量
};
struct pool kernel_pool ,user_pool; //生成内核内存池 和 用户内存池
struct virtual_addr kernel_vaddr; //内核虚拟内存管理池
void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt)
{
int vaddr_start = 0,bit_idx_start = -1;
uint32_t cnt = 0;
if(pf == PF_KERNEL)
{
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt);
if(bit_idx_start == -1) return NULL;
while(cnt < pg_cnt)
bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start + (cnt++),1);
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else
{}
return (void*)vaddr_start;
}
uint32_t* pte_ptr(uint32_t vaddr)
{
uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
return pte;
}
uint32_t* pde_ptr(uint32_t vaddr)
{
uint32_t* pde = (uint32_t*) ((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
void* palloc(struct pool* m_pool)
{
int bit_idx = bitmap_scan(&m_pool->pool_bitmap,1);
if(bit_idx == -1) return NULL;
bitmap_set(&m_pool->pool_bitmap,bit_idx,1);
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void*)page_phyaddr;
}
void page_table_add(void* _vaddr,void* _page_phyaddr)
{
uint32_t vaddr = (uint32_t)_vaddr,page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);
if(*pde & 0x00000001)
{
ASSERT(!(*pte & 0x00000001));
if(!(*pte & 0x00000001))
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
else
{
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
else
{
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
memset((void*)((int)pte & 0xfffff000),0,PG_SIZE);
ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
return;
}
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt)
{
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
void* vaddr_start = vaddr_get(pf,pg_cnt);
if(vaddr_start == NULL) return NULL;
uint32_t vaddr = (uint32_t)vaddr_start,cnt = pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
while(cnt-- > 0)
{
void* page_phyaddr = palloc(mem_pool);
if(page_phyaddr == NULL) return NULL;
page_table_add((void*)vaddr,page_phyaddr);
vaddr += PG_SIZE;
}
return vaddr_start;
}
void* get_kernel_pages(uint32_t pg_cnt)
{
void* vaddr = malloc_page(PF_KERNEL,pg_cnt);
if(vaddr != NULL) memset(vaddr,0,pg_cnt*PG_SIZE);
return vaddr;
}
void mem_pool_init(uint32_t all_mem)
{
put_str(" mem_pool_init start!\n");
uint32_t page_table_size = PG_SIZE * 256; //页表占用的大小
uint32_t used_mem = page_table_size + 0x100000; //低端1MB的内存 + 页表所占用的大小
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; //空余的页数 = 总空余内存 / 一页的大小
uint16_t kernel_free_pages = all_free_pages /2; //内核 与 用户 各平分剩余内存
uint16_t user_free_pages = all_free_pages - kernel_free_pages; //万一是奇数 就会少1 减去即可
//kbm kernel_bitmap ubm user_bitmap
uint32_t kbm_length = kernel_free_pages / 8; //一位即可表示一页 8位一个数
uint32_t ubm_length = user_free_pages / 8;
//kp kernel_pool up user_pool
uint32_t kp_start = used_mem;
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_char('\n');
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_char('\n');
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
return;
}
void mem_init()
{
put_str("mem_init start!\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00)); //我们把总内存的值放在了0xb00,现在取出来!
mem_pool_init(mem_bytes_total);
put_str("mem_init done!\n");
return;
}
然后在init.c加了一个#include "memory.h
“ 和 一句 mem_init();
就可以。
main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
int main(void) {
put_str("I am kernel\n");
init_all();
void* addr = get_kernel_pages(3);
put_str("\n get_kernel_page start vaddr is ");
put_int((uint32_t)addr);
put_char('\n');
while(1);
return 0;
}
makefile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h kernel/memory.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h\
lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/cooiboi/bochs/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
线程
线程可以说是自进程之后最小的作业单元了!他进一步细分了进程,让进程成为了被分配的单元之一而实际进行改成了进程来实现!
内核级线程 指的是操作系统支持线程 即提供了线程这个概念 程序员可以在操作系统的帮助下来进程创建线程操作 线程这个实物概念对于操作系统是可见的
而用户级线程 是指的是 操作系统不支持线程这个概念 用户 程序员在写进程程序的时候 自己写了调度器或者自己写了一个执行流 而线程的切换是通过进程本身来调节的
用户级线程的好处是切换线程时不需要陷入内核,切换线程的代价要小的多,速度非常快。但坏处是一旦发生阻塞,操作系统是不清楚有这个线程的存在,可能就直接把进程撤下,整个的进程就直接阻塞
我们所以实现内核级线程:
thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
typedef void thread_func(void*); //这里有点不懂定义的什么意思 搜了搜博客 发现是函数声明
enum task_status
{
TASK_RUNNING, // 0
TASK_READY, // 1
TASK_BLOCKED, // 2
TASK_WAITING, // 3
TASK_HANGING, // 4
TASK_DIED // 5
};
/* intr_stack 用于处理中断被切换的上下文环境储存 */
/* 这里我又去查了一下 为什么是反着的 越在后面的参数 地址越高 */
struct intr_struct
{
uint32_t vec_no; //中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
uint32_t err_code;
void (*eip) (void); //这里声明了一个函数指针
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/* 线程栈 保护线程环境 */
struct thread_stack
{
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
void (*eip) (thread_func* func,void* func_arg); //和下面的相互照应 以ret 汇编代码进入kernel_thread函数调用
void (*unused_retaddr); //占位数 在栈顶站住了返回地址的位置 因为是汇编ret
thread_func* function; //进入kernel_thread要调用的函数地址
void* func_arg; //参数指针
};
struct task_struct
{
uint32_t* self_kstack; //pcb中的 kernel_stack 内核栈
enum task_status status; //线程状态
uint8_t priority; //特权级
char name[16];
uint32_t stack_magic; //越界检查 因为我们pcb上面的就是我们要用的栈了 到时候还要越界检查
};
void kernel_thread(thread_func* function,void* func_arg);
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);
void init_thread(struct task_struct* pthread,char* name,int prio);
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
#endif
kernel.c
#include "thread.h" //函数声明 各种结构体
#include "stdint.h" //前缀
#include "string.h" //memset
#include "global.h" //不清楚
#include "memory.h" //分配页需要
#define PG_SIZE 4096
void kernel_thread(thread_func* function,void* func_arg)
{
function(func_arg);
}
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg)
{
pthread->self_kstack -= sizeof(struct intr_struct); //减去中断栈的空间
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread; //地址为kernel_thread 由kernel_thread 执行function
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->ebx = kthread_stack->esi = 0; //初始化一下
return;
}
void init_thread(struct task_struct* pthread,char* name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //pcb位置清0
strcpy(pthread->name,name);
pthread->status = TASK_RUNNING;
pthread->priority = prio;
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //刚开始的位置是最低位置 栈顶位置+一页
pthread->stack_magic = 0x23333333; //设置的魔数 检测是否越界限
}
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg)
{
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,prio);
thread_create(thread,function,func_arg);
asm volatile("movl %0,%%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g"(thread->self_kstack) :"memory"); //栈顶的位置为 thread->self_kstack
return thread;
}
main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "../thread/thread.h"
void test_thread(void* arg);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("kernel_thread_a",31,test_thread,"argA ");
while(1);
return 0;
}
void test_thread(void* arg)
{
while(1)
put_str((char*)arg);
}
双向链表(这个我熟)(喜)
不说了,这个我搓过了,直接上代码,没啥好讲的
list.c
#include "list.h"
#include "interrupt.h"
#include "stdint.h"
#include "debug.h"
#define NULL 0
//初始化双向链表
void list_init(struct list* list)
{
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}
//把链表 elem放在 before前面
void list_insert_before(struct list_elem* before,struct list_elem* elem)
{
enum intr_status old_status = intr_disable();
elem->next = before;
elem->prev = before->prev;
before->prev->next = elem;
before->prev = elem;
intr_set_status(old_status);
}
//添加元素到链表队首
void list_push(struct list* plist,struct list_elem* elem)
{
list_insert_before(plist->head.next,elem);
}
//添加元素到链表队尾
void list_append(struct list* plist,struct list_elem* elem)
{
list_insert_before(&plist->tail,elem);
}
//让pelem脱离链表
void list_remove(struct list_elem* pelem)
{
enum intr_status old_status = intr_disable();
pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;
intr_set_status(old_status);
}
//让链表的第一个元素脱离链表
struct list_elem* list_pop(struct list* plist)
{
ASSERT(plist->head.next != &plist->tail);
struct list_elem* ret = plist->head.next;
list_remove(plist->head.next);
return ret;
}
bool list_empty(struct list* plist)
{
return (plist->head.next == &plist->tail ? true : false);
}
uint32_t list_len(struct list* plist)
{
uint32_t ret = 0;
struct list_elem* next = plist->head.next;
while(next != &plist->tail)
{
next = next->next;
++ret;
}
return ret;
}
struct list_elem* list_traversal(struct list* plist,function func,int arg)
{
struct list_elem* elem = plist->head.next;
if(list_empty(plist)) return NULL;
while(elem != &plist->tail)
{
if(func(elem,arg)) return elem;
elem = elem->next;
}
return NULL;
}
bool elem_find(struct list* plist,struct list_elem* obj_elem)
{
struct list_elem* ptr = plist->head.next;
while(ptr != &plist->tail)
{
if(ptr == obj_elem) return true;
ptr = ptr->next;
}
return false;
}
list.h
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "stdint.h"
#define offset(struct_type,member) (int) (&((struct_type*)0)->member) // 就是结构体的成员寻址偏移
#define elem2entry(struct_type,struct_member_name,elem_ptr) \
(struct_type*)((int)elem_ptr - offset(struct_type,struct_member_name)) // 就是结构体的成员寻址偏移
struct list_elem
{
struct list_elem* prev; //前面的节点
struct list_elem* next; //后面的节点
};
struct list
{
struct list_elem head; // 亘古不变的头部
struct list_elem tail; // 亘古不变的尾部
};
typedef bool (function) (struct list_elem*,int arg);
void list_init(struct list*);
void list_insert_before(struct list_elem* before,struct list_elem* elem);
void list_push(struct list* plist,struct list_elem* elem);
void list_append(struct list* plist,struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist,function func,int arg);
bool elem_find(struct list* plist,struct list_elem* obj_elem);
#endif
PCB结果完善
1、先创建线程
2、打开中断 每个时钟中断调用中断函数 减去当前时间片
3、时间片为0 简称到期了 到期之后 调用schedule调度器 切换线程
4、schedule 把在最前面的准备队列的任务的pcb获取 把当前的放到最后
5、之后转到switch_to 保存寄存器 上下文环境 切换esp 即切换线程
main
主线程的本来pcb
的内存空间就是留了的 这部分的问题在上面已经说过了 分配了一页内存后 pcb
的核心部分在最低端的内存处 最高处是中断栈 就是存放各种寄存器的位置 最高处的下面就是内核栈 里面有四个我们相关的寄存器 还有一个esp
的位置 剩下的就是第一次被切换所需要的内容了
thread.c
#include "thread.h" //函数声明 各种结构体
#include "stdint.h" //前缀
#include "string.h" //memset
#include "global.h"
#include "memory.h" //分配页需要
#include "debug.h"
#include "interrupt.h"
#include "print.h"
#define PG_SIZE 4096
struct task_struct* main_thread; //主线程main_thread的pcb
struct list thread_ready_list; //就绪队列
struct list thread_all_list; //总线程队列
extern void switch_to(struct task_struct* cur,struct task_struct* next);
// 获取 pcb 指针
// 这部分我可以来稍微解释一下
// 我们线程所在的esp 肯定是在 我们get得到的那一页内存 pcb页上下浮动 但是我们的pcb的最起始位置是整数的 除去后面的12位
// 那么我们对前面的取 & 则可以得到 我们的地址所在地
struct task_struct* running_thread(void)
{
uint32_t esp;
asm ("mov %%esp,%0" : "=g"(esp));
return (struct task_struct*)(esp & 0xfffff000);
}
void kernel_thread(thread_func* function,void* func_arg)
{
intr_enable(); //开中断 防止后面的时间中断被屏蔽无法切换线程
function(func_arg);
}
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg)
{
pthread->self_kstack -= sizeof(struct intr_struct); //减去中断栈的空间
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread; //地址为kernel_thread 由kernel_thread 执行function
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->ebx = kthread_stack->esi = 0; //初始化一下
return;
}
void init_thread(struct task_struct* pthread,char* name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //pcb位置清0
strcpy(pthread->name,name);
if(pthread == main_thread)
pthread->status = TASK_RUNNING; //我们的主线程肯定是在运行的
else
pthread->status = TASK_READY; //放到就绪队列里面
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //刚开始的位置是最低位置 栈顶位置+一页得最顶部
//后面还要对这个值进行修改
pthread->priority = prio;
pthread->ticks = prio; //和特权级 相同的时间片
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; //线程没有单独的地址
pthread->stack_magic = 0x23333333; //设置的魔数 检测是否越界限
}
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg)
{
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,prio);
thread_create(thread,function,func_arg);
ASSERT(!elem_find(&thread_ready_list,&thread->general_tag)); //之前不应该在就绪队列里面
list_append(&thread_ready_list,&thread->general_tag);
ASSERT(!elem_find(&thread_all_list,&thread->all_list_tag));
list_append(&thread_all_list,&thread->all_list_tag);
return thread;
}
//之前在loader.S的时候已经 mov esp,0xc0009f00
//现在的esp已经就在预留的pcb位置上了
void make_main_thread(void)
{
main_thread = running_thread(); //得到main_thread 的pcb指针
init_thread(main_thread,"main",31);
ASSERT(!elem_find(&thread_all_list,&main_thread->all_list_tag));
list_append(&thread_all_list,&main_thread->all_list_tag);
}
void schedule(void)
{
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread(); //得到当前pcb的地址
if(cur->status == TASK_RUNNING)
{
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag)); //目前在运行的肯定ready_list是不在的
list_append(&thread_ready_list,&cur->general_tag); //加入尾部
cur->status = TASK_READY;
cur->ticks = cur->priority;
}
ASSERT(!list_empty(&thread_ready_list));
struct task_struct* thread_tag = list_pop(&thread_ready_list);
//书上面的有点难理解 代码我写了一个我能理解的
struct task_struct* next = (struct task_struct*)((uint32_t)thread_tag & 0xfffff000);
next->status = TASK_RUNNING;
switch_to(cur,next); //esp头顶的是 返回地址 +12是next +8是cur
}
void thread_init(void)
{
put_str("thread_init start!\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
make_main_thread();
put_str("thread_init done!\n");
}
thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"
typedef void thread_func(void*); //这里有点不懂定义的什么意思 搜了搜博客 发现是函数声明
enum task_status
{
TASK_RUNNING, // 0
TASK_READY, // 1
TASK_BLOCKED, // 2
TASK_WAITING, // 3
TASK_HANGING, // 4
TASK_DIED // 5
};
/* intr_stack 用于处理中断被切换的上下文环境储存 */
/* 这里我又去查了一下 为什么是反着的 越在后面的参数 地址越高 */
struct intr_struct
{
uint32_t vec_no; //中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
uint32_t err_code;
void (*eip) (void); //这里声明了一个函数指针
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/* 线程栈 保护线程环境 */
struct thread_stack
{
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
void (*eip) (thread_func* func,void* func_arg); //和下面的相互照应 以ret 汇编代码进入kernel_thread函数调用
void (*unused_retaddr); //占位数 在栈顶站住了返回地址的位置 因为是汇编ret
thread_func* function; //进入kernel_thread要调用的函数地址
void* func_arg; //参数指针
};
struct task_struct
{
uint32_t* self_kstack; //pcb中的 kernel_stack 内核栈
enum task_status status; //线程状态
uint8_t priority; //特权级
uint8_t ticks; //在cpu 运行的滴答数 看ticks 来判断是否用完了时间片
uint32_t elapsed_ticks; //一共执行了多久
char name[16];
struct list_elem general_tag; //就绪队列中的连接节点
struct list_elem all_list_tag; //总队列的连接节点
uint32_t* pgdir; //进程自己页表的虚拟地址 线程没有
uint32_t stack_magic; //越界检查 因为我们pcb上面的就是我们要用的栈了 到时候还要越界检查
};
struct task_struct* running_thread(void);
void kernel_thread(thread_func* function,void* func_arg);
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);
void init_thread(struct task_struct* pthread,char* name,int prio);
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
void make_main_thread(void);
void schedule(void);
void thread_init(void);
#endif
interrupt.c
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
#define EFLAGS_IF 0x00000200 // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))
/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};
// 静态函数声明,非必须
static void pic_init(void);
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static void general_intr_handler(uint8_t vec_nr);
static void exception_init(void);
void idt_init(void);
void register_handler(uint8_t vec_no,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt是中断描述符表,本质上就是个中断门描述符数组
void register_handler(uint8_t vec_no,intr_handler function);
char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字
/******** 定义中断处理程序数组 ********
* 在kernel.S中定义的intrXXentry只是中断处理程序的入口,
* 最终调用的是ide_table中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];
/********************************************/
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/* 创建中断门描述符 */
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void) {
int i, lastindex = IDT_DESC_CNT - 1;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
* 中断处理程序为单独的syscall_handler */
put_str(" idt_desc_init done\n");
}
/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
set_cursor(0); //光标设置在0号位
int cursor_pos = 0;
while((cursor_pos++) < 320) //一行80字 4行空格
put_char(' ');
set_cursor(0);
put_str("!!!!!! excetion message begin !!!!!!\n");
set_cursor(88); //第二行第八个字开始打印
put_str(intr_name[vec_nr]); //打印中断向量号
if(vec_nr == 14)
{
int page_fault_vaddr = 0;
asm("movl %%cr2,%0" : "=r" (page_fault_vaddr)); //把虚拟地址 出错的放到了这个变量里面
put_str("\npage fault addr is ");
put_int(page_fault_vaddr);
}
put_str("!!!!!! excetion message end !!!!!!\n");
while(1); //悬停
}
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void) { // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为unknown
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
exception_init(); // 异常名初始化并注册通常的中断处理函数
pic_init(); // 初始化8259A
/* 加载idt */
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
void register_handler(uint8_t vec_no,intr_handler function)
{
//把相关向量号的注册函数指针放进去了
idt_table[vec_no] = function;
}
enum intr_status intr_enable()
{
if(intr_get_status() != INTR_ON)
{
asm volatile("sti");
return INTR_OFF;
}
return INTR_ON;
}
enum intr_status intr_disable()
{
if(intr_get_status() != INTR_OFF)
{
asm volatile("cli");
return INTR_ON;
}
return INTR_OFF;
}
enum intr_status intr_set_status(enum intr_status status)
{
return (status == INTR_ON) ? intr_enable() : intr_disable();
}
enum intr_status intr_get_status()
{
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (eflags & EFLAGS_IF) ? INTR_ON : INTR_OFF;
}
switch.S
[bits 32]
section .text
global switch_to
switch_to:
push esi ;这里是根据ABI原则保护四个寄存器 放到栈里面
push edi
push ebx
push ebp
mov eax,[esp+20] ;esp+20的位置是cur cur的pcb赋值给eax
mov [eax],esp ;[eax]为pcb的内核栈指针变量 把当前环境的esp值记录下来
mov eax,[esp+24]
mov esp,[eax] ;把要切换的线程的pcb 内核栈esp取出来
pop ebp
pop ebx
pop edi
pop esi
ret ;这里的返回地址为 kernel_thread的地址
稍加修改过的print.S
为了用set_cursor
修改了一下 把set_cursor
变成可以调用的函数了
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
section .data
put_int_buffer dq 0 ; 定义8字节缓冲区用于数字到字符的转换
[bits 32]
section .text
;--------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;--------------------------------------------
global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
push ebx
push ecx
xor ecx, ecx ; 准备用ecx存储参数,清空
mov ebx, [esp + 12] ; 从栈中得到待打印的字符串地址
.goon:
mov cl, [ebx]
cmp cl, 0 ; 如果处理到了字符串尾,跳到结束处返回
jz .str_over
push ecx ; 为put_char函数传递参数
call put_char
add esp, 4 ; 回收参数所占的栈空间
inc ebx ; 使ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
;-------------------- 将小端字节序的数字变成对应的ascii后,倒置 -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp+4*9] ; call的返回地址占4字节+pushad的8个4字节
mov edx, eax
mov edi, 7 ; 指定在put_int_buffer中初始的偏移量
mov ecx, 8 ; 32位数字中,16进制数字的位数是8个
mov ebx, put_int_buffer
;将32位数字按照16进制的形式从低位到高位逐个处理,共处理8个16进制数字
.16based_4bits: ; 每4位二进制是16进制数字的1位,遍历每一位16进制数字
and edx, 0x0000000F ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
cmp edx, 9 ; 数字0~9和a~f需要分别处理成对应的字符
jg .is_A2F
add edx, '0' ; ascii码是8位大小。add求和操作后,edx低8位有效。
jmp .store
.is_A2F:
sub edx, 10 ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
add edx, 'A'
;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
; 此时dl中应该是数字对应的字符的ascii码
mov [ebx+edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
;现在put_int_buffer中已全是字符,打印之前,
;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
inc edi ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:
cmp edi,8 ; 若已经比较第9个字符了,表示待打印的字符串为全0
je .full0
;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:
mov cl, [put_int_buffer+edi]
inc edi
cmp cl, '0'
je .skip_prefix_0 ; 继续判断下一位字符是否为字符0(不是数字0)
dec edi ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符
jmp .put_each_num
.full0:
mov cl,'0' ; 输入的数字为全0时,则只打印0
.put_each_num:
push ecx ; 此时cl中为可打印的字符
call put_char
add esp, 4
inc edi ; 使edi指向下一个字符
mov cl, [put_int_buffer+edi] ; 获取下一个字符到cl寄存器
cmp edi,8
jl .put_each_num
popad
ret
;------------------------ put_char -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------
global put_char
put_char:
pushad ;备份32位寄存器环境
;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器
mov gs, ax
;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
;先获得高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
in al, dx ;得到了光标位置的高8位
mov ah, al
;再获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
;将光标存入bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp + 36] ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节
cmp cl, 0xd ;CR是0x0d,LF是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace)的asc码是8
jz .is_backspace
jmp .put_other
;;;;;;;;;;;;;;;;;;
.is_backspace:
;;;;;;;;;;;; backspace的一点说明 ;;;;;;;;;;
; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
; 这就显得好怪异,所以此处添加了空格或空字符0
dec bx
shl bx,1
mov byte [gs:bx], 0x20 ;将待删除的字节补为0或空格皆可
inc bx
mov byte [gs:bx], 0x07
shr bx,1
jmp set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.put_other:
shl bx, 1 ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
mov [gs:bx], cl ; ascii字符本身
inc bx
mov byte [gs:bx],0x07 ; 字符属性
shr bx, 1 ; 恢复老的光标值
inc bx ; 下一个光标值
cmp bx, 2000
jl set_cursor ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
; 若超出屏幕字符数大小(2000)则换行处理
.is_line_feed: ; 是换行符LF(\n)
.is_carriage_return: ; 是回车符CR(\r)
; 如果是CR(\r),只要把光标移到行首就行了。
xor dx, dx ; dx是被除数的高16位,清0.
mov ax, bx ; ax是被除数的低16位.
mov si, 80 ; 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
div si ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。
sub bx, dx ; 光标值减去除80的余数便是取整
; 以上4行处理\r的代码
.is_carriage_return_end: ; 回车符CR处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ; 若是LF(\n),将光标移+80便可。
jl set_cursor
;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen: ; 若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次
mov esi, 0xc00b80a0 ; 第1行行首
mov edi, 0xc00b8000 ; 第0行行首
rep movsd
;;;;;;;将最后一行填充为空白
mov ebx, 3840 ; 最后一行首字符的第一个字节偏移= 1920 * 2
mov ecx, 80 ;一行是80字符(160字节),每次清理1字符(2字节),一行需要移动80次
.cls:
mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx,1920 ;将光标值重置为1920,最后一行的首字符.
global set_cursor:
set_cursor:
;将光标设为bx值
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
稍微修改了一下的stdint.h
发现bool
经常会出错 而且true
false
报错 就这里加了一下
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
#define true 1
#define false 0
typedef int bool;
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif
timer.c
#include "timer.h"
#include "io.h"
#include "print.h"
#include "../kernel/interrupt.h"
#include "../thread/thread.h"
#include "debug.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define COUNTER0_PORT 0X40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_COUNTROL_PORT 0x43
//自中断开启以来总的滴答数
uint32_t ticks;
void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value)
{
outb(PIT_COUNTROL_PORT,(uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1));
outb(counter_port,(uint8_t)counter_value);
outb(counter_port,(uint8_t)counter_value >> 8);
return;
}
void intr_timer_handler(void)
{
struct task_struct* cur_thread = running_thread(); //得到pcb指针
ASSERT(cur_thread->stack_magic == 0x23333333); //检测栈是否溢出
++ticks;
++cur_thread->elapsed_ticks;
if(!cur_thread->ticks)
schedule();
else
--cur_thread->ticks;
return;
}
void timer_init(void)
{
put_str("timer_init start!\n");
frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH,COUNTER_MODE,COUNTER0_VALUE);
register_handler(0x20,intr_timer_handler); //注册时间中断函数 0x20向量号函数更换
put_str("timer_init done!\n");
return;
}
timer.h
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"
void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value);
void intr_timer_handler(void);
void timer_init(void);
void mtime_sleep(uint32_t m_seconds);
#endif
MakeFile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o $(BUILD_DIR)/switch.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h kernel/memory.h \
thread/thread.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h \
thread/thread.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/io.h lib/kernel/print.h \
kernel/interrupt.h thread/thread.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h kernel/global.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/debug.h kernel/interrupt.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
kernel/interrupt.h lib/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/switch.o: thread/switch.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/cooiboi/bochs/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
注意,这样的代码会抛出GP异常,我们会在下一张实现锁来防止错乱。
输入输出
关于锁的概念
公共资源:可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
互斥:互斥也可称为排他,是指某一时刻公共资源只能被1个任务独享,其他任务想访问公共资源时,必须等待当前公共资源的访问者使用完资源后再开始访问。
临界区:程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。强调一下,临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源。
竞争条件:竞争条件是指多个任务以非互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行进行的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。
所以同步机制的核心,就是让公共资源在某一时刻只能被一个线程的临界区所访问,并且要让它执行完。
围绕上面的目的,我们提出信号量的概念,它实质就是个计数器,有真实含义(如某个资源的数量),常见取值是0与1。当某个进程的临界区需要访问公共资源,就要去查询这个公共资源的信号量,只有信号量>0时(一般就是1),也就是资源可用,才能访问公共资源,此时该公共资源的信号量被减为0。另个进程此时要来访问这个公共资源,发现信号量为0,也就意味着自己需要的资源不可用,但是进程没有所需要的资源,就无法继续推进,所以最好的方法不是让进程在此处死等,而是将自己换下处理器让出处理器,这就是把自己阻塞起来,等待信号量为1后才能被唤醒运行。我们把只有0和1两种情况的信号量叫做二元信号量。
所以,同步机制的核心,现在就变成了,获得信号量的进程才可以运行,没有获得信号量的进程把自己阻塞起来,获得信号量的进程运行完毕释放信号量的时候,要把阻塞起来的进程唤醒,唤醒就是把阻塞进程的pcb从信号量的阻塞队列移入就绪队列。
为了实现同步机制,我们先实现两个函数thread_block与thread_unlock,前者用于将进程阻塞起来,实现原理就是将线程的pcb中的状态字段修改,然后使用调度schedule函数(后面需要修改其对于BLOCKED线程的调度策略);后者用于将进程解除阻塞,原理就是修改pcb的状态字段,然后将线程的pcb放入就绪队列队首(为了尽快调度)。这两个函数均不涉及对某信号量的阻塞队列的操作,我们在别处实现
我们先再加上我们的thread.c, thread.h
void thread_block(enum task_status stat)
{
//设置block状态的参数必须是下面三个以下的
ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || stat == TASK_HANGING));
enum intr_status old_status = intr_disable(); //关中断
struct task_struct* cur_thread = running_thread();
cur_thread->status = stat; //把状态重新设置
//调度器切换其他进程了 而且由于status不是running 不会再被放到就绪队列中
schedule();
//被切换回来之后再进行的指令了
intr_set_status(old_status);
}
//由锁拥有者来执行的 善良者把原来自我阻塞的线程重新放到队列中
void thread_unblock(struct task_struct* pthread)
{
enum intr_status old_status = intr_disable();
ASSERT(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
if(pthread->status != TASK_READY)
{
//被阻塞线程 不应该存在于就绪队列中)
ASSERT(!elem_find(&thread_ready_list,&pthread->general_tag));
if(elem_find(&thread_ready_list,&pthread->general_tag))
PANIC("thread_unblock: blocked thread in ready_list\n"); //debug.h中定义过
//让阻塞了很久的任务放在就绪队列最前面
list_push(&thread_ready_list,&pthread->general_tag);
//状态改为就绪态
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
void thread_block(enum task_status stat);
void thread_unblock(struct task_struct* pthread);
接下来我们要实现锁机制,来实现对二元信号量的有序分配。首先为信号量与锁建立数据结构。信号量与锁的关系:信号量是对某项资源的管理,实际就是表示资源有多少,与哪些线程在等待这个资源。锁是在信号量机制上实现的,相比信号量多了记录谁造成了锁(也就是二元信号量,或者叫资源分配给了谁)
sync.c/ sync.h
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"
/* 信号量结构 */
struct semaphore {
uint8_t value; //一个信号量肯定有值来表示这个量
struct list waiters; //用一个双链表结点来管理所有阻塞在该信号量上的线程
};
/* 锁结构 */
struct lock {
struct task_struct* holder; //用于记录谁把二元信号量申请走了,而导致了该信号量的锁
struct semaphore semaphore; //一个锁肯定是来管理信号量的
uint32_t holder_repeat_nr; //有时候线程拿到了信号量,但是线程内部不止一次使用该信号量对应公共资源,就会不止一次申请锁
//内外层函数在释放锁时就会对一个锁释放多次,所以必须要记录重复申请的次数
};
void sema_init(struct semaphore* psema, uint8_t value);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
#endif
#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"
//用于初始化信号量,传入参数就是指向信号量的指针与初值
void sema_init(struct semaphore* psema, uint8_t value) {
psema->value = value; // 为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}
//用于初始化锁,传入参数是指向该锁的指针
void lock_init(struct lock* plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1); //将信号量初始化为1,因为此函数一般处理二元信号量
}
//信号量的down操作,也就是减1操作,传入参数是指向要操作的信号量指针。线程想要申请信号量的时候用此函数
void sema_down(struct semaphore* psema) {
enum intr_status old_status = intr_disable(); //对于信号量的操作是必须关中断的
//一个自旋锁,来不断判断是否信号量已经被分配出去了。为什么不用if,见书p450。
while(psema->value == 0) { // 若value为0,表示已经被别人持有
ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
/* 当前线程不应该已在信号量的waiters队列中 */
if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
PANIC("sema_down: thread blocked has been in waiters_list\n");
}
//如果此时信号量为0,那么就将该线程加入阻塞队列,为什么不用判断是否在阻塞队列中呢?因为线程被阻塞后,会加入阻塞队列,除非被唤醒,否则不会
//分配到处理器资源,自然也不会重复判断是否有信号量,也不会重复加入阻塞队列
list_append(&psema->waiters, &running_thread()->general_tag);
thread_block(TASK_BLOCKED); // 阻塞线程,直到被唤醒
}
/* 若value为1或被唤醒后,会执行下面的代码,也就是获得了锁。*/
psema->value--;
ASSERT(psema->value == 0);
/* 恢复之前的中断状态 */
intr_set_status(old_status);
}
//信号量的up操作,也就是+1操作,传入参数是指向要操作的信号量的指针。且释放信号量时,应唤醒阻塞在该信号量阻塞队列上的一个进程
void sema_up(struct semaphore* psema) {
/* 关中断,保证原子操作 */
enum intr_status old_status = intr_disable();
ASSERT(psema->value == 0);
if (!list_empty(&psema->waiters)) { //判断信号量阻塞队列应为非空,这样才能执行唤醒操作
struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
thread_unblock(thread_blocked);
}
psema->value++;
ASSERT(psema->value == 1);
/* 恢复之前的中断状态 */
intr_set_status(old_status);
}
//获取锁的函数,传入参数是指向锁的指针
void lock_acquire(struct lock* plock) {
//这是为了排除掉线程自己已经拿到了锁,但是还没有释放就重新申请的情况
if (plock->holder != running_thread()) {
sema_down(&plock->semaphore); //对信号量进行down操作
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1; //申请了一次锁
} else {
plock->holder_repeat_nr++;
}
}
//释放锁的函数,参数是指向锁的指针
void lock_release(struct lock* plock) {
ASSERT(plock->holder == running_thread());
//如果>1,说明自己多次申请了该锁,现在还不能立即释放锁
if (plock->holder_repeat_nr > 1) {
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1); //判断现在lock的重复持有数是不是1只有为1,才能释放
plock->holder = NULL; //这句必须放在up操作前,因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}
现在,我们利用锁机制,建立锁console_lock(意为终端锁)用于协调打印,将原有的put_int,put_char,put_str进行封装。
device/console.c, console.h
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock; // 控制台锁
/* 初始化终端 */
void console_init() {
lock_init(&console_lock);
}
/* 获取终端 */
void console_acquire() {
lock_acquire(&console_lock);
}
/* 释放终端 */
void console_release() {
lock_release(&console_lock);
}
/* 终端中输出字符串 */
void console_put_str(char* str) {
console_acquire();
put_str(str);
console_release();
}
/* 终端中输出字符 */
void console_put_char(uint8_t char_asci) {
console_acquire();
put_char(char_asci);
console_release();
}
/* 终端中输出16进制整数 */
void console_put_int(uint32_t num) {
console_acquire();
put_int(num);
console_release();
}
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
void console_init(void);
void console_acquire(void);
void console_release(void);
void console_put_str(char* str);
void console_put_char(uint8_t char_asci);
void console_put_int(uint32_t num);
#endif
放进init.c
init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init();
console_init();
}
main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");
intr_enable();
while(1) {
console_put_str("Main ");
};
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
console_put_str(para);
}
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
console_put_str(para);
}
}
键盘驱动
在键盘内部,会有一个8048芯片,主板内部有8042芯片,当我们按下某个键/松开某个键,8048就会报告这个按键的按下与松开情况给8042,然后8042向中断控制器发送信号。
键盘按下键位后的流程:
1、当键位被按下(不弹起)
2、8048 监控哪个键位被按下,8048 把键位对应的通码(用于描述一个键按下的码,断码就是描述一个键松开的码)发送给 8042,如果不松开,那么就持续发送
3、8042 接收到通码后,便知道具体哪个键位被按下了,对其进行处理,接着保存通码到自己的寄存器
4、8042 接着向中断代理 8259A 发送中断,如果不松开,那么就持续发送中断
5、发生中断后,处理器执行对应的中断处理程序
键位弹起的过程和按下的过程一致。
扫描码(通码与断码合称扫描码)由键盘编码器决定,不同的键盘编码器会产生不同的编码方案,如今有三套:
scan code set 1, 应用:XT 键盘
scan code set 2, 应用:AT 键盘
scan code set 3, 应用:IBM PS/2 系列高端计算机所用键盘
现在大多数用的都是第二套,因此大多数键盘向 8042 发生的都是第二套的扫描码,为了兼容,不管我们用的是第几套编码方案,当键盘发送扫描码到 8042 后,由 8042 进行处理,转为第一套扫描码,这也是 8042 存在的理由之一。因此我们只需要在键盘的中断处理程序中只处理第一套扫描码就可以了。
p465、p466、p467剖析代码kernel.S、interrupt.c、main.c、keyboard.c:
1、代码功能
按下键盘任意键后,屏幕上打印字符k,如果一直按住不松开,那么就连续打印
2、实现原理
键盘信号最后都是由中断处理器来处理,最后都是由对应的中断处理函数来处理
3、代码逻辑
A、为键盘中断建立中断处理函数
B、设定中断控制器,打开键盘中断信号
4、怎么写代码?
A、kernel.S:增加用汇编模板定义的汇编中断函数处理入口
B、kerboard.c:写出键盘中断处理函数intr_keyboard_handler;写出键盘初始化函数keyboard_init(调用中断注册函数注册键盘中断处理函数),并将其封装进入init_all
C、interrupt.c:修改支持中断数量;修改中断控制器初始化代码pic_inic,只打开键盘中断(键盘中断信号接在主片的IR1引脚上)
D、写出测试代码main.c,就是一个死循环
我们先扩展中断向量表:
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0
extern put_str;
extern idt_table;
section .data
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
VECTOR 0x0 ,ZERO
VECTOR 0X1 ,ZERO
VECTOR 0X2 ,ZERO
VECTOR 0x3 ,ZERO
VECTOR 0X4 ,ZERO
VECTOR 0X5 ,ZERO
VECTOR 0x6 ,ZERO
VECTOR 0X7 ,ZERO
VECTOR 0X8 ,ERROR_CODE
VECTOR 0x9 ,ZERO
VECTOR 0XA ,ERROR_CODE
VECTOR 0XB ,ERROR_CODE
VECTOR 0XC ,ERROR_CODE
VECTOR 0XD ,ERROR_CODE
VECTOR 0XE ,ERROR_CODE
VECTOR 0XF ,ZERO
VECTOR 0X10 ,ZERO
VECTOR 0X11 ,ERROR_CODE
VECTOR 0x12 ,ZERO
VECTOR 0X13 ,ZERO
VECTOR 0X14 ,ZERO
VECTOR 0x15 ,ZERO
VECTOR 0X16 ,ZERO
VECTOR 0X17 ,ZERO
VECTOR 0X18 ,ZERO
VECTOR 0X19 ,ZERO
VECTOR 0X1A ,ZERO
VECTOR 0X1B ,ZERO
VECTOR 0X1C ,ZERO
VECTOR 0X1D ,ZERO
VECTOR 0X1E ,ERROR_CODE ;处理器自动推错误码
VECTOR 0X1F ,ZERO
VECTOR 0X20 ,ZERO ;时钟中断
VECTOR 0X21 ,ZERO ;键盘中断
VECTOR 0X22 ,ZERO ;级联
VECTOR 0X23 ,ZERO ;串口2
VECTOR 0X24 ,ZERO ;串口1
VECTOR 0X25 ,ZERO ;并口2
VECTOR 0X26 ,ZERO ;软盘
VECTOR 0X27 ,ZERO ;并口1
VECTOR 0X28 ,ZERO ;实时时钟
VECTOR 0X29 ,ZERO ;重定向
VECTOR 0X2A ,ZERO ;保留
VECTOR 0x2B ,ZERO ;保留
VECTOR 0x2C ,ZERO ;ps/2 鼠标
VECTOR 0x2D ,ZERO ;fpu 浮点单元异常
VECTOR 0x2E ,ZERO ;硬盘
VECTOR 0x2F ,ZERO ;保留
修改interrupt.c
主要修改这两个地方
#define IDT_DESC_CNT 0x30 // 目前总共支持的中断数
outb (PIC_M_DATA, 0xfd); //这里修改了 只打开键盘中断
outb (PIC_S_DATA, 0xff);
编写keyboard.c
#include "keyboard.h"
#include "print.h"
#include "io.h"
#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#define KBD_BUF_PORT 0X60
#define KBD_BUF_PORT 0X60
#define esc '\033' //esc 和 delete都没有
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'
#define char_invisible 0 //功能性 不可见字符均设置为0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
bool ctrl_status = false,shift_status = false,alt_status = false,caps_lock_status = false,ext_scancode = false;
// 键值映射
char keymap[][2] = {
/* 0x00 */ {0, 0},
/* 0x01 */ {esc, esc},
/* 0x02 */ {'1', '!'},
/* 0x03 */ {'2', '@'},
/* 0x04 */ {'3', '#'},
/* 0x05 */ {'4', '$'},
/* 0x06 */ {'5', '%'},
/* 0x07 */ {'6', '^'},
/* 0x08 */ {'7', '&'},
/* 0x09 */ {'8', '*'},
/* 0x0A */ {'9', '('},
/* 0x0B */ {'0', ')'},
/* 0x0C */ {'-', '_'},
/* 0x0D */ {'=', '+'},
/* 0x0E */ {backspace, backspace},
/* 0x0F */ {tab, tab},
/* 0x10 */ {'q', 'Q'},
/* 0x11 */ {'w', 'W'},
/* 0x12 */ {'e', 'E'},
/* 0x13 */ {'r', 'R'},
/* 0x14 */ {'t', 'T'},
/* 0x15 */ {'y', 'Y'},
/* 0x16 */ {'u', 'U'},
/* 0x17 */ {'i', 'I'},
/* 0x18 */ {'o', 'O'},
/* 0x19 */ {'p', 'P'},
/* 0x1A */ {'[', '{'},
/* 0x1B */ {']', '}'},
/* 0x1C */ {enter, enter},
/* 0x1D */ {ctrl_l_char, ctrl_l_char},
/* 0x1E */ {'a', 'A'},
/* 0x1F */ {'s', 'S'},
/* 0x20 */ {'d', 'D'},
/* 0x21 */ {'f', 'F'},
/* 0x22 */ {'g', 'G'},
/* 0x23 */ {'h', 'H'},
/* 0x24 */ {'j', 'J'},
/* 0x25 */ {'k', 'K'},
/* 0x26 */ {'l', 'L'},
/* 0x27 */ {';', ':'},
/* 0x28 */ {'\'', '"'},
/* 0x29 */ {'`', '~'},
/* 0x2A */ {shift_l_char, shift_l_char},
/* 0x2B */ {'\\', '|'},
/* 0x2C */ {'z', 'Z'},
/* 0x2D */ {'x', 'X'},
/* 0x2E */ {'c', 'C'},
/* 0x2F */ {'v', 'V'},
/* 0x30 */ {'b', 'B'},
/* 0x31 */ {'n', 'N'},
/* 0x32 */ {'m', 'M'},
/* 0x33 */ {',', '<'},
/* 0x34 */ {'.', '>'},
/* 0x35 */ {'/', '?'},
/* 0x36 */ {shift_r_char, shift_r_char},
/* 0x37 */ {'*', '*'},
/* 0x38 */ {alt_l_char, alt_l_char},
/* 0x39 */ {' ', ' '},
/* 0x3A */ {caps_lock_char, caps_lock_char}
};
void keyboard_init()
{
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}
void intr_keyboard_handler(void)
{
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
if(scancode == 0xe0) //多字节处理
{
ext_scancode = true;
return;
}
break_code = ((scancode & 0x0080) != 0); //断码 = 通码 + 0x80 通码最小比0x80小 则只有断码才可以有
if(break_code)
{
uint16_t make_code = (scancode &= 0xff7f); //多字节不处理
if(make_code == ctrl_l_make || make_code == ctrl_r_make) ctrl_status = false;
else if(make_code == shift_l_make || make_code == shift_r_make) shift_status = false;
else if(make_code == alt_l_make || make_code == alt_r_make) alt_status = false;
return;
}
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
{
bool shift = false; //先默认设置成false
if((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || \
(scancode == 0x1b) || (scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || (scancode == 0x34) || \
(scancode == 0x35))
{
if(shift_down_last) shift = true;
}
else
{
if(shift_down_last && caps_lock_last) shift = false; //效果确实是这样子的 我试了一下
else if(shift_down_last || caps_lock_last) shift = true; //其中任意一个都是大写的作用
else shift = false;
}
uint8_t index = (scancode & 0x00ff);
char cur_char = keymap[index][shift];
if(cur_char)
{
put_char(cur_char);
return;
}
if(scancode == ctrl_l_make || scancode == ctrl_r_make)
ctrl_status = true;
else if(scancode == shift_l_make || scancode == shift_r_make)
shift_status = true;
else if(scancode == alt_l_make || scancode == alt_r_make)
alt_status = true;
else if(scancode == caps_lock_make)
caps_lock_status = !caps_lock_status;
else put_str("unknown key\n");
}
return;
}
编写keyboard.h
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void intr_keyboard_handler(void);
void keyboard_init(void);
#endif
修改main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "../thread/thread.h"
#include "interrupt.h"
#include "../device/console.h"
void test_thread1(void* arg);
void test_thread2(void* arg);
int main(void) {
put_str("I am kernel\n");
init_all();
//thread_start("kernel_thread_a",8,test_thread1,"Arga ");
//thread_start("kernel_thread_b",8,test_thread2,"Argb ");
intr_enable();
while(1);
/*{
console_put_str("Main ");
}*/
return 0;
}
void test_thread1(void* arg)
{
while(1)
{
console_put_str((char*)arg);
}
}
void test_thread2(void* arg)
{
while(1)
{
console_put_str((char*)arg);
}
}
修改init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "../device/keyboard.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init();
timer_init();
thread_init();
console_init();
keyboard_init(); //新增
}
修改MakeFile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/switch.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \
$(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o $(BUILD_DIR)/keyboard.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h \
thread/thread.h device/console.h device/keyboard.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/io.h lib/kernel/print.h \
kernel/interrupt.h thread/thread.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h kernel/global.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/debug.h kernel/interrupt.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
kernel/interrupt.h lib/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \
lib/stdint.h thread/thread.h kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h \
lib/kernel/print.h thread/sync.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \
lib/kernel/print.h lib/kernel/io.h kernel/interrupt.h \
kernel/global.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/switch.o: thread/switch.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/cooiboi/bochs/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
实现环形缓冲区 生产者消费者
我们下面就是搞一下缓冲区,这是为了可以让指令可以完整的进行处理!
编写ioqueue.h
#ifndef __DEVICE__IOQUEUE_H
#define __DEVICE__IOQUEUE_H
#include "stdint.h"
#include "../thread/thread.h"
#include "../thread/sync.h"
#define bufsize 64
struct ioqueue
{
struct lock lock;
struct task_struct* consumer;
struct task_struct* producer;
char buf[bufsize];
uint32_t head; //头部读入数据
uint32_t tail; //尾部拿数据
};
void init_ioqueue(struct ioqueue* ioq);
uint32_t next_pos(uint32_t pos);
bool ioq_full(struct ioqueue* ioq);
bool ioq_empty(struct ioqueue* ioq);
void ioq_wait(struct task_struct** waiter); //这里是waiter的二级指针 取二级指针的原因是这样可以对指针的地址值进行修改
void wakeup(struct task_struct** waiter);
char ioq_getchar(struct ioqueue* ioq);
void ioq_putchar(struct ioqueue* ioq,char chr);
#endif
编写ioqueue.c
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
void init_ioqueue(struct ioqueue* ioq)
{
lock_init(&ioq->lock);
ioq->consumer = ioq->producer = NULL;
ioq->head = ioq->tail = 0;
}
uint32_t next_pos(uint32_t pos)
{
return (pos+1)%bufsize;
}
bool ioq_full(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);
return next_pos(ioq->head) == ioq->tail;
}
bool ioq_empty(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);
return ioq->head == ioq->tail;
}
void ioq_wait(struct task_struct** waiter)
{
ASSERT(waiter != NULL && *waiter == NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
void wakeup(struct task_struct** waiter)
{
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
char ioq_getchar(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);
//空的时候就需要动锁了 把另外的消费者卡住
while(ioq_empty(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char retchr = ioq->buf[ioq->tail];
ioq->tail = next_pos(ioq->tail);
if(ioq->producer)
wakeup(&ioq->producer);
return retchr;
}
void ioq_putchar(struct ioqueue* ioq,char chr)
{
ASSERT(intr_get_status() == INTR_OFF);
while(ioq_full(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = chr;
ioq->head = next_pos(ioq->head);
if(ioq->consumer)
wakeup(&ioq->consumer);
}
稍修修改一下的keyboard.c
#include "keyboard.h"
#include "print.h"
#include "io.h"
#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#include "ioqueue.h"
#define KBD_BUF_PORT 0X60
struct ioqueue ioqueue;
#define esc '\033' //esc 和 delete都没有
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'
#define char_invisible 0 //功能性 不可见字符均设置为0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
bool ctrl_status = false,shift_status = false,alt_status = false,caps_lock_status = false,ext_scancode = false;
/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码 未与shift组合 与shift组合*/
/* ---------------------------------- */
/* 0x00 */ {0, 0},
/* 0x01 */ {esc, esc},
/* 0x02 */ {'1', '!'},
/* 0x03 */ {'2', '@'},
/* 0x04 */ {'3', '#'},
/* 0x05 */ {'4', '$'},
/* 0x06 */ {'5', '%'},
/* 0x07 */ {'6', '^'},
/* 0x08 */ {'7', '&'},
/* 0x09 */ {'8', '*'},
/* 0x0A */ {'9', '('},
/* 0x0B */ {'0', ')'},
/* 0x0C */ {'-', '_'},
/* 0x0D */ {'=', '+'},
/* 0x0E */ {backspace, backspace},
/* 0x0F */ {tab, tab},
/* 0x10 */ {'q', 'Q'},
/* 0x11 */ {'w', 'W'},
/* 0x12 */ {'e', 'E'},
/* 0x13 */ {'r', 'R'},
/* 0x14 */ {'t', 'T'},
/* 0x15 */ {'y', 'Y'},
/* 0x16 */ {'u', 'U'},
/* 0x17 */ {'i', 'I'},
/* 0x18 */ {'o', 'O'},
/* 0x19 */ {'p', 'P'},
/* 0x1A */ {'[', '{'},
/* 0x1B */ {']', '}'},
/* 0x1C */ {enter, enter},
/* 0x1D */ {ctrl_l_char, ctrl_l_char},
/* 0x1E */ {'a', 'A'},
/* 0x1F */ {'s', 'S'},
/* 0x20 */ {'d', 'D'},
/* 0x21 */ {'f', 'F'},
/* 0x22 */ {'g', 'G'},
/* 0x23 */ {'h', 'H'},
/* 0x24 */ {'j', 'J'},
/* 0x25 */ {'k', 'K'},
/* 0x26 */ {'l', 'L'},
/* 0x27 */ {';', ':'},
/* 0x28 */ {'\'', '"'},
/* 0x29 */ {'`', '~'},
/* 0x2A */ {shift_l_char, shift_l_char},
/* 0x2B */ {'\\', '|'},
/* 0x2C */ {'z', 'Z'},
/* 0x2D */ {'x', 'X'},
/* 0x2E */ {'c', 'C'},
/* 0x2F */ {'v', 'V'},
/* 0x30 */ {'b', 'B'},
/* 0x31 */ {'n', 'N'},
/* 0x32 */ {'m', 'M'},
/* 0x33 */ {',', '<'},
/* 0x34 */ {'.', '>'},
/* 0x35 */ {'/', '?'},
/* 0x36 */ {shift_r_char, shift_r_char},
/* 0x37 */ {'*', '*'},
/* 0x38 */ {alt_l_char, alt_l_char},
/* 0x39 */ {' ', ' '},
/* 0x3A */ {caps_lock_char, caps_lock_char}
};
void keyboard_init()
{
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
init_ioqueue(&ioqueue);
put_str("keyboard init done\n");
}
void intr_keyboard_handler(void)
{
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
if(scancode == 0xe0) //多字节处理
{
ext_scancode = true;
return;
}
break_code = ((scancode & 0x0080) != 0); //断码 = 通码 + 0x80 通码最小比0x80小 则只有断码才可以有
if(break_code)
{
uint16_t make_code = (scancode &= 0xff7f); //多字节不处理
if(make_code == ctrl_l_make || make_code == ctrl_r_make) ctrl_status = false;
else if(make_code == shift_l_make || make_code == shift_r_make) shift_status = false;
else if(make_code == alt_l_make || make_code == alt_r_make) alt_status = false;
return;
}
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
{
bool shift = false; //先默认设置成false
if((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) || \
(scancode == 0x1b) || (scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || (scancode == 0x34) || \
(scancode == 0x35))
{
if(shift_down_last) shift = true;
}
else
{
if(shift_down_last && caps_lock_last) shift = false; //效果确实是这样子的 我试了一下
else if(shift_down_last || caps_lock_last) shift = true; //其中任意一个都是大写的作用
else shift = false;
}
uint8_t index = (scancode & 0x00ff);
char cur_char = keymap[index][shift];
if(cur_char)
{
if(!ioq_full(&ioqueue))
ioq_putchar(&ioqueue,cur_char);
//put_char(cur_char);
return;
}
if(scancode == ctrl_l_make || scancode == ctrl_r_make)
ctrl_status = true;
else if(scancode == shift_l_make || scancode == shift_r_make)
shift_status = true;
else if(scancode == alt_l_make || scancode == alt_r_make)
alt_status = true;
else if(scancode == caps_lock_make)
caps_lock_status = !caps_lock_status;
else put_str("unknown key\n");
}
return;
}
稍微修改一下的keyboard.h
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
extern struct ioqueue ioqueue;
void intr_keyboard_handler(void);
void keyboard_init(void);
#endif
稍微修改一下的interrupt.c
这里就只把修改部分贴出来
第0位 是键盘中断 第1位 是时钟中断 这两位得是0 1表示屏蔽
则 1100(二进制) == 0xc(16进制)
换一下即可
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
outb (PIC_M_DATA, 0xfc);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
稍微修改一下的main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "../thread/thread.h"
#include "interrupt.h"
#include "../device/console.h"
#include "../device/ioqueue.h"
#include "../device/keyboard.h"
void test_thread1(void* arg);
void test_thread2(void* arg);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("kernel_thread_a",31,test_thread1," A_");
thread_start("kernel_thread_b",31,test_thread2," B_");
intr_enable();
while(1);
return 0;
}
void test_thread1(void* arg)
{
while(1)
{
enum intr_status old_status = intr_disable();
while(!ioq_empty(&ioqueue))
{
console_put_str((char*)arg);
char chr = ioq_getchar(&ioqueue);
console_put_char(chr);
}
intr_set_status(old_status);
}
}
void test_thread2(void* arg)
{
while(1)
{
enum intr_status old_status = intr_disable();
while(!ioq_empty(&ioqueue))
{
console_put_str((char*)arg);
char chr = ioq_getchar(&ioqueue);
console_put_char(chr);
}
intr_set_status(old_status);
}
}
修改后的MakeFile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/switch.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \
$(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o $(BUILD_DIR)/keyboard.o \
$(BUILD_DIR)/ioqueue.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h \
device/keyboard.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h \
thread/thread.h device/console.h device/keyboard.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/io.h lib/kernel/print.h \
kernel/interrupt.h thread/thread.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h kernel/global.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/debug.h kernel/interrupt.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
kernel/interrupt.h lib/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \
lib/stdint.h thread/thread.h kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h \
lib/kernel/print.h thread/sync.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \
lib/kernel/print.h lib/kernel/io.h kernel/interrupt.h \
kernel/global.h lib/stdint.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \
kernel/interrupt.h kernel/global.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/switch.o: thread/switch.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/cooiboi/bochs/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
用户进程
我们来看看用户进程,从这里开始,我们就进入了用户世界。TSS之前就聊过了,这里复习一下:
TSS:是用于存储任务状态的一个数据结构,每个任务都有自己的TSS。这个数据结构包含了在任务切换时需要保存和恢复的信息,例如处理器寄存器的值、堆栈指针、页目录基地址寄存器的值等,它放在内存的一块连续区域中。TSS是硬件级别任务切换机制的一部分。以下是TSS的使用方式:
- 初始化TSS:在操作系统启动时,它会初始化一个或多个TSS。每个TSS都被初始化为包含一个任务的初始状态。比如,TSS可能被初始化为指向新任务的代码段的入口点,堆栈指针可能被初始化为指向新任务的栈顶,等等。
- 将TSS的描述符添加到GDT中:操作系统会为每个TSS在全局描述符表(GDT)中创建一个描述符。TSS描述符包含了TSS的基地址和大小,以及其他一些标志。一旦TSS描述符被添加到GDT中,处理器就可以使用这个描述符来访问这个TSS了。
- 使用LTR指令加载TSS:当操作系统想要切换到一个新任务时,它会使用LTR(Load Task Register)指令将新任务的TSS描述符加载到任务寄存器(TR)中,TR寄存器中是这个任务的TSS的GDT选择子。这个操作告诉处理器新任务的TSS在哪里。
- 任务切换:当处理器执行任务切换时,它会自动保存当前任务的状态到当前任务的TSS中,并从新任务的TSS中恢复新任务的状态。这包括保存和恢复处理器寄存器的值、堆栈指针的值,等等。
现代x86体系上的操作系统并没有采用intel设计CPU时想的那种任务切换方式,因为其开销过大而导致效率过低,而是采用的一种基于TSS机制(因为这是硬件提供的,绕不开)的缩减版任务切换方式,在这种情况下,TSS主要被用于存储每个处理器的内核栈地址,以支持从用户模式到内核模式的切换,以下是关于利用TSS实现任务切换的一些要点。
1、当一个中断发生在用户态(特权级 3),处理器将从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值。
2、每个 CPU 中只创建一个 TSS,在各个 CPU 上执行的所有任务都共享一个 TSS。
3、在 TR 加载 TSS 后,该 TR 寄存器将永远指向那一个 TSS,之后再也不会重新加载 TSS。
4、在进程切换时,只需要把 TSS 中的 SS0 和 ESP0 更新为新任务的内核栈的段地址以及栈指针。
5、Linux 对 TSS 的操作是一次性加载 TSS 到 TR,之后不断修改同一个 TSS 的内容,不再重复加载。
6、Linux 中任务切换不使用 call 和 jmp 指令,避免了任务切换的低效。
任务的状态信息存储位置: 当用户态触发中断后,由特权级 3 陷入特权级 0 后,CPU 自动从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值,作为特权级 0 的栈,然后手动执行一系列 push 指令将任务的状态保存在特权级0的栈中
TSS作为绕不开的硬件机制,所以我们必须要先进入这种机制。也就是必须要GDT表中为其创建一个TSS段描述符,然后用加载选择子进入TR寄存器。现在我们来编写代码来做这件事情,为用户进程做准备。
首先,我们修改kernel/global.h,在其中添加模块化的段描述符字段,为我们后面拼凑段描述符(TSS段描述符、用户程序用的代码段,数据段,栈段描述符)做准备。并且定义TSS的选择子、同时也要定义用户程序用的代码段、数据段、栈段选择子 字段含义查看p151
// ---------------- GDT描述符属性 ----------------
#define DESC_G_4K 1
#define DESC_D_32 1
#define DESC_L 0 // 64位代码标记,此处标记为0便可。
#define DESC_AVL 0 // cpu不用此位,暂置为0
#define DESC_P 1
#define DESC_DPL_0 0
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
#define DESC_S_CODE 1
#define DESC_S_DATA DESC_S_CODE
#define DESC_S_SYS 0
#define DESC_TYPE_CODE 8 // x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
#define DESC_TYPE_DATA 2 // x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
#define DESC_TYPE_TSS 9 // B位为0,不忙
//定义不同的用户程序用的段描述符选择子
#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_STACK SELECTOR_U_DATA
#define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4)) //定义段描述符的高32位的高字
#define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE) //定义用户程序用的代码段描述符高32位的低字
#define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA) //定义用户程序用的数据段描述符高32位的低字
//--------------- TSS描述符属性 ------------
#define TSS_DESC_D 0 //这个D/B位在其他段描述中用于表示操作数的大小,但这里不是,实际上它根本就没有被使用(总是设置为0)。
//这是因为TSS的大小和结构并不依赖于处理器运行在16位模式还是32位模式。
//无论何时,TSS都包含了32位的寄存器值、32位的线性地址等等,因此没有必要用D/B位来表示操作的大小
#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0) //TSS段描述符高32位高字
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS) //TSS段描述符高32位低字
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2 ) + RPL0)
struct gdt_desc {
uint16_t limit_low_word;
uint16_t base_low_word;
uint8_t base_mid_byte;
uint8_t attr_low_byte;
uint8_t limit_high_attr_high;
uint8_t base_high_byte;
};
#define PG_SIZE 4096
接下来我们写函数,来完成TSS段描述符的创建与初始化
userprog/tss.c
#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"
//定义tss的数据结构,在内存中tss的分布就是这个结构体
struct tss {
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t (*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint16_t trace;
uint16_t io_base;
};
static struct tss tss;
//用于更新TSS中的esp0的值,让它指向线程/进程的0级栈
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}
//用于创建gdt描述符,传入参数1,段基址,传入参数2,段界限;参数3,属性低字节,参数4,属性高字节(要把低四位置0,高4位才是属性)
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
uint32_t desc_base = (uint32_t)desc_addr;
struct gdt_desc desc;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}
/* 在gdt中创建tss并重新加载gdt */
void tss_init() {
put_str("tss_init start\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;
/* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 */
//在gdt表中添加tss段描述符,在本系统的,GDT表的起始位置为0x00000900,那么tss的段描述就应该在0x920(0x900+十进制4*8)
*((struct gdt_desc*)0xc0000920) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
/* 在gdt中添加dpl为3的数据段和代码段描述符 */
*((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
*((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
/* gdt 16位的limit 32位的段基址 */
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); // 7个描述符大小
asm volatile ("lgdt %0" : : "m" (gdt_operand));
asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
put_str("tss_init and ltr done\n");
}
建立userprog/tss.h 增加函数声明
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
void update_tss_esp(struct task_struct* pthread);
void tss_init(void);
#endif
修改kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化PIT
console_init(); // 控制台初始化最好放在开中断之前
keyboard_init(); // 键盘初始化
tss_init(); // tss初始化
}
接下来,我们实现用户进程
进程与内核线程的核心区别是有两个:
1、进程有单独的4GB空间(虚拟)
2、进程运行在特权级3,而内核线程运行在特权级0
现在我们来实现核心1,它包含两个方面,A、一个管理自己虚拟地址空间的地址池;B、一个自己独立的页表
现在实现上述核心1的A方面。这就意味着每个进程肯定要有个内存池结构体来管理这个虚拟地址空间,所以修改 thread/thread.h 中的task_struct结构体,增加虚拟内存池结构体,来管理自己的虚拟地址空间
#include "memory.h"
struct task_struct {
uint32_t* self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; //用于存储自己的线程的名字
uint8_t ticks; //线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; //此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; //general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; //all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
uint32_t stack_magic; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
增加了这个内存池结构体,自然就意味着我们创建进程的时候我们要初始化它,由于我们创建进程大部分工作与创建线程重合,所以就不修改创建线程的函数也就是增加初始化内存池结构体的代码,因为线程没有自己的虚拟地址空间。所以我们单独写个初始化虚拟内存池结构体的函数
userprog/process.c
#include "process.h"
#include "thread.h"
#include "global.h" //定义了PG_SIZE
#include "memory.h"
#include "bitmap.h"
//用于初始化进程pcb中的用于管理自己虚拟地址空间的虚拟内存池结构体
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE); //计算出管理用于进程那么大的虚拟地址的
//位图需要多少页的空间来存储(向上取整结果)
user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt); //申请位图空间
user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8; //计算出位图长度(字节单位)
bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap); //初始化位图
}
支持代码 userprog/process.h
#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H
#include "thread.h"
void create_user_vaddr_bitmap(struct task_struct* user_prog);
#define USER_VADDR_START 0x8048000 //linux下大部分可执行程序的入口地址(虚拟)都是这个附近,我们也仿照这个设定
#endif
12345678
支持代码 kernel/global.h
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP)) //用于向上取整的宏,如9/10=1
1
接着,我们实现上述核心1的B方面。即独立的页表
userprog/process.c
#include "string.h"
#include "console.h"
//用于为进程创建页目录表,并初始化(系统映射+页目录表最后一项是自己的物理地址,以此来动态操作页目录表),成功后,返回页目录表虚拟地址,失败返回空地址
uint32_t* create_page_dir(void) {
uint32_t* page_dir_vaddr = get_kernel_pages(1); //用户进程的页表不能让用户直接访问到,所以在内核空间来申请
if (page_dir_vaddr == NULL) {
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}
//将内核页目录表的768号项到1022号项复制过来
memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 768*4), (uint32_t*)(0xfffff000 + 768 * 4), 255 * 4);
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr); //将进程的页目录表的虚拟地址,转换成物理地址
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1; //页目录表最后一项填自己的地址,为的是动态操作页表
return page_dir_vaddr;
}
函数声明 userprog/process.h
uint32_t* create_page_dir(void);
支持代码 kernel/memory.c 物理内存池结构体增加锁,为的是互斥访问物理内存池,增加函数函数addr_v_to_p与get_user_pages,其余的函数只是增加了锁相关的操作
#include "sync.h"
/* 核心数据结构,物理内存池, 生成两个实例用于管理内核物理内存池和用户物理内存池 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
struct lock lock; // 申请内存时互斥
};
//初始化内核物理内存池与用户物理内存池
static void mem_pool_init(uint32_t all_mem) {
put_str(" mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256; // 页表大小= 1页的页目录表+第0和第768个页目录项指向同一个页表+
// 第769~1022个页目录项共指向254个页表,共256个页表
uint32_t used_mem = page_table_size + 0x100000; // 已使用内存 = 1MB + 256个页表
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; //将所有可用内存转换为页的数量,内存分配以页为单位,丢掉的内存不考虑
uint16_t kernel_free_pages = all_free_pages / 2; //可用内存是用户与内核各一半,所以分到的页自然也是一半
uint16_t user_free_pages = all_free_pages - kernel_free_pages; //用于存储用户空间分到的页
/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
uint32_t kbm_length = kernel_free_pages / 8; // 内核物理内存池的位图长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8; // 用户物理内存池的位图长度.
uint32_t kp_start = used_mem; // Kernel Pool start,内核使用的物理内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // User Pool start,用户使用的物理内存池的起始地址
kernel_pool.phy_addr_start = kp_start; //赋值给内核使用的物理内存池的起始地址
user_pool.phy_addr_start = up_start; //赋值给用户使用的物理内存池的起始地址
kernel_pool.pool_size = kernel_free_pages * PG_SIZE; //赋值给内核使用的物理内存池的总大小
user_pool.pool_size = user_free_pages * PG_SIZE; //赋值给用户使用的物理内存池的总大小
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length; //赋值给管理内核使用的物理内存池的位图长度
user_pool.pool_bitmap.btmp_bytes_len = ubm_length; //赋值给管理用户使用的物理内存池的位图长度
/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节。
* 所以改为指定一块内存来生成位图.
* ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE; //管理内核使用的物理内存池的位图起始地址
/* 用户内存池的位图紧跟在内核内存池位图之后 */
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length); //管理用户使用的物理内存池的位图起始地址
/******************** 输出内存池信息 **********************/
put_str(" kernel_pool_bitmap_start:");put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");put_int(user_pool.phy_addr_start);
put_str("\n");
/* 将位图置0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
lock_init(&kernel_pool.lock);
lock_init(&user_pool.lock);
/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length; // 赋值给管理内核可以动态使用的虚拟地址池(堆区)的位图长度,
//其大小与管理内核可使用的物理内存池位图长度相同,因为虚拟内存最终都要转换为真实的物理内存,可用虚拟内存大小超过可用物理内存大小在
//我们这个简单操作系统无意义(现代操作系统中有意义,因为我们可以把真实物理内存不断换出,回收,来让可用物理内存变相变大)
/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length); //赋值给管理内核可以动态使用的虚拟内存池(堆区)的位图起始地址
kernel_vaddr.vaddr_start = K_HEAP_START; //赋值给内核可以动态使用的虚拟地址空间的起始地址
bitmap_init(&kernel_vaddr.vaddr_bitmap); //初始化管理内核可以动态使用的虚拟地址池的位图
put_str(" mem_pool_init done\n");
}
/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt) {
lock_acquire(&kernel_pool.lock);
void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回
memset(vaddr, 0, pg_cnt * PG_SIZE);
}
lock_release(&kernel_pool.lock);
return vaddr;
}
/* 在用户空间中申请4k内存,并返回其虚拟地址 */
void* get_user_pages(uint32_t pg_cnt) {
lock_acquire(&user_pool.lock);
void* vaddr = malloc_page(PF_USER, pg_cnt);
memset(vaddr, 0, pg_cnt * PG_SIZE);
lock_release(&user_pool.lock);
return vaddr;
}
//将虚拟地址转换成真实的物理地址
uint32_t addr_v2p(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr); //将虚拟地址转换成页表对应的页表项的地址
return ((*pte & 0xfffff000) + (vaddr & 0x00000fff)); //(*pte)的值是页表所在的物理页框地址,去掉其低12位的页表项属性+虚拟地址vaddr的低12位
}
函数声明 kernel/memory.h
uint32_t addr_v2p(uint32_t vaddr);
void* get_user_pages(uint32_t pg_cnt);
现在来完成核心2,从特权级0进入特权级3
特权级从0到3的途径之一是中断返回:执行iret时,此时cs中加载的是用户程序代码段的选择子(RPL = 3)。所以完成特权级的切换核心就是围绕在中断返回时让CS是用户程序的代码段选择子。
我们先要初始化中断栈的数据,为第一次启动进程做准备。由于我们的进程是基于线程,所以我们新加入的函数模块最好不要修改原有的线程执行逻辑。在原有内核线程中,我们是通过switch_to的ret指令进入了kernel_thread这个线程启动器,由线程启动器去执行我们真正的内核线程。
所以,我们在这个基础上修改,让这个线程启动器kernel_thread进入中断栈初始化函数,也就是我们到时候送入kernel_thread的参数不是我们要执行的进程地址,而是中断栈初始化函数start_process。也就是在ret基础上加上了iret。
userprog/process.c
extern void intr_exit(void);
//用于初始化进入进程所需要的中断栈中的信息,传入参数是实际要运行的函数地址(进程),这个函数是用线程启动器进入的(kernel_thread)
void start_process(void* filename_) {
void* function = filename_;
struct task_struct* cur = running_thread();
cur->self_kstack += sizeof(struct thread_stack); //当我们进入到这里的时候,cur->self_kstack指向thread_stack的起始地址,跳过这里,才能设置intr_stack
struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
proc_stack->gs = 0; //用户态根本用不上这个,所以置为0(gs我们一般用于访问显存段,这个让内核态来访问)
proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; //设定要执行的函数(进程)的地址
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); //设置用户态下的eflages的相关字段
//下面这一句是在初始化中断栈中的栈顶位置,我们先为虚拟地址0xc0000000 - 0x1000申请了个物理页,然后将虚拟地址+4096置为栈顶
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;
proc_stack->ss = SELECTOR_U_DATA;
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}
支持代码 userprog/process.h
#define USER_STACK3_VADDR (0xc0000000 - 0x1000) //定义了一页C语言程序的栈顶起始地址(虚拟),书p511
void start_process(void* filename_);
void intr_init(void* func);
支持代码 kernel/global.h
//定义eflages寄存器用的一些字段,含义见书p511
#define EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if为1,开中断
#define EFLAGS_IF_0 0 // if为0,关中断
#define EFLAGS_IOPL_3 (3 << 12) // IOPL3,用于测试用户程序在非系统调用下进行IO
#define EFLAGS_IOPL_0 (0 << 12) // IOPL0
支持代码 kernel/memory.c
#include "thread.h"
//用于为指定的虚拟地址申请一个物理页,传入参数是这个虚拟地址,要申请的物理页所在的地址池的标志。申请失败,返回null
void* get_a_page(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
struct task_struct* cur = running_thread();
int32_t bit_idx = -1;
/* 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图 */
if (cur->pgdir != NULL && pf == PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
else if (cur->pgdir == NULL && pf == PF_KERNEL){
/* 如果是内核线程申请内核内存,就修改kernel_vaddr. */
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
}
else {
PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");
}
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL)
return NULL;
page_table_add((void*)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void*)vaddr;
}
kernel/memory.h 函数声明
void* get_a_page(enum pool_flags pf, uint32_t vaddr);
现在还没有完成所有的工作,上述工作仅仅是初始化或者叫创建一个进程。当进程由由于主进程的时间片到期而调度上机时。A、需要切换到进程自己的页表,这个还没有做到。B、中断退出进入进程执行,但是当进程执行过程中,由于时钟中断发生,需要从TSS中取出进程0级的ss与esp,才能顺利切换到内核栈中,所以,我们需要修改schedule函数,将进程的内核栈的esp0保存到TSS中。
userprog/process.c
#include "tss.h"
#include "debug.h"
/* 激活页表 */
void page_dir_activate(struct task_struct* p_thread) {
/********************************************************
* 执行此函数时,当前任务可能是线程。
* 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程,
* 否则不恢复页表的话,线程就会使用进程的页表了。
********************************************************/
/* 若为内核线程,需要重新填充页表为0x100000 */
uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表
if (p_thread->pgdir != NULL) { //如果不为空,说明要调度的是个进程,那么就要执行加载页表,所以先得到进程页目录表的物理地址
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);
}
asm volatile ("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory"); //更新页目录寄存器cr3,使新页表生效
}
//用于加载进程自己的页目录表,同时更新进程自己的0特权级esp0到TSS中
void process_activate(struct task_struct* p_thread) {
ASSERT(p_thread != NULL);
/* 激活该进程或线程的页表 */
page_dir_activate(p_thread);
/* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
if (p_thread->pgdir)
update_tss_esp(p_thread); /* 更新该进程的esp0,用于此进程被中断时保留上下文 */
}
支持代码 userprog/process.h
void page_dir_activate(struct task_struct* p_thread);
void process_activate(struct task_struct* p_thread);
在 thread/thread.c中的schedule使用上面的process_activate函数
#include "process.h"
/* 实现任务调度 */
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
}
else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
process_activate(next); //激活任务页表
switch_to(cur, next);
}
接下来,我们进行封装,将之前写的,封装成一个用于创建进程的函数
userprog/process.c
#include "interrupt.h"
//用于创建进程,参数是进程要执行的函数与他的名字
void process_execute(void* filename, char* name) {
/* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread, name, default_prio);
create_user_vaddr_bitmap(thread);
thread_create(thread, start_process, filename);
thread->pgdir = create_page_dir();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
intr_set_status(old_status);
}
支持代码, userprog/process.h
#define default_prio 31 //定义默认的优先级
struct list thread_ready_list; //线程就绪队列
struct list thread_all_list; //线程全部队列
void process_execute(void* func, char* name);
支持代码, thread/thread.h
extern struct list thread_ready_list;
extern struct list thread_all_list;
测试函数 main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");
intr_enable();
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
while(1) {
console_put_str(" v_a:0x");
console_put_int(test_var_a);
}
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
while(1) {
console_put_str(" v_b:0x");
console_put_int(test_var_b);
}
}
/* 测试用户进程 */
void u_prog_a(void) {
while(1) {
test_var_a++;
}
}
/* 测试用户进程 */
void u_prog_b(void) {
while(1) {
test_var_b++;
}
}
我们这里做一个总结,归纳进程是怎么创建与被调度切换的。
如同内核线程一样,进程需要有一个自己的task_struct结构体(内核空间中),这个结构体中存着进程自己的管理信息,相比于内核线程,进程的task_struct中多出了至关重要的虚拟内存池结构体用于管理进程自己的虚拟地址空间(这个结构体与它的位图都在内核空间中),以及记录自己的页目录表位置的变量(创建的页目录表与页表均放在内核空间中),这个就体现了为什么进程有自己独立的虚拟地址空间。
switch_to的ret进入的start_process函数准备进程的中断栈中的内容,通过iret去真正进入进程要执行的函数(作为对比,内核线程是switch_to中的ret进入线程启动器直接执行函数,相当于进程在线程的基础上多了iret),所以我们只要在中断栈中准备好iret返回的信息就行了,中断栈里面的段寄存器选择子字段全是DPL = 3,所以iret之后,就进入了用户态。而且中断栈中要设定好用户栈的栈顶位置(这个栈空间就要在用户空间中)。
切换到进程执行前,我们要去TSS中,设定好下次进程进入中断时用到的内核栈的栈顶。
当进程运行的好好的,发生中断后(如时钟中断),CPU会自动从TSS中取出ss0与esp0,然后将进程在用户态运行的信息保存在取出的ss0:esp0指向的内核栈中(相当于内核栈中存着用户栈的栈顶位置)。假设此时发生切换,那么内核栈的栈顶位置将会保存在task_struct结构体中与TSS中。当下次被切换上CPU时,从task_struct中取出内核栈的栈顶位置,然后从中弹出用户栈的栈顶位置与其他执行环境,最后iret发返回(时钟中断的)回到用户态继续执行。
进一步完善我们的内核
为了方便在操作系统上的应用程序开发,操作系统必须提供一系列接口供程序调用,这就是我们所说的系统调用。系统调用允许用户程序请求操作系统的服务。现在主流操作系统实现系统调用的方法都是利用中断机制,所以在实现系统调用之前,我们需要复习一下这个操作系统上中断机制的运作流程:
- 当外部设备有事件发生时,会通过中断代理向CPU发送一个带有号码的中断信号;
- CPU会根据传入的中断号码并结合IDTR寄存器中指向的IDT表找到对应这个中断号码的中断门描述符;
- 从上一步的中断门描述符中取出CS选择子与IP,然后跳转到这个CS:IP对应的中断处理程序执行。
- 然后,上述的第3步中跳转的是汇编语言编写的中断处理程序,它会负责保存现场(保存用户态的上下文到内核栈中),并跳转到用C语言编写的中断处理程序。
在x86体系的Linux中,系统调用是通过将要执行的系统调用号码放入EAX寄存器,然后主动调用INT 0x80软中断实现的。由于系统调用机制是基于中断机制,所以系统调用的流程和中断流程非常类似:
- 程序用int 0x80触发软中断
- CPU结合IDTR寄存器指向的IDT表找到0x80对应的中断门描述符;
- 从上一步的中断门描述符中取出CS选择子与IP,然后跳转到这个CS:IP对应的中断处理程序执行;
- 中断处理程序根据EAX中存放的值,去调用对应的系统调用函数。中断处理程序也是汇编编写的,它将负责保存现场(保存用户态的上下文到内核栈中)并根据EAX寄存器中的系统调用号码,跳转到对应的C语言编写的系统调用函数中去执行。
所以,我们现在来依据这个基于中断机制的系统调用流程来实现系统调用机制:
首先,我们来准备用户程序的系统调用入口,也就是用于触发int 0x80的程序。我们定义4个用户系统调用程序入口,用于不同的系统调用参数数量场景。寄存器参数传递规则参考linux 实现方法
#include "syscall.h"
/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER) \
: "memory" \
); \
retval; \
})
/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG1) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1) \
: "memory" \
); \
retval; \
})
/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG1, ARG2) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2) \
: "memory" \
); \
retval; \
})
/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3) \
: "memory" \
); \
retval; \
})
然后,我们准备0x80软中断对应的中断门描述符, 修改 (kernel/interrput.c)
#define IDT_DESC_CNT 0x81 // 目前总共支持的中断数,最后一个支持的中断号0x80 + 1
extern uint32_t syscall_handler(void); //定义的汇编中断处理程序代码
//此函数用来循环调用make_idt_desc函数来完成中断门描述符与中断处理函数映射关系的建立,传入三个参数:中断描述符表某个中段描述符(一个结构体)的地址
//属性字段,中断处理函数的地址
static void idt_desc_init(void) {
int i, lastindex = IDT_DESC_CNT - 1;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
//单独处理系统调用,系统调用对应的中断门dpl为3,中断处理程序为汇编的syscall_handler
make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);
put_str(" idt_desc_init done\n");
}
接下来,我们定义汇编版的系统调用处理函数 (kernel/kernel.S)
;;;;;;;;;;;;;;;; 0x80号中断 ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table ;如同之前我们中断处理机制中引入了C中定义的中断处理程序入口地址表一样,这里引入了C中定义的系统调用函数入口地址表
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境,为了复用之前写好的intr_exit:,所以我们仿照中断处理机制压入的东西,构建系统调用压入的东西
push 0 ; 压入0, 使栈中格式统一
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式
;2 为系统调用子功能传入参数,由于这个函数是3个参数的用户程序系统调用入口都会使用
; 所以我们为了格式统一,直接按照最高参数数量压入3个参数
push edx ; 系统调用中第3个参数
push ecx ; 系统调用中第2个参数
push ebx ; 系统调用中第1个参数
;3 调用c中定义的功能处理函数
call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数
add esp, 12 ; 跨过上面的三个参数
;4 将call调用后的返回值存入待当前内核栈中eax的位置,c语言会自动把返回值放入eax中(c语言的ABI规定)
mov [esp + 8*4], eax
jmp intr_exit ; intr_exit返回,恢复上下文
然后,我们实现类似于中断机制中那种汇编代码跳入c中断处理程序的机制,这样我们就能用c管理系统调用 (userprog/syscall-init.c)
#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr];
至此,我们的系统调用机制就已经构建完成,以后我们只需要将c写好的系统调用函数地址放入这个数组就行了
现在,我们来为我们的系统增加第一个系统调用sys_get_pid
用于获得进程或线程的进程号,其实它就是将进程/线程pcb中的pid值返回回来
首先,我们先在进程/线程的pcb中添加pid成员,不然都没有pid这个成员,sys_get_pid
返回啥呢 修改(thread/thread.h)
typedef uint16_t pid_t;
struct task_struct {
uint32_t* self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
pid_t pid;
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; //用于存储自己的线程的名字
uint8_t ticks; //线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; //此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; //general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; //all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
uint32_t stack_magic; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
pcb有了这个pid这个成员,那么自然我们创建进程/线程的时候要去为这个成员赋值 修改(thread/thread.c)
#include "sync.h"
struct lock pid_lock; // 分配pid锁
/* 分配pid */
static pid_t allocate_pid(void) {
static pid_t next_pid = 0;
lock_acquire(&pid_lock);
next_pid++;
lock_release(&pid_lock);
return next_pid;
}
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread)); //把pcb初始化为0
pthread->pid = allocate_pid();
strcpy(pthread->name, name); //将传入的线程的名字填入线程的pcb中
if(pthread == main_thread){
pthread->status = TASK_RUNNING; //由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
}
else{
pthread->status = TASK_READY;
}
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; //线程没有自己的地址空间,进程的pcb这一项才有用,指向自己的页表虚拟地址
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
pthread->stack_magic = 0x19870916; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 将当前main函数创建为线程 */
make_main_thread();
put_str("thread_init done\n");
}
现在,我们来写这个sys_get_pid
函数,并将这个函数地址放入我们的系统调用表syscall_table中 修改(userprog/syscall-init.c)
#include "syscall-init.h"
#include "syscall.h"
#include "stdint.h"
#include "print.h"
#include "thread.h"
/* 返回当前任务的pid */
uint32_t sys_getpid(void) {
return running_thread()->pid;
}
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
put_str("syscall_init done\n");
}
函数声明 (userprog/syscall-init.h)
#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H
#include "stdint.h"
void syscall_init(void);
uint32_t sys_getpid(void);
#endif
支持代码 (user/syscall.h)
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {
SYS_GETPID
};
#endif
然后,在init_all
中调用syscall_init
完成系统调用处理函数的安装 修改(kernel/init.c)
#include "syscall-init.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化PIT
console_init(); // 控制台初始化最好放在开中断之前
keyboard_init(); // 键盘初始化
tss_init(); // tss初始化
syscall_init(); // 初始化系统调用
}
最后,我们写一个用户程序进行系统调用的入口 (lib/user/syscall.c)
/* 返回当前任务pid */
uint32_t getpid() {
return _syscall0(SYS_GETPID);
}
然后函数声明 (user/syscall.h)
uint32_t getpid(void);
一定要区分作为实际系统调用处理函数的sys_getpid
与作为用户程序入口的getpid
,前者是运行在内核态的,后者是用户态程序的入口去执行int 0x80的。
测试代码 (kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int prog_a_pid = 0, prog_b_pid = 0;
int main(void) {
put_str("I am kernel\n");
init_all();
process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");
intr_enable();
console_put_str(" main_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
console_put_str(" thread_a_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
console_put_str(" prog_a_pid:0x");
console_put_int(prog_a_pid);
console_put_char('\n');
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
console_put_str(" thread_b_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
console_put_str(" prog_b_pid:0x");
console_put_int(prog_b_pid);
console_put_char('\n');
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
prog_a_pid = getpid();
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
prog_b_pid = getpid();
while(1);
}
之前,我们是用寄存器来进行参数传递,其实,我们也能用栈来进行参数传递。原理就是因为系统调用机制基于中断机制,当通过用户程序入口进行系统调用时,用户程序入口会将系统调用号与参数压入用户栈中,然后触发int 0x80软中断,此时特权级切换,cpu会自动在内核栈中压入用户栈的位置,如果我们从内核栈中获取到这个用户栈的位置,那么自然就能获得系统调用号与参数,然后调用对应的系统调用处理函数。
请注意,这个实验只是为你展示可以用栈传递参数,并不作为后续开发的基础!
修改(user/syscall.c)
/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({ \
int retval; \
asm volatile ( \
"pushl %[number]; int $0x80; addl $4, %%esp" \
: "=a" (retval) \
: [number] "i" (NUMBER) \
: "memory" \
); \
retval; \
})
/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG0) ({ \
int retval; \
asm volatile ( \
"pushl %[arg0]; pushl %[number]; int $0x80; addl $8, %%esp" \
: "=a" (retval) \
: [number] "i" (NUMBER), [arg0] "g" (ARG0) \
: "memory" \
); \
retval; \
})
/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG0, ARG1) ({ \
int retval; \
asm volatile ( \
"pushl %[arg1]; pushl %[arg0]; " \
"pushl %[number]; int $0x80; addl $12, %%esp" \
: "=a" (retval) \
: [number] "i" (NUMBER), \
[arg0] "g" (ARG0), \
[arg1] "g" (ARG1) \
: "memory" \
); \
retval; \
})
/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG0, ARG1, ARG2) ({ \
int retval; \
asm volatile ( \
"pushl %[arg2]; pushl %[arg1]; pushl %[arg0]; " \
"pushl %[number]; int $0x80; addl $16, %%esp" \
: "=a" (retval) \
: [number] "i" (NUMBER), \
[arg0] "g" (ARG0), \
[arg1] "g" (ARG1), \
[arg2] "g" (ARG2) \
: "memory" \
); \
retval; \
})
修改(kernel/kernel.S)
;;;;;;;;;;;;;;;; 0x80号中断 ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table ;如同之前我们中断处理机制中引入了C中定义的中断处理程序入口地址表一样,这里引入了C中定义的系统调用函数入口地址表
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境,为了复用之前写好的intr_exit:,所以我们仿照中断处理机制压入的东西,构建系统调用压入的东西
push 0 ; 压入0, 使栈中格式统一
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式
;2 从内核栈中获取cpu自动压入的用户栈指针esp的值
mov ebx, [esp + 4 + 48 + 4 + 12]
; 为系统调用子功能传入参数,由于这个函数是3个参数的用户程序系统调用入口都会使用
; 所以我们为了格式统一,直接按照最高参数数量压入3个参数, 此时ebx是用户栈指针
push dword [ebx + 12] ; 系统调用中第3个参数
push dword [ebx + 8] ; 系统调用中第2个参数
push dword [ebx + 4] ; 系统调用中第1个参数
mov edx, [ebx] ; 系统调用的子功能号
;3 调用c中定义的功能处理函数
call [syscall_table + edx*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数
add esp, 12 ; 跨过上面的三个参数
;4 将call调用后的返回值存入待当前内核栈中eax的位置,c语言会自动把返回值放入eax中(c语言的ABI规定)
mov [esp + 8*4], eax
jmp intr_exit ; intr_exit返回,恢复上下文
修改kernel.S中的核心是获取到用户栈的栈顶位置
mov ebx, [esp + 4 + 48 + 4 + 12]
此时内核栈的栈顶位置 + 4 是跳过了压入的0x80,+ 48 是跳过 push ad 与 gs fs es ds,+ 4 是跳过了 push 0,最后 + 12是因为用户程序调用int 0x80触发软中断,然后导致特权级切换,cpu会自动向内核栈中按照顺序压入此时的用户程序执行时的ss, esp, eflag, cs, eip,跳过12字节(ss = 2, esp = 4, eflag = 4, cs = 2)就是此时用户栈的栈顶位置eip
之前,我们一直用的是put_str, put_ch, put_int来用于打印,这些函数只能用于内核态。现在我们要实现用户态打印功能,也就是像c语言中的printf一样。c语言的printf是libc库提供的,它调用了很多系统调用来实现功能,其一是write系统调用,如果我们写一个简单的打印hello的c函数,然后编译,用strace命令去追踪这个编译好的可执行二进制文件,如strace ./hello.bin
我们能清楚看见调用了write系统调用,这个系统调用在此处发挥的功能就是向标准输出文件(也就是我们的控制台shell,控制台在Linux中抽象成了一个文件,叫标准输出)写入hello。
我们printf也是仿照这个,最终是调用write系统调用来实现,但是由于我们现在没有实现文件系统,所以我们的write是一个简易版本的。现在我们来增加这个write系统调用。首先,我们来增加write系统调用号。
修改(lib/users/syscall.h)
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE
};
然后,我们实现write系统调用的用户程序入口。
修改(lib/user/syscall.c)
/* 打印字符串str */
uint32_t write(char* str) {
return _syscall1(SYS_WRITE, str);
}
之后,我们声明write系统调用的用户程序入口。
修改(lib/users/syscall.h)
uint32_t write(char* str);
现在我们已经实现了write系统调用的用户程序入口,现在来实现真正的系统调用执行函数。并将其添加进入系统调用表中。
修改(userprog/syscall-init.c)
#include "console.h"
#include "string.h"
/* 打印字符串str(未实现文件系统前的版本) */
uint32_t sys_write(char* str) {
console_put_str(str);
return strlen(str);
}
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
put_str("syscall_init done\n");
}
然后声明这个系统调用函数
修改(userprog/syscall-init.h)
uint32_t sys_write(char* str);
由于现在我们开启了页表机制,任何地址都将视作虚拟地址。我们之前编写print.S时,由于是给内核用的,所以用于与显存段打交道的地址有些是借助于内核页目录表0号项进行寻址的,现在我们将print共享给了用户进程,而用户进程无法去访问内核页目录表0号项。但是由于进程页目录表768号项与内核页目录表0号项指向同一张内核的页表(因为进程页目录表768号项就是拷贝的内核页目录表768号项)。所以我们能通过进程页目录表768号项访问原来通过内核页目录表0号项访问的地址。所以,我们需要修改print.S中的一些地址访问,将其升高3G,这样才能让原本通过内核页目录表0号项访问的地址,现在能通过进程页目录表768号项访问。
修改(lib/kernel/print.S)
.roll_screen: ; 若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次
;mov esi, 0xb80a0
mov esi, 0xc00b80a0 ; 第1行行首
;mov edi, 0xb8000
mov edi, 0xc00b8000 ; 第0行行首
rep movsd ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw
现在我们就完整实现了系统调用write用于输出文字。你可以在用户进程中尝试用write进行输出。
我们的write只支持:字符串输出(因为底层实现是用put_str)与一个字符串地址参数;而c语言中的printf不仅能处理字符串,还能处理数字、字符、地址等。而且参数可以无限多,如下:
printf("a = %x, b = %c, c = %p, d = %s", a, b, c, d);
所以printf绝不是简单调用write,而是增加了:1、将多种格式转换成字符串供write打印。比如上述例子中,我们将a的值直接拿去替换第一个%x,b的值去替换第二个%c…然后将整个字符串打印出来;2、支持可变参数 ,也就是不限制参数数量(一般来说,我们写一个函数,然后建立函数声明必须指定参数数量与类型,这样编译器才知道给函数开辟多大的栈空间。可变参数原理见书p536)。
可变参数依靠的是编译器的特性,其原理的核心就是,调用者依据c调用约定,从右到左依次向栈中压入参数,而被调用者是能够依据栈中的数据来找到传入的参数。就用上述printf来举例子,当一个函数调用printf,被调者的栈是如下分布:
200 d 字符串的地址 高地址
196 c 地址值
195 b 字符的ascii码值
191 a 32位值
187 "a = %x, b = %c, c = %p, d = %s" 字符串地址
183 eip 返回地址 低地址
123456
只要我们知道第一个参数的位置,也就是那个字符串地址,然后知道每个参数类型(知道参数类型才知道参数在栈中的大小,才能通过移动指针的方式找到参数),就能找到之后所有的参数。而这两个都不难。
现在来编写printf函数,但是由于这个函数功能非常多,所以我们现在只增加支持16进制输出。
(myso/lib/stdio.c)
#include "stdio.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "syscall.h"
#define va_start(ap, v) ap = (va_list)&v // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4)) // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL // 清除ap
/* 将整型转换成字符(integer to ascii) */
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
uint32_t m = value % base; // 求模,最先掉下来的是最低位
uint32_t i = value / base; // 取整
if (i) { // 如果倍数不为0则递归调用。
itoa(i, buf_ptr_addr, base);
}
if (m < 10) { // 如果余数是0~9
*((*buf_ptr_addr)++) = m + '0'; // 将数字0~9转换为字符'0'~'9'
}
else { // 否则余数是A~F
*((*buf_ptr_addr)++) = m - 10 + 'A'; // 将数字A~F转换为字符'A'~'F'
}
}
/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
char* buf_ptr = str;
const char* index_ptr = format;
char index_char = *index_ptr;
int32_t arg_int;
while(index_char) {
if (index_char != '%') {
*(buf_ptr++) = index_char;
index_char = *(++index_ptr);
continue;
}
index_char = *(++index_ptr); // 得到%后面的字符
switch(index_char) {
case 'x':
arg_int = va_arg(ap, int);
itoa(arg_int, &buf_ptr, 16);
index_char = *(++index_ptr); // 跳过格式字符并更新index_char
break;
}
}
return strlen(str);
}
/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
va_list args;
va_start(args, format); // 使args指向format
char buf[1024] = {0}; // 用于存储拼接后的字符串
vsprintf(buf, format, args);
va_end(args);
return write(buf);
}
支持代码(lib/stdio.h)
#ifndef __LIB_STDIO_H
#define __LIB_STDIO_H
#include "stdint.h"
typedef char* va_list;
uint32_t printf(const char* str, ...);
uint32_t vsprintf(char* str, const char* format, va_list ap);
#endif
1234567
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");
intr_enable();
console_put_str(" main_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
console_put_str(" thread_a_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
console_put_str(" thread_b_pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
printf(" prog_a_pid:0x%x\n", getpid());
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
printf(" prog_b_pid:0x%x\n", getpid());
while(1);
}
修改Makefile为新增加的文件增加编译规则
修改(Makefile)
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -fno-stack-protector # 保护栈
接下来,我们完善printf,增加处理%s, %c, %d的功能
修改(lib/stdio.c)
char* arg_str;
/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
char* buf_ptr = str;
const char* index_ptr = format;
char index_char = *index_ptr;
int32_t arg_int;
char* arg_str;
while(index_char) {
if (index_char != '%') {
*(buf_ptr++) = index_char;
index_char = *(++index_ptr);
continue;
}
index_char = *(++index_ptr); // 得到%后面的字符
switch(index_char) {
case 's':
arg_str = va_arg(ap, char*);
strcpy(buf_ptr, arg_str);
buf_ptr += strlen(arg_str);
index_char = *(++index_ptr);
break;
case 'c':
*(buf_ptr++) = va_arg(ap, char);
index_char = *(++index_ptr);
break;
case 'd':
arg_int = va_arg(ap, int);
if (arg_int < 0) {
arg_int = 0 - arg_int; /* 若是负数, 将其转为正数后,再正数前面输出个负号'-'. */
*buf_ptr++ = '-';
}
itoa(arg_int, &buf_ptr, 10);
index_char = *(++index_ptr);
break;
case 'x':
arg_int = va_arg(ap, int);
itoa(arg_int, &buf_ptr, 16);
index_char = *(++index_ptr); // 跳过格式字符并更新index_char
break;
}
}
return strlen(str);
}
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
console_put_str(" I am main, my pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
intr_enable();
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
console_put_str(" I am thread_a, my pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
console_put_str(" I am thread_b, my pid:0x");
console_put_int(sys_getpid());
console_put_char('\n');
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
char* name = "prog_a";
printf(" I am %s, my pid:%d%c", name, getpid(),'\n');
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
char* name = "prog_b";
printf(" I am %s, my pid:%d%c", name, getpid(), '\n');
while(1);
}
在进行接下来的开发之前,我们先填坑。是否记得我们之前实现页分配时,只实现了内核页面的分配,而把用户页面分配代码是空着的!
修改(kernel/memory.c)
/* 在pf表示的虚拟内存池中申请pg_cnt个虚拟页,
* 成功则返回虚拟页的起始地址, 失败则返回NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL) {
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else { // 用户内存池
struct task_struct* cur = running_thread();
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
/* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */
ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
}
return (void*)vaddr_start;
}
之前,我们的内存管理是:
只有分配没有释放;
以页为单位;
只能内核态使用;
现在我们完善内存管理:
实现释放机制;
实现释放机制;
用户态也可以使用;
释放机制的实现很简单,是分配机制的逆操作;
更细粒度的内存管理单位,需要依靠arena模型的理解与实现。在这个模型中,我们先申请一个完整的4KB页面,然后将这个4KB页面划分成不同的小块,如256个16B小块、8个512B的小块,然后这些独立的小块就成了分配与释放的基本单位;用户态使用,直接使用系统调用机制。首先,我们来进行底层数据结构的建立:
修改(kernel/memory.h)
#include "list.h"
/* 内存块 */
struct mem_block {
struct list_elem free_elem;
};
/* 内存块描述符 */
struct mem_block_desc {
uint32_t block_size; // 内存块大小
uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.
struct list free_list; // 目前可用的mem_block链表
};
#define DESC_CNT 7 // 内存块描述符个数
修改(kernel/memory.c)
/* 内存仓库arena元信息 */
struct arena {
struct mem_block_desc* desc; // 此arena关联的mem_block_desc
uint32_t cnt;
bool large; /* large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量 */
};
struct mem_block_desc
、struct mem_block
、struct arena
的关系:
struct mem_block_desc
描述了不同类型的小块,比如刚刚那个例子:4KB页面划分成不同的小块,如256个16B小块、8个512B的小块。512B的小块对应一个mem_block_desc
,而16B的小块对应另一个。block_size
就是记录这个mem_block_desc
用于描述哪种大小的小内存块,比如512或者16。blocks_per_arena
用于记录一个页面拆分成了多少个小块,比如8个或者256个。free_list
用于管理可以分配的小块,也就是用于将可以分配的小块形成链表。
struct mem_block
其实本意是用来描述这个由4KB页面二次划分而成的固定小块,但是作者为了实现更通用的管理逻辑,所以这个结构体里面只包含了一个用于管理这个空闲小块的链表节点。
struct arena
用于描述这个arena,desc
用于指向这个管理这种arena的mem_block_desc结构体,cnt的值意义取决于large的值,如果large = true,那么表示本arena占用的页框数目,否则表示本arena中还有多少空闲小内存块可用。需要注意的是,一个mem_block_desc对应的arena数量可不止一个,其实很好理解,当一个arena的小内存块分配完毕,我们就要再分配一个新的页充当arena然后划分成固定大小的小块。然后初始化管理内核不同种类型arena的不同mem_block_desc
修改(kernel/memory.c)
struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组
//初始化管理不同种类型arena的不同mem_block_desc
void block_desc_init(struct mem_block_desc* desc_array) {
uint16_t desc_idx, block_size = 16;
for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
desc_array[desc_idx].block_size = block_size;
desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
list_init(&desc_array[desc_idx].free_list);
block_size *= 2; // 更新为下一个规格内存块
}
}
/* 内存管理部分初始化入口 */
void mem_init() {
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total); // 初始化内存池
block_desc_init(k_block_descs);
put_str("mem_init done\n");
}
添加函数声明,修改(kernel/memory.h)
void block_desc_init(struct mem_block_desc* desc_array);
内核有了管理不同类型arena的mem_block_desck数组。我们说进程是独立分配资源的单位,进程拥有自己的独立虚拟地址空间,那么进程也应该拥有管理自己不同类型arena的mem_block_desc数组。这样进程分配内存,就去进程自己的mem_block_desc数组中找对应的mem_block_desc,然后通过free_list找到空闲小块就行了。
修改(thread/thread.h)为task_struct添加u_block_desc
,这样每个task_struct都有了这个mem_block_desc数组
struct task_struct {
uint32_t* self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
pid_t pid;
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; //用于存储自己的线程的名字
uint8_t ticks; //线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; //此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; //general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; //all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
uint32_t stack_magic; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
但是,我们只初始化进程的mem_block_desc数组
修改(userprog/process.c)
//用于创建进程,参数是进程要执行的函数与他的名字
void process_execute(void* filename, char* name) {
/* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread, name, default_prio);
create_user_vaddr_bitmap(thread);
thread_create(thread, start_process, filename);
thread->pgdir = create_page_dir();
block_desc_init(thread->u_block_desc);
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
intr_set_status(old_status);
}
现在我们来编写能够与arena模型配合的sys_malloc
,用于真正进行内存分配
修改(kernel/memory.c)
#include "interrupt.h"
/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}
/* 返回内存块b所在的arena地址 */
static struct arena* block2arena(struct mem_block* b) {
return (struct arena*)((uint32_t)b & 0xfffff000);
}
/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {
enum pool_flags PF;
struct pool* mem_pool;
uint32_t pool_size;
struct mem_block_desc* descs; //用于存储mem_block_desc数组地址
struct task_struct* cur_thread = running_thread();
/* 判断用哪个内存池*/
if (cur_thread->pgdir == NULL) { // 若为内核线程
PF = PF_KERNEL;
pool_size = kernel_pool.pool_size;
mem_pool = &kernel_pool;
descs = k_block_descs;
}
else { // 用户进程pcb中的pgdir会在为其分配页表时创建
PF = PF_USER;
pool_size = user_pool.pool_size;
mem_pool = &user_pool;
descs = cur_thread->u_block_desc;
}
/* 若申请的内存不在内存池容量范围内则直接返回NULL */
if (!(size > 0 && size < pool_size)) {
return NULL;
}
struct arena* a;
struct mem_block* b;
lock_acquire(&mem_pool->lock);
/* 超过最大内存块1024, 就分配页框 */
if (size > 1024) {
uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE); // 向上取整需要的页框数
a = malloc_page(PF, page_cnt);
if (a != NULL) {
memset(a, 0, page_cnt * PG_SIZE); // 将分配的内存清0
/* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
a->desc = NULL;
a->cnt = page_cnt;
a->large = true;
lock_release(&mem_pool->lock);
return (void*)(a + 1); // 跨过arena大小,把剩下的内存返回
}
else {
lock_release(&mem_pool->lock);
return NULL;
}
}
else { // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配
uint8_t desc_idx;
/* 从内存块描述符中匹配合适的内存块规格 */
for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
if (size <= descs[desc_idx].block_size) { // 从小往大后,找到后退出
break;
}
}
/* 若mem_block_desc的free_list中已经没有可用的mem_block,
* 就创建新的arena提供mem_block */
if (list_empty(&descs[desc_idx].free_list)) {
a = malloc_page(PF, 1); // 分配1页框做为arena
if (a == NULL) {
lock_release(&mem_pool->lock);
return NULL;
}
memset(a, 0, PG_SIZE);
/* 对于分配的小块内存,将desc置为相应内存块描述符,
* cnt置为此arena可用的内存块数,large置为false */
a->desc = &descs[desc_idx];
a->large = false;
a->cnt = descs[desc_idx].blocks_per_arena;
uint32_t block_idx;
enum intr_status old_status = intr_disable();
/* 开始将arena拆分成内存块,并添加到内存块描述符的free_list中 */
for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {
b = arena2block(a, block_idx);
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
list_append(&a->desc->free_list, &b->free_elem);
}
intr_set_status(old_status);
}
/* 开始分配内存块 */
b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
memset(b, 0, descs[desc_idx].block_size);
a = block2arena(b); // 获取内存块b所在的arena
a->cnt--; // 将此arena中的空闲内存块数减1
lock_release(&mem_pool->lock);
return (void*)b;
}
}
声明函数,修改(kernel/memory.h)
void* sys_malloc(uint32_t size);
测试代码,修改(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
void* addr = sys_malloc(33);
console_put_str(" I am thread_a, sys_malloc(33), addr is 0x");
console_put_int((int)addr);
console_put_char('\n');
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
void* addr = sys_malloc(63);
console_put_str(" I am thread_b, sys_malloc(63), addr is 0x");
console_put_int((int)addr);
console_put_char('\n');
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
char* name = "prog_a";
printf(" I am %s, my pid:%d%c", name, getpid(),'\n');
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
char* name = "prog_b";
printf(" I am %s, my pid:%d%c", name, getpid(), '\n');
while(1);
}
接下来实现页级别的内存回收,页回收是页分配的逆操作:1、清除物理内存池中位图的位;2、清除虚拟地址对应的页表表项;3、清除虚拟内存池中位图的位;
修改(kernel/memory.c)
//将物理地址pg_phy_addr回收到物理内存池,实质就是清除物理内存池中位图的位
void pfree(uint32_t pg_phy_addr) {
struct pool* mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start) { // 用户物理内存池
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
}
else { // 内核物理内存池
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0); // 将位图中该位清0
}
/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr);
*pte &= ~PG_P_1; // 将页表项pte的P位置0
asm volatile ("invlpg %0"::"m" (vaddr):"memory"); //更新tlb
}
//在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址,实质就是清楚虚拟内存池位图的位
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0;
if (pf == PF_KERNEL) { // 内核虚拟内存池
bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
else { // 用户虚拟内存池
struct task_struct* cur_thread = running_thread();
bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt) {
bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
}
/* 释放以虚拟地址vaddr为起始的cnt个物理页框 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
uint32_t pg_phy_addr;
uint32_t vaddr = (int32_t)_vaddr, page_cnt = 0;
ASSERT(pg_cnt >=1 && vaddr % PG_SIZE == 0);
pg_phy_addr = addr_v2p(vaddr); // 获取虚拟地址vaddr对应的物理地址
/* 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);
/* 判断pg_phy_addr属于用户物理内存池还是内核物理内存池 */
if (pg_phy_addr >= user_pool.phy_addr_start) { // 位于user_pool内存池
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt) {
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
/* 确保物理地址属于用户物理内存池 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);
/* 先将对应的物理页框归还到内存池 */
pfree(pg_phy_addr);
/* 再从页表中清除此虚拟地址所在的页表项pte */
page_table_pte_remove(vaddr);
page_cnt++;
}
/* 清空虚拟地址的位图中的相应位 */
vaddr_remove(pf, _vaddr, pg_cnt);
}
else { // 位于kernel_pool内存池
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt) {
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
/* 确保待释放的物理内存只属于内核物理内存池 */
ASSERT((pg_phy_addr % PG_SIZE) == 0 && \
pg_phy_addr >= kernel_pool.phy_addr_start && \
pg_phy_addr < user_pool.phy_addr_start);
/* 先将对应的物理页框归还到内存池 */
pfree(pg_phy_addr);
/* 再从页表中清除此虚拟地址所在的页表项pte */
page_table_pte_remove(vaddr);
page_cnt++;
}
/* 清空虚拟地址的位图中的相应位 */
vaddr_remove(pf, _vaddr, pg_cnt);
}
}
函数声明:修改(kernel/memory.h)
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt);
void pfree(uint32_t pg_phy_addr);
现在,我们实现与arena模型分配机制对应的回收机制,并将之前的页回收封装进入,直接实现统一的内存回收系统调用sys_free
修改(kernel/memory.c)
/* 回收内存ptr */
void sys_free(void* ptr) {
ASSERT(ptr != NULL);
if (ptr != NULL) {
enum pool_flags PF;
struct pool* mem_pool;
/* 判断是线程还是进程 */
if (running_thread()->pgdir == NULL) {
ASSERT((uint32_t)ptr >= K_HEAP_START);
PF = PF_KERNEL;
mem_pool = &kernel_pool;
}
else {
PF = PF_USER;
mem_pool = &user_pool;
}
lock_acquire(&mem_pool->lock);
struct mem_block* b = ptr;
struct arena* a = block2arena(b); // 把mem_block转换成arena,获取元信息
ASSERT(a->large == 0 || a->large == 1);
if (a->desc == NULL && a->large == true) { // 大于1024的内存
mfree_page(PF, a, a->cnt);
}
else { // 小于等于1024的内存块先将内存块回收到free_list
list_append(&a->desc->free_list, &b->free_elem);
/* 再判断此arena中的内存块是否都是空闲,如果是就释放arena */
if (++a->cnt == a->desc->blocks_per_arena) {
uint32_t block_idx;
for (block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {
struct mem_block* b = arena2block(a, block_idx);
ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
list_remove(&b->free_elem);
}
mfree_page(PF, a, 1);
}
}
lock_release(&mem_pool->lock);
}
}
函数声明,修改(kernel/memory.h)
void sys_free(void* ptr);
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
void* addr1;
void* addr2;
void* addr3;
void* addr4;
void* addr5;
void* addr6;
void* addr7;
console_put_str(" thread_a start\n");
int max = 1000;
while (max-- > 0) {
int size = 128;
addr1 = sys_malloc(size);
size *= 2;
addr2 = sys_malloc(size);
size *= 2;
addr3 = sys_malloc(size);
sys_free(addr1);
addr4 = sys_malloc(size);
size *= 2; size *= 2; size *= 2; size *= 2;
size *= 2; size *= 2; size *= 2;
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
sys_free(addr5);
size *= 2;
addr7 = sys_malloc(size);
sys_free(addr6);
sys_free(addr7);
sys_free(addr2);
sys_free(addr3);
sys_free(addr4);
}
console_put_str(" thread_a end\n");
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
void* addr1;
void* addr2;
void* addr3;
void* addr4;
void* addr5;
void* addr6;
void* addr7;
void* addr8;
void* addr9;
int max = 1000;
console_put_str(" thread_b start\n");
while (max-- > 0) {
int size = 9;
addr1 = sys_malloc(size);
size *= 2;
addr2 = sys_malloc(size);
size *= 2;
sys_free(addr2);
addr3 = sys_malloc(size);
sys_free(addr1);
addr4 = sys_malloc(size);
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
sys_free(addr5);
size *= 2;
addr7 = sys_malloc(size);
sys_free(addr6);
sys_free(addr7);
sys_free(addr3);
sys_free(addr4);
size *= 2; size *= 2; size *= 2;
addr1 = sys_malloc(size);
addr2 = sys_malloc(size);
addr3 = sys_malloc(size);
addr4 = sys_malloc(size);
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
addr7 = sys_malloc(size);
addr8 = sys_malloc(size);
addr9 = sys_malloc(size);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
sys_free(addr4);
sys_free(addr5);
sys_free(addr6);
sys_free(addr7);
sys_free(addr8);
sys_free(addr9);
}
console_put_str(" thread_b end\n");
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
char* name = "prog_a";
printf(" I am %s, my pid:%d%c", name, getpid(),'\n');
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
char* name = "prog_b";
printf(" I am %s, my pid:%d%c", name, getpid(), '\n');
while(1);
}
最后,我们来将sys_malloc
与sys_free
封装成用户程序可用的调用接口
增加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE
};
封装系统调用用户入口,修改(lib/user/syscall.c)
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
return (void*)_syscall1(SYS_MALLOC, size);
}
/* 释放ptr指向的内存 */
void free(void* ptr) {
_syscall1(SYS_FREE, ptr);
}
声明函数,修改(lib/user/syscall.h)
void* malloc(uint32_t size);
void free(void* ptr);
注册系统调用函数
修改(userprog/syscall-init.c)
#include "memory.h"
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
put_str("syscall_init done\n");
}
测试函数(kernel/main.c),相比作者代码,k_thread_a与k_thread_b内的消耗时间变量由100000增加到了9999999,否则不会出现书上结果,因为线程在切换前就已经释放了地址
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 9999999;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 999999;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
本章是编写磁盘驱动,也就是用于与磁盘打交道(比如读、写)的程序。未来我们实现文件系统是放在磁盘上的,所以编写磁盘驱动必不可少。然后在输入框依次输入以下,输入一个,按一次回车
1
hd
flat
80
hd80M.img
接下来,我们在bochsrc.disk文件中,写入
ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63
这样,bochs虚拟机启动时,就会识别这个磁盘并且自动挂载。
现在,我们来为这个新的磁盘进行分区,这是为了之后的文件系统做准备。分区的本质,就是将多个连续的柱面(详见书P123)划分为一个区域。
在bochs目录下,输入:
fdisk ./hd80M.img
来使用fdisk工具来为刚刚创建的磁盘开始进行分区。
然后在输入框依次输入以下,输入一个,按一次回车
m
显示菜单
x
使用额外功能
m
显示菜单
c
设定柱面
162
h
设定磁头数
16
r
返回上一级菜单
n
新增一个分区
p
分区是个主分区
1
分区号设定为1
2048
在旧的fdisk版本中,通常使用”柱面”(Cylinders)、“磁头”(Heads)和”扇区”(Sectors)这些术语来描述磁盘布局。现代磁盘和磁盘管理工具通常直接以扇区为单位进行操作。作者的1分区起始柱面是1,计算得起始扇区应该是63 * 16 = 1008,我们创建hd80M.img时,会提示Creating hard disk image 'hd80M.img' with CHS=162/16/63
,意思就是柱面162个,磁头16个,每个磁道63个扇区。但是由于fdisk工具指定第一个分区最小起始扇区为2048,所以我们只能设定1分区起始扇区为2048
33263
作者指定了1分区结束柱面为32,计算得结束扇区为(32 + 1) 16 63 - 1 = 33263,-1 是因为扇区从0开始编号
n
新增一个分区
e
分区是个扩展分区。最初磁盘只支持最多分4个区。但是后来由于需求增加,磁盘需要支持能分更多区,同时又为了能够兼容旧有的最多支持4个分区,所以就发明了扩展分区。扩展分区并不直接用于存储数据,而是作为一个容器,可以创建多个逻辑分区在其中。这样,即使主分区的数量限制为四个,我们仍然可以在一个扩展分区中创建多个逻辑分区,从而实现对更多分区的需求。一个磁盘,最多一个扩展分区。
4
分区号码设定为4
33264
分区起始扇区设定为分区1结束扇区下一个扇区
163295
分区结束扇区设定为整个磁盘最后一个扇区,也就是说我们这个磁盘就两个分区,一个主分区1,一个扩展分区4
p
查看现有分区
n
创建分区,由于我们现在磁盘已经被两个分区占满了,所以不能再支持新的主分区创建,又由于扩展分区只能有1个,所以不能再支持扩展分区的创建。所以此时输入n,直接是创建逻辑分区(扩展分区再次分区后,每个分区叫做逻辑分区)
35312
直接设定工具允许的逻辑分区起始最小扇区
51407
作者指定了逻辑分区5结束柱面为50,计算得结束扇区为(50 + 1) 16 63 - 1 = 51407,-1 是因为扇区从0开始编号
n
创建分区
53456
直接设定工具允许的逻辑分区起始最小扇区
76607
作者指定了逻辑分区6结束柱面为75,计算得结束扇区为(75 + 1) 16 63 - 1 = 76607,-1 是因为扇区从0开始编号
n
创建分区
78656
直接设定工具允许的逻辑分区起始最小扇区
91727
作者指定了逻辑分区7结束柱面为90,计算得结束扇区为(90 + 1) 16 63 - 1 = 91727,-1 是因为扇区从0开始编号
n
创建分区
93776
直接设定工具允许的逻辑分区起始最小扇区
121967
作者指定了逻辑分区8结束柱面为120,计算得结束扇区为(120 + 1) 16 63 - 1 = 121967,-1 是因为扇区从0开始编号
n
创建分区
124016
直接设定工具允许的逻辑分区起始最小扇区
163295
设定最后一个可用扇区为逻辑分区9的结束扇区
p
显示分区
t
设定分区类型id
5
改变逻辑分区5的类型id
66
设定类型id为0x66
t
设定分区类型id
6
改变逻辑分区6的类型id
66
设定类型id为0x66
t
设定分区类型id
7
改变逻辑分区7的类型id
66
设定类型id为0x66
t
设定分区类型id
8
改变逻辑分区8的类型id
66
设定类型id为0x66
t
设定分区类型id
9
改变逻辑分区9的类型id
66
设定类型id为0x66
p
显示分区
w
将分区表写入磁盘,并退出fdisk 分区表是用于描述每个分区的信息,详见P571
fdisk -l hd80M.img
查看分区信息
至此,我们创建分区就完毕了。我们创建的分区布局如下图:
这里面有几个要强调的点:
1、子拓展分区在逻辑上等同于一整块硬盘,也就是说,我们把总拓展分区划分成了多块硬盘;
2、MBR分区表共有4项,用于标识4个分区的起始扇区与大小,只不过我们只创建了一个分区,另一分区用作总扩展分区。而EBR分区表也有4项,第一项用于表示这个子拓展分区的逻辑分区偏移与大小,第二项用于表示下一个子拓展分区的偏移与大小,第3项与第4项没有使用,所以说一个子拓展分区就一个逻辑分区
在以前,我们内核态下进行打印一直用的console_put_xxx之类的函数,这很不方便,因为我们经常打印信息需要调用console_put_int, console_put_str, console_put_ch这三个函数配合使用。所以我们先来实现一个类似于用户态函数printf的内核态函数printk
(lib/kernel/stdio-kernel.c)
#include "stdio-kernel.h"
#include "stdio.h"
#include "console.h"
#include "global.h"
#define va_start(args, first_fix) args = (va_list)&first_fix
#define va_end(args) args = NULL
/* 供内核使用的格式化输出函数 */
void printk(const char* format, ...) {
va_list args;
va_start(args, format);
char buf[1024] = {0};
vsprintf(buf, format, args);
va_end(args);
console_put_str(buf);
}
函数声明(lib/kernel/stdio-kernel.h)
#ifndef __LIB_KERNEL_STDIOSYS_H
#define __LIB_KERNEL_STDIOSYS_H
void printk(const char* format, ...);
#endif
再实现一个用于将格式化字符串放入缓冲区的函数sprintf,同printf相比,它不将信息打印在屏幕上,而是放入缓冲区中。
修改(lib/stdio.c)
/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {
va_list args;
uint32_t retval;
va_start(args, format);
retval = vsprintf(buf, format, args);
va_end(args);
return retval;
}
添加函数声明,修改(lib/stdio.h)
uint32_t sprintf(char* buf, const char* format, ...);
接下来我们正式编写硬盘驱动
首先,我们需要做的是打开硬盘对应的中断信号,主板上有两个IDE通道,每个通道可以挂载两个硬盘。第一个IDE通道的中断信号通过8259A从片的IRQ14线进入,而第二个IDE通道的中断信号通过8259A从片的IRQ15线进入(虽然下面的图上写的是保留)
由于我们的两个磁盘都是挂在了IDE通道0上,而IDE通道0又是挂在了IRQ14线上,所以我们只需要再打开这条线的中断信号就行
修改(kernel/interrupt.c)
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
outb (PIC_M_DATA, 0xf8); //IRQ2用于级联从片,必须打开,否则无法响应从片上的中断主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭
outb (PIC_S_DATA, 0xbf); //打开从片上的IRQ14,此引脚接收硬盘控制器的中断
put_str(" pic_init done\n");
}
硬盘是真实存在的硬件,我们要想在软件中管理它们,只能从逻辑上抓住这些硬件的特性,将它们抽象成一些数据结构,用这些数据结构来组织硬件的信息及状态,我们管理这些数据结构就是在管理真实的硬盘。所以,我们来实现与硬盘相关的数据结构。注意,以下3个数据结构形成了层级关系,即:通道挂载硬盘,共有两个通道,每个通道可挂载一主一从两个硬盘;每个硬盘都有分区,可以最多支持4个主分区,一个主分区划分成扩展分区后可以再次划分出多个逻辑分区。
(device/ide.h)
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "sync.h"
#include "bitmap.h"
/* 分区结构 */
struct partition {
uint32_t start_lba; // 起始扇区
uint32_t sec_cnt; // 扇区数
struct disk* my_disk; // 分区所属的硬盘
struct list_elem part_tag; // 用于队列中的标记,用于将分区形成链表进行管理
char name[8]; // 分区名称
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
struct list open_inodes; // 本分区打开的i结点队列
};
/* 硬盘结构 */
struct disk {
char name[8]; // 本硬盘的名称,如sda等
struct ide_channel* my_channel; // 此块硬盘归属于哪个ide通道
uint8_t dev_no; // 本硬盘是主0还是从1
struct partition prim_parts[4]; // 主分区顶多是4个
struct partition logic_parts[8]; // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};
/* ata通道结构 */
struct ide_channel {
char name[8]; // 本ata通道名称
uint16_t port_base; // 本通道的起始端口号(书p126)
uint8_t irq_no; // 本通道所用的中断号
struct lock lock; // 通道锁
bool expecting_intr; // 表示等待硬盘的中断
struct semaphore disk_done; // 用于阻塞、唤醒驱动程序
struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};
#endif
有了数据结构,自然就需要根据我们的环境来创建并初始化(device.ide.c)
#include "stdint.h"
#include "global.h"
#include "ide.h"
#include "debug.h"
#include "sync.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
/* 定义硬盘各寄存器的端口号,见书p126 */
#define reg_data(channel) (channel->port_base + 0)
#define reg_error(channel) (channel->port_base + 1)
#define reg_sect_cnt(channel) (channel->port_base + 2)
#define reg_lba_l(channel) (channel->port_base + 3)
#define reg_lba_m(channel) (channel->port_base + 4)
#define reg_lba_h(channel) (channel->port_base + 5)
#define reg_dev(channel) (channel->port_base + 6)
#define reg_status(channel) (channel->port_base + 7)
#define reg_cmd(channel) (reg_status(channel))
#define reg_alt_status(channel) (channel->port_base + 0x206)
#define reg_ctl(channel) reg_alt_status(channel)
/* reg_alt_status寄存器的一些关键位,见书p128 */
#define BIT_STAT_BSY 0x80 // 硬盘忙
#define BIT_STAT_DRDY 0x40 // 设备准备好
#define BIT_STAT_DRQ 0x8 // 数据传输准备好了
/* device寄存器的一些关键位 */
#define BIT_DEV_MBS 0xa0 // 第7位和第5位固定为1
#define BIT_DEV_LBA 0x40 //指定为LBA寻址方式
#define BIT_DEV_DEV 0x10 //指定主盘或从盘,DEV位为1表示从盘,为0表示主盘
/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY 0xec // identify指令
#define CMD_READ_SECTOR 0x20 // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令
/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((80*1024*1024/512) - 1) // 只支持80MB硬盘
uint8_t channel_cnt; // 记录通道数
struct ide_channel channels[2]; // 有两个ide通道
/* 硬盘数据结构初始化 */
void ide_init() {
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0;
/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);
/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}
channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);
/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);
channel_no++; // 下一个channel
}
printk("ide_init done\n");
}
函数声明,并把通道数量与通道数组全局声明,修改(device/ide.h)
void ide_init(void);
extern uint8_t channel_cnt;
extern struct ide_channel channels[];
接下来我们实现一个idle
线程,用于在就绪队列为空时运行。需要注意一点:我们之前没有idle
线程,我们的系统没有出现书上说的由于就绪队列为空然后被ASSERT(!list_empty(&thread_ready_list);
悬停的情况,是因为我们的主线程(简单理解,就是main函数里面的while(1))会一直被不断加入就绪队列,所以就绪队列并不存在为空的时候。
修改(thread/thread.c)
struct task_struct* idle_thread; // idle线程
/* 系统空闲时运行的线程 */
static void idle(void* arg UNUSED) {
while(1) {
thread_block(TASK_BLOCKED);
//执行hlt时必须要保证目前处在开中断的情况下
asm volatile ("sti; hlt" : : : "memory");
}
}
/* 实现任务调度 */
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
}
else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}
/* 如果就绪队列中没有可运行的任务,就唤醒idle */
if (list_empty(&thread_ready_list)) {
thread_unblock(idle_thread);
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
process_activate(next); //激活任务页表
switch_to(cur, next);
}
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 将当前main函数创建为线程 */
make_main_thread();
/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread_init done\n");
}
支持代码,修改(kernel/global.h)
#define UNUSED __attribute__ ((unused))
硬盘是一个相对于CPU来说及其低速的设备,所以,当硬盘在进行需要长时间才能完成的工作时(比如写入数据),我们最好能让驱动程序把CPU让给其他任务。所以,我们来实现一个thread_yield
函数,就是用于把CPU让出来。实质就是将调用者重新放入就绪队列队尾。
修改(thread/thread.c)
/* 主动让出cpu,换其它线程运行 */
void thread_yield(void) {
struct task_struct* cur = running_thread();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->status = TASK_READY;
schedule();
intr_set_status(old_status);
}
thread_yield
中有个关中断的操作,会不会导致切换后由于关闭中断,而不响应时钟中断导致一直运行在切换后的进程/线程中呢?其实并不会,我们讨论两种情况,一种是进程/线程第一次上机运行,一种是进程/线程之前已经运行过,但由于时间片到期而换下过处理器。对于前者,我们进程/线程第一次上机运行都会经过kernel_thread
这个线程启动器,而这个里面是有开中断的代码的。对于后者,当切换回进程/线程时,它们执行kernel.S
中的中断退出代码jmp intr_exit
,这里面有一条指令iretd
会打开中断,让处理器能够继续响应中断代理发送来的中断信号。
由于thread_yield
不是由于时间片到期而换下处理器,所以当再次调度运行时,并不会执行kernel.S
中的中断退出代码jmp intr_exit
,所以要人为写上恢复中断状态的代码(开中断)。
函数声明(thread/thread.h)
void thread_yield(void);
之前我们实现的thread_yield
是将当前任务加入就绪队列队尾,仅仅是把CPU让出来一次。我们来实现一个定时让出CPU的函数,也就是让一个任务在固定时间内都不执行。
修改(device/timer.c)
#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY)
/* 以tick为单位的sleep,任何时间形式的sleep会转换此ticks形式 */
static void ticks_to_sleep(uint32_t sleep_ticks) {
uint32_t start_tick = ticks;
/* 若间隔的ticks数不够便让出cpu */
while (ticks - start_tick < sleep_ticks) {
thread_yield();
}
}
/* 以毫秒为单位的sleep 1秒= 1000毫秒 */
void mtime_sleep(uint32_t m_seconds) {
uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr);
ASSERT(sleep_ticks > 0);
ticks_to_sleep(sleep_ticks);
}
函数声明,修改(device/timer.h)
#include "stdint.h"
void mtime_sleep(uint32_t m_seconds);
现在,我们来实现驱动程序的主体部分,也就是实际与硬盘打交道的函数,实质就是将一系列寄存器操作进行封装
修改(device/ide.c)
#include "io.h"
#include "timer.h"
/* 选择读写的硬盘 */
static void select_disk(struct disk* hd) {
uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
if (hd->dev_no == 1) { // 若是从盘就置DEV位为1
reg_device |= BIT_DEV_DEV;
}
outb(reg_dev(hd->my_channel), reg_device);
}
/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
static void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) {
ASSERT(lba <= max_lba);
struct ide_channel* channel = hd->my_channel;
/* 写入要读写的扇区数*/
outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区
/* 写入lba地址(即扇区号) */
outb(reg_lba_l(channel), lba); // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。
outb(reg_lba_m(channel), lba >> 8); // lba地址的8~15位
outb(reg_lba_h(channel), lba >> 16); // lba地址的16~23位
/* 因为lba地址的24~27位要存储在device寄存器的0~3位,
* 无法单独写入这4位,所以在此处把device寄存器再重新写入一次*/
outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}
/* 向通道channel发命令cmd */
static void cmd_out(struct ide_channel* channel, uint8_t cmd) {
/* 只要向硬盘发出了命令便将此标记置为true,硬盘中断处理程序需要根据它来判断 */
channel->expecting_intr = true;
outb(reg_cmd(channel), cmd);
}
/* 硬盘读入sec_cnt个扇区的数据到buf */
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
}
else {
size_in_byte = sec_cnt * 512;
}
insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}
/* 将buf中sec_cnt扇区的数据写入硬盘 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
} else {
size_in_byte = sec_cnt * 512;
}
outsw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}
/* 等待30秒 */
static bool busy_wait(struct disk* hd) {
struct ide_channel* channel = hd->my_channel;
uint16_t time_limit = 30 * 1000; // 可以等待30000毫秒
while (time_limit -= 10 >= 0) {
if (!(inb(reg_status(channel)) & BIT_STAT_BSY)) {
return (inb(reg_status(channel)) & BIT_STAT_DRQ);
}
else {
mtime_sleep(10); // 睡眠10毫秒
}
}
return false;
}
/* 从硬盘读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock);
/* 1 先选择操作的硬盘 */
select_disk(hd);
uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
}
else {
secs_op = sec_cnt - secs_done;
}
/* 2 写入待读入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op);
/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_READ_SECTOR); // 准备开始读数据
/********************* 阻塞自己的时机 ***********************
在硬盘已经开始工作(开始在内部读数据或写数据)后才能阻塞自己,现在硬盘已经开始忙了,
将自己阻塞,等待硬盘完成读操作后通过中断处理程序唤醒自己*/
sema_down(&hd->my_channel->disk_done);
/*************************************************************/
/* 4 检测硬盘状态是否可读 */
/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}
/* 5 把数据从硬盘的缓冲区中读出 */
read_from_sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);
secs_done += secs_op;
}
lock_release(&hd->my_channel->lock);
}
/* 将buf中sec_cnt扇区数据写入硬盘 */
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock);
/* 1 先选择操作的硬盘 */
select_disk(hd);
uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
}
else {
secs_op = sec_cnt - secs_done;
}
/* 2 写入待写入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op);
/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_WRITE_SECTOR); // 准备开始写数据
/* 4 检测硬盘状态是否可读 */
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}
/* 5 将数据写入硬盘 */
write2sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);
/* 在硬盘响应期间阻塞自己 */
sema_down(&hd->my_channel->disk_done);
secs_done += secs_op;
}
/* 醒来后开始释放锁*/
lock_release(&hd->my_channel->lock);
}
/* 硬盘中断处理程序 */
void intr_hd_handler(uint8_t irq_no) {
ASSERT(irq_no == 0x2e || irq_no == 0x2f);
uint8_t ch_no = irq_no - 0x2e;
struct ide_channel* channel = &channels[ch_no];
ASSERT(channel->irq_no == irq_no);
/* 不必担心此中断是否对应的是这一次的expecting_intr,
* 每次读写硬盘时会申请锁,从而保证了同步一致性 */
if (channel->expecting_intr) {
channel->expecting_intr = false;
sema_up(&channel->disk_done);
/* 读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写 */
inb(reg_status(channel));
}
}
/* 硬盘数据结构初始化 */
void ide_init() {
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0;
/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);
/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}
channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);
/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);
register_handler(channel->irq_no, intr_hd_handler);
channel_no++; // 下一个channel
}
printk("ide_init done\n");
}
函数声明,修改(device/ide.h)
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
void intr_hd_handler(uint8_t irq_no);
123
现在,我们来验证驱动程序能够运行,我们用它来:1、发送identify命令给硬盘来获取硬盘信息;2、扫描分区表;
修改(device/ide.c)
#include "string.h"
/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
int32_t ext_lba_base = 0;
uint8_t p_no = 0, l_no = 0; // 用来记录硬盘主分区和逻辑分区的下标
struct list partition_list; // 分区队列
/* 构建一个16字节大小的结构体,用来存分区表项 */
struct partition_table_entry {
uint8_t bootable; // 是否可引导
uint8_t start_head; // 起始磁头号
uint8_t start_sec; // 起始扇区号
uint8_t start_chs; // 起始柱面号
uint8_t fs_type; // 分区类型
uint8_t end_head; // 结束磁头号
uint8_t end_sec; // 结束扇区号
uint8_t end_chs; // 结束柱面号
/* 更需要关注的是下面这两项 */
uint32_t start_lba; // 本分区起始扇区的lba地址
uint32_t sec_cnt; // 本分区的扇区数目
} __attribute__ ((packed)); // 保证此结构是16字节大小
/* 引导扇区,mbr或ebr所在的扇区 */
struct boot_sector {
uint8_t other[446]; // 引导代码
struct partition_table_entry partition_table[4]; // 分区表中有4项,共64字节
uint16_t signature; // 启动扇区的结束标志是0x55,0xaa,
} __attribute__ ((packed));
/* 将dst中len个相邻字节交换位置后存入buf */
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len) {
uint8_t idx;
for (idx = 0; idx < len; idx += 2) {
/* buf中存储dst中两相邻元素交换位置后的字符串*/
buf[idx + 1] = *dst++;
buf[idx] = *dst++;
}
buf[idx] = '\0';
}
/* 获得硬盘参数信息 */
static void identify_disk(struct disk* hd) {
char id_info[512];
select_disk(hd);
cmd_out(hd->my_channel, CMD_IDENTIFY);
/* 向硬盘发送指令后便通过信号量阻塞自己,
* 待硬盘处理完成后,通过中断处理程序将自己唤醒 */
sema_down(&hd->my_channel->disk_done);
/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s identify failed!!!!!!\n", hd->name);
PANIC(error);
}
read_from_sector(hd, id_info, 1);
char buf[64];
uint8_t sn_start = 10 * 2, sn_len = 20, md_start = 27 * 2, md_len = 40;
swap_pairs_bytes(&id_info[sn_start], buf, sn_len);
printk(" disk %s info:\n SN: %s\n", hd->name, buf);
memset(buf, 0, sizeof(buf));
swap_pairs_bytes(&id_info[md_start], buf, md_len);
printk(" MODULE: %s\n", buf);
uint32_t sectors = *(uint32_t*)&id_info[60 * 2];
printk(" SECTORS: %d\n", sectors);
printk(" CAPACITY: %dMB\n", sectors * 512 / 1024 / 1024);
}
/* 扫描硬盘hd中地址为ext_lba的扇区中的所有分区 */
static void partition_scan(struct disk* hd, uint32_t ext_lba) {
struct boot_sector* bs = sys_malloc(sizeof(struct boot_sector));
ide_read(hd, ext_lba, bs, 1);
uint8_t part_idx = 0; //用于遍历主分区的变量
struct partition_table_entry* p = bs->partition_table;
/* 遍历分区表4个分区表项 */
while (part_idx++ < 4) {
if (p->fs_type == 0x5) { // 若为扩展分区
if (ext_lba_base != 0) {
/* 子扩展分区的start_lba是相对于主引导扇区中的总扩展分区地址 */
partition_scan(hd, p->start_lba + ext_lba_base);
}
else { // ext_lba_base为0表示是第一次读取引导块,也就是主引导记录所在的扇区
/* 记录下扩展分区的起始lba地址,后面所有的扩展分区地址都相对于此 */
ext_lba_base = p->start_lba;
partition_scan(hd, p->start_lba);
}
}
else if (p->fs_type != 0) { // 若是有效的分区类型
if (ext_lba == 0) { // 此时全是主分区
hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
hd->prim_parts[p_no].my_disk = hd;
list_append(&partition_list, &hd->prim_parts[p_no].part_tag);
sprintf(hd->prim_parts[p_no].name, "%s%d", hd->name, p_no + 1);
p_no++;
ASSERT(p_no < 4); // 0,1,2,3
}
else {
hd->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
hd->logic_parts[l_no].my_disk = hd;
list_append(&partition_list, &hd->logic_parts[l_no].part_tag);
sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no + 5); // 逻辑分区数字是从5开始,主分区是1~4.
l_no++;
if (l_no >= 8) // 只支持8个逻辑分区,避免数组越界
return;
}
}
p++;
}
sys_free(bs);
}
/* 打印分区信息 */
static bool partition_info(struct list_elem* pelem, int arg UNUSED) {
struct partition* part = elem2entry(struct partition, part_tag, pelem);
printk(" %s start_lba:0x%x, sec_cnt:0x%x\n",part->name, part->start_lba, part->sec_cnt);
/* 在此处return false与函数本身功能无关,
* 只是为了让主调函数list_traversal继续向下遍历元素 */
return false;
}
/* 硬盘数据结构初始化 */
void ide_init() {
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
ASSERT(hd_cnt > 0);
list_init(&partition_list);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0, dev_no = 0;
/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);
/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}
channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);
/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);
register_handler(channel->irq_no, intr_hd_handler);
/* 分别获取两个硬盘的参数及分区信息 */
while (dev_no < 2) {
struct disk* hd = &channel->devices[dev_no];
hd->my_channel = channel;
hd->dev_no = dev_no;
sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no);
identify_disk(hd); // 获取硬盘参数
if (dev_no != 0) { // 内核本身的裸硬盘(hd60M.img)不处理
partition_scan(hd, 0); // 扫描该硬盘上的分区
}
p_no = 0, l_no = 0;
dev_no++;
}
dev_no = 0; // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。
channel_no++; // 下一个channel
}
printk("\n all partition info\n");
/* 打印所有分区信息 */
list_traversal(&partition_list, partition_info, (int)NULL);
printk("ide_init done\n");
}
修改(kernel/init.c)完成ide的初始化
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"
#include "syscall-init.h"
#include "ide.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化PIT
console_init(); // 控制台初始化最好放在开中断之前
keyboard_init(); // 键盘初始化
tss_init(); // tss初始化
syscall_init(); // 初始化系统调用
ide_init(); // 初始化硬盘
}
修改(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
while(1);
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
由于我们fdisk工具分区时,设定分区起始与大小是用扇区做单位而非作者操作时的柱面,所以我们显示的硬盘分区信息start_lba
与sec_cnt
与他不一样。
文件系统
文件系统中,最核心的就是三个概念:inode、目录(目录也是文件)和超级块。文件系统是以分区为单位,也就是每个分区都有自己的文件系统。
- inode
- inode记录了文件与磁盘位置的映射。
- 每个文件对应一个inode。当我们找到了这个文件对应的inode,就能知道这个文件在磁盘中的存储位置。
- 每个磁盘分区的所有inode都会形成一个数组。使用文件的inode数组下标,我们可以在该数组中查找对应的inode信息。
- 目录
- 一个目录其实也是一个特殊的文件。
- 目录由众多目录项构成,目录项记录了文件名到inode的映射。如果一个目录管理
/Desktop
文件夹,那么这个目录下的众多目录项就是管理/Desktop
文件夹下的各个文件和子目录。
- 超级块
- 超级块包含了关于inode、目录及其他文件系统元数据的信息。
- 通常,它位于每个磁盘分区的第二个扇区,这是一个固定位置。
- 对于inode,它记录了inode数组在磁盘中的起始位置;对于根目录,超级块还会记录其inode标号。
查找文件的流程示例:
如果我们要查找一个/home/test.c
,那么文件系统是如何工作的呢?
- 首先,由于超级块位置固定,我们可以去磁盘中直接访问它。
- 从超级块中,我们能知道根目录的inode标号和inode数组的位置。
- 使用根目录的inode标号与inode数组位置,我们可以找到根目录文件对应的inode,然后在磁盘中找到根目录文件。
- 在根目录文件中,我们查找一个名叫
home
的目录项,从中取出home
的inode数组标号。 - 使用
home
的inode标号,我们再次访问inode数组,找到home
目录文件在磁盘上的实际位置。 - 最后,在
home
目录文件中,我们查找一个名叫test.c
的目录项,从中取出test.c
的inode数组标号,进而找到test.c
在磁盘上的位置。
一个典型的文件系统元数据结构在磁盘中的位置如图所示
小节a:
我们就是写一个函数partition_format
用于创建文件系统,也就是创建文件系统元数据(超级块,空闲块位图,inode位图,inode数组,根目录);
然后写一个函数filesys_init
遍历所有分区,如果该分区没有文件系统就调用partition_format
来创建文件系统
首先我们来准备数据结构
超级块 (fs/super_block.h)
#ifndef __FS_SUPER_BLOCK_H
#define __FS_SUPER_BLOCK_H
#include "stdint.h"
/* 超级块 */
struct super_block {
uint32_t magic; // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型
uint32_t sec_cnt; // 本分区总共的扇区数
uint32_t inode_cnt; // 本分区中inode数量
uint32_t part_lba_base; // 本分区的起始lba地址
uint32_t block_bitmap_lba; // 块位图本身起始扇区地址
uint32_t block_bitmap_sects; // 扇区位图本身占用的扇区数量
uint32_t inode_bitmap_lba; // i结点位图起始扇区lba地址
uint32_t inode_bitmap_sects; // i结点位图占用的扇区数量
uint32_t inode_table_lba; // i结点表起始扇区lba地址
uint32_t inode_table_sects; // i结点表占用的扇区数量
uint32_t data_start_lba; // 数据区开始的第一个扇区号
uint32_t root_inode_no; // 根目录所在的I结点号
uint32_t dir_entry_size; // 目录项大小
uint8_t pad[460]; // 加上460字节,凑够512字节1扇区大小
} __attribute__ ((packed));
#endif
inode (fs/inode.h)
#ifndef __FS_INODE_H
#define __FS_INODE_H
#include "stdint.h"
#include "list.h"
/* inode结构 */
struct inode {
uint32_t i_no; // inode编号
/* 当此inode是文件时,i_size是指文件大小,
若此inode是目录,i_size是指该目录下所有目录项大小之和*/
uint32_t i_size;
uint32_t i_open_cnts; // 记录此文件被打开的次数
bool write_deny; // 写文件不能并行,进程写文件前检查此标识
/* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */
uint32_t i_sectors[13];
struct list_elem inode_tag;
};
#endif
目录与目录项 (fs/dir.h) 需要注意的是:目录的数据结构struct dir
只会存在于内存之中,因为它管理的是对一个目录文件的操作(比如打开一个目录文件,就会在内存中创建这样一个结构体)
#ifndef __FS_DIR_H
#define __FS_DIR_H
#include "stdint.h"
#include "inode.h"
#define MAX_FILE_NAME_LEN 16 // 最大文件名长度
/* 目录结构 */
struct dir {
struct inode* inode;
uint32_t dir_pos; // 记录在目录内的偏移
uint8_t dir_buf[512]; // 目录的数据缓存
};
/* 目录项结构 */
struct dir_entry {
char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称
uint32_t i_no; // 普通文件或目录对应的inode编号
enum file_types f_type; // 文件类型
};
#endif
文件类型定义: (fs/fs.h)
#ifndef __FS_FS_H
#define __FS_FS_H
#include "stdint.h"
#define MAX_FILES_PER_PART 4096 // 每个分区所支持最大创建的文件数
#define BITS_PER_SECTOR 4096 // 每扇区的位数
#define SECTOR_SIZE 512 // 扇区字节大小
#define BLOCK_SIZE SECTOR_SIZE // 块字节大小
/* 文件类型 */
enum file_types {
FT_UNKNOWN, // 不支持的文件类型
FT_REGULAR, // 普通文件
FT_DIRECTORY // 目录
};
#endif
然后编写函数partition_format
用于创建文件系统,也就是创建文件系统元数据(超级块,空闲块位图,inode位图,inode数组,根目录)
(fs/fs.c)
#include "stdint.h"
#include "fs.h"
#include "inode.h"
#include "ide.h"
#include "memory.h"
#include "super_block.h"
#include "dir.h"
#include "stdio-kernel.h"
#include "string.h"
/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
static void partition_format(struct partition* part) {
/* 为方便实现,一个块大小是一扇区 */
uint32_t boot_sector_sects = 1;
uint32_t super_block_sects = 1;
uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR); // I结点位图占用的扇区数.最多支持4096个文件
uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
uint32_t free_sects = part->sec_cnt - used_sects;
/************** 简单处理块位图占据的扇区数 ***************/
uint32_t block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
/* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
/*********************************************************/
/* 超级块初始化 */
struct super_block sb;
sb.magic = 0x19590318;
sb.sec_cnt = part->sec_cnt;
sb.inode_cnt = MAX_FILES_PER_PART;
sb.part_lba_base = part->start_lba;
sb.block_bitmap_lba = sb.part_lba_base + 2; // 第0块是引导块,第1块是超级块
sb.block_bitmap_sects = block_bitmap_sects;
sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
sb.inode_bitmap_sects = inode_bitmap_sects;
sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
sb.inode_table_sects = inode_table_sects;
sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects; //数据区的起始就是inode数组的结束
sb.root_inode_no = 0;
sb.dir_entry_size = sizeof(struct dir_entry);
printk("%s info:\n", part->name);
printk(" magic:0x%x\n part_lba_base:0x%x\n all_sectors:0x%x\n inode_cnt:0x%x\n block_bitmap_lba:0x%x\n block_bitmap_sectors:0x%x\n inode_bitmap_lba:0x%x\n inode_bitmap_sectors:0x%x\n inode_table_lba:0x%x\n inode_table_sectors:0x%x\n data_start_lba:0x%x\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba);
struct disk* hd = part->my_disk;
/*******************************
* 1 将超级块写入本分区的1扇区 *
******************************/
ide_write(hd, part->start_lba + 1, &sb, 1);
printk(" super_block_lba:0x%x\n", part->start_lba + 1);
/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/
uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects);
buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;
uint8_t* buf = (uint8_t*)sys_malloc(buf_size); // 申请的内存由内存管理系统清0后返回
/**************************************
* 2 将块位图初始化并写入sb.block_bitmap_lba *
*************************************/
/* 初始化块位图block_bitmap */
buf[0] |= 0x01; // 第0个块预留给根目录,位图中先占位
uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8; //计算出块位图最后一字节的偏移
uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8; //计算出块位图最后一位的偏移
uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE); // last_size是位图所在最后一个扇区中,不足一扇区的其余部分
/* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/
memset(&buf[block_bitmap_last_byte], 0xff, last_size);
/* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */
uint8_t bit_idx = 0;
while (bit_idx <= block_bitmap_last_bit) {
buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
}
ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);
/***************************************
* 3 将inode位图初始化并写入sb.inode_bitmap_lba *
***************************************/
/* 先清空缓冲区*/
memset(buf, 0, buf_size);
buf[0] |= 0x1; // 第0个inode分给了根目录
/* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区,
* 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode,
* 无须再像block_bitmap那样单独处理最后一扇区的剩余部分,
* inode_bitmap所在的扇区中没有多余的无效位 */
ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);
/***************************************
* 4 将inode数组初始化并写入sb.inode_table_lba *
***************************************/
/* 准备写inode_table中的第0项,即根目录所在的inode */
memset(buf, 0, buf_size); // 先清空缓冲区buf
struct inode* i = (struct inode*)buf;
i->i_size = sb.dir_entry_size * 2; // .和..
i->i_no = 0; // 根目录占inode数组中第0个inode
i->i_sectors[0] = sb.data_start_lba; // 由于上面的memset,i_sectors数组的其它元素都初始化为0
ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);
/***************************************
* 5 将根目录初始化并写入sb.data_start_lba
***************************************/
/* 写入根目录的两个目录项.和.. */
memset(buf, 0, buf_size);
struct dir_entry* p_de = (struct dir_entry*)buf;
/* 初始化当前目录"." */
memcpy(p_de->filename, ".", 1);
p_de->i_no = 0;
p_de->f_type = FT_DIRECTORY;
p_de++;
/* 初始化当前目录父目录".." */
memcpy(p_de->filename, "..", 2);
p_de->i_no = 0; // 根目录的父目录依然是根目录自己
p_de->f_type = FT_DIRECTORY;
/* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */
ide_write(hd, sb.data_start_lba, buf, 1);
printk(" root_dir_lba:0x%x\n", sb.data_start_lba);
printk("%s format done\n", part->name);
sys_free(buf);
}
采用无限迭代,直到值稳定下来的方法。这个代码实际解决的问题就是:在总量不变的情况下,有多少是可用资源,有多少是管理资源。
修改(fs/fs.c/partition_format)
/************** 简单处理块位图占据的扇区数 ***************/
uint32_t block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
/* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
/*********************************************************/
为
/************** 简单处理块位图占据的扇区数 ***************/
uint32_t now_total_free_sects = free_sects; // 定义一个现在总的可用扇区数
uint32_t prev_block_bitmap_sects = 0; // 之前的块位图扇区数
uint32_t block_bitmap_sects = DIV_ROUND_UP(now_total_free_sects, BITS_PER_SECTOR); // 初始估算
uint32_t block_bitmap_bit_len;
while (block_bitmap_sects != prev_block_bitmap_sects) {
prev_block_bitmap_sects = block_bitmap_sects;
/* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
block_bitmap_bit_len = now_total_free_sects - block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
}
/*********************************************************/
由于block_bitmap_last_bit实际含义是块位图最后一字节有效位的数量,且bit_idx从0开始取,所以以下while循环会多循环1次,应该修改:(fs/fs.c/partition_format)
while (bit_idx <= block_bitmap_last_bit) {
buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
}
为
while (bit_idx < block_bitmap_last_bit) {
buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
}
然后写一个函数filesys_init
遍历所有分区,如果该分区没有文件系统就调用partition_format
来创建文件系统
修改(fs/fs.c)
#include "debug.h"
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) { //遍历两个通道
dev_no = 0;
while(dev_no < 2) { //遍历通道下1主1从两个硬盘
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 遍历硬盘的分区,4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}
/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);
/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);
/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
}
else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++; // 下一分区
}
dev_no++; // 下一磁盘
}
channel_no++; // 下一通道
}
sys_free(sb_buf);
}
函数声明 修改(fs/fs.h)
void filesys_init(void);
修改 (kernel/init.c)以初始化文件系统
#include "fs.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化PIT
console_init(); // 控制台初始化最好放在开中断之前
keyboard_init(); // 键盘初始化
tss_init(); // tss初始化
syscall_init(); // 初始化系统调用
intr_enable(); // 后面的ide_init需要打开中断
ide_init(); // 初始化硬盘
filesys_init(); // 初始化文件系统
}
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
while(1);
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
/* 测试用户进程 */
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
Makefile 除了增加编译fs.c的规则外,还要新增-I fs/
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/ -I fs/
建议在运行前,先备份hd80M.img
,不然出错了,很可能这里面会写入无意义数据。命令:cp hd80M.img hd80M.img-beifen
小节b:
之前我们写好了函数用于创建文件系统元数据,并且将其写入了磁盘。当我们实际要用到某个分区的数据,就需要挂载该分区,也就是读取磁盘中文件系统元数据(超级块,块位图,inode位图,inode数组由于过大,不读入到内存中,)到内存中,然后对这些元数据的改动,要及时同步到磁盘中。现在我们就来写这个挂载分区的函数mount_partition
。这个函数会被list_traversal
调用。
修改(fs/fs.c)
struct partition* cur_part; // 默认情况下操作的是哪个分区
/* 在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part */
static bool mount_partition(struct list_elem* pelem, int arg) {
char* part_name = (char*)arg;
struct partition* part = elem2entry(struct partition, part_tag, pelem);
if (!strcmp(part->name, part_name)) {
cur_part = part;
struct disk* hd = cur_part->my_disk;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
/* 在内存中创建分区cur_part的超级块 */
cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
if (cur_part->sb == NULL) {
PANIC("alloc memory failed!");
}
/* 读入超级块 */
memset(sb_buf, 0, SECTOR_SIZE);
ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);
/* 把sb_buf中超级块的信息复制到分区的超级块sb中。*/
memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));
/********** 将硬盘上的块位图读入到内存 ****************/
cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
if (cur_part->block_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入块位图到分区的block_bitmap.bits */
ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects);
/*************************************************************/
/********** 将硬盘上的inode位图读入到内存 ************/
cur_part->inode_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
if (cur_part->inode_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入inode位图到分区的inode_bitmap.bits */
ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits, sb_buf->inode_bitmap_sects);
/*************************************************************/
list_init(&cur_part->open_inodes);
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
return true;
}
return false; // 使list_traversal继续遍历
}
修改(fs/fs.c)filesys_init
增加使用list_traversal
调用mount_partition
挂载指定分区文件系统的代码
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) { //遍历两个通道
dev_no = 0;
while(dev_no < 2) { //遍历通道下1主1从两个硬盘
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 遍历硬盘的分区,4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}
/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);
/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);
/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
}
else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++; // 下一分区
}
dev_no++; // 下一磁盘
}
channel_no++; // 下一通道
}
sys_free(sb_buf);
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
}
修改(device/ide.h)
(这个分区链表是我们挂载磁盘ide_init
函数中调用partition_scan
就创建了的,所以上一小节创建分区文件系统,其实我们可以去遍历这个链表)
extern struct list partition_list;
mount_partition
内的sb_buf
没有释放!会造成内存泄漏!
修改(fs/fs.c/mountpartition)
list_init(&cur_part->open_inodes);
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
return true;
}
return false; // 使list_traversal继续遍历
为
list_init(&cur_part->open_inodes);
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
sys_free(sb_buf);
return true;
}
return false; // 使list_traversal继续遍历
小节c:
文件系统的核心功能是管理文件(inode)与其在磁盘上存储位置之间的映射关系。但当我们谈到实际的文件操作(如打开、读取、写入),这些操作总是基于特定的进程或线程来进行。所以我们需要一个机制来建立进程或线程到文件(inode)的映射,这正是文件描述符的作用。而文件描述符则是基于“文件结构”来实现的。
文件结构详细描述了应用进程对于一个文件的当前操作状态:
- 当应用进程操作(比如打开)一个文件时,操作系统会创建一个对应的全局文件结构,放置于全局文件结构数组(也叫文件表)中,用于跟踪该进程在该文件上的操作状态。
- 文件结构中包含一个
fd_pos
字段,它表示当前进程操作的位置。例如,当我们打开一个文件test.txt
,fd_pos
的初始值通常是文件的开头。 - 当我们对文件执行实际的读写操作,
fd_pos
会被更新到操作的位置。例如,如果在文件中间开始写入数据,fd_pos
会更新为那个位置并随着写入操作继续前移。当进行编辑操作时,输入的内容首先被存放在一个内存输入缓冲区中。按下ctrl + s
后,操作系统会使用文件结构中的fd_pos
作为起始点,将缓冲区的内容写入磁盘文件中,同时更新fd_pos
到写入结束后的位置。 - 文件结构还包含一个字段
fd_inode
,它是对应文件的inode的指针。通过这个fd_inode
字段,操作系统可以轻松地找到文件在磁盘上的位置,并将内存输入缓冲区的内容写入磁盘。
由于文件结构与具体的应用程序高度相关,我们需要引入一个新的概念:文件描述符数组。
文件描述符数组是存储在进程的进程控制块(PCB)中的一个数组。每个元素都是一个文件描述符(就是一个数字),它指向全局文件结构数组中的一个特定条目。
当进程打开一个文件,操作系统会为这个文件创建一个文件结构并将其放入全局文件结构数组中。接着,操作系统会将这个文件结构在全局数组中的位置(或索引)赋给进程的文件描述符数组中的一个空闲元素。
例如一个进程打开了两个文件,它们在全局文件结构数组中的位置分别是32和58。那么,进程的PCB中的文件描述符数组的第0个元素就会被赋值为32,而第1个元素则会被赋值为58。
PCB中的文件描述符数组的大小限制了一个进程能够同时打开文件的最大数量。如果进程打开一个新的文件,但文件描述符数组已满,则操作将失败。
需要注意的是,打开不同的文件当然会消耗一个文件描述符。但由于我们在文件结构中引入了文件操作位置的概念(即
fd_pos
),如果一个进程多次打开同一个文件(即每次打开都有自己的读写位置),那么每次打开都会消耗一个新的文件描述符。
文件系统这一章函数,代码非常多,其实核心就是处理超级块,inode,目录,文件结构这四个概念之间的交互。
为了支持文件描述符,我们要取修改task_struct
结构体的定义。(还新增了parent_id
与cwd_inode_nr
)
修改(thread/thread.h)
#define MAX_FILES_OPEN_PER_PROC 8
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{
uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
pid_t pid;
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; // 用于存储自己的线程的名字
uint8_t ticks; // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t *pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打开文件数组
uint32_t cwd_inode_nr; // 进程所在的工作目录的inode编号
int16_t parent_pid; // 父进程pid
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
uint32_t stack_magic; // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
在创建进程/线程的初始化pcb环节,增加对这个文件描述符数组以及parent_id
和cwd_inode_nr
初始化代码
修改(thread/thread.c)
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct *pthread, char *name, int prio)
{
memset(pthread, 0, sizeof(*pthread)); // 把pcb初始化为0
pthread->pid = allocate_pid();
strcpy(pthread->name, name); // 将传入的线程的名字填入线程的pcb中
if (pthread == main_thread)
{
pthread->status = TASK_RUNNING; // 由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
}
else
{
pthread->status = TASK_READY;
}
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; // 线程没有自己的地址空间,进程的pcb这一项才有用,指向自己的页表虚拟地址
pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE); // 本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
/* 标准输入输出先空出来 */
pthread->fd_table[0] = 0;
pthread->fd_table[1] = 1;
pthread->fd_table[2] = 2;
/* 其余的全置为-1 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC)
{
pthread->fd_table[fd_idx] = -1;
fd_idx++;
}
pthread->cwd_inode_nr = 0; // 以根目录做为默认工作路径
pthread->parent_pid = -1; // -1表示没有父进程
pthread->stack_magic = 0x19870916; // 定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
任何有关文件及目录的操作都了不开对inode的操作,因为我们需要通过inode知道文件的存储位置,所以操作文件,总是意味着要找到该文件的inode。接下来我们实现一堆对于inode的处理函数。涉及:找到一个inode在磁盘中的位置,初始化一个inode,加载该inode到内存中,修改内存中的inode之后同步到磁盘中,从内存中删除一个inode。
inode_locate
用于通过传入指定inode在inode数组中的索引,来获取inode所在扇区和扇区内的偏移。原理:由于传入了inode在inode数组中的索引,且超级块(挂载文件系统后,超级块在内存中)中已经记录了inode数组起始扇区。所以我们能通过索引 * 每个inode大小,计算出这个inode所在扇区和扇区内的偏移。
(fs/inode.c)
#include "stdint.h"
#include "ide.h"
#include "inode.h"
#include "debug.h"
#include "super_block.h"
/* 用来存储inode位置 */
struct inode_position {
bool two_sec; // inode是否跨扇区
uint32_t sec_lba; // inode所在的扇区号
uint32_t off_size; // inode在扇区内的字节偏移量
};
/* 获取inode所在的扇区和扇区内的偏移量 */
static void inode_locate(struct partition *part, uint32_t inode_no, struct inode_position *inode_pos)
{
/* inode_table在硬盘上是连续的 */
ASSERT(inode_no < 4096);
uint32_t inode_table_lba = part->sb->inode_table_lba;
uint32_t inode_size = sizeof(struct inode);
uint32_t off_size = inode_no * inode_size; // 第inode_no号I结点相对于inode_table_lba的字节偏移量
uint32_t off_sec = off_size / 512; // 第inode_no号I结点相对于inode_table_lba的扇区偏移量
uint32_t off_size_in_sec = off_size % 512; // 待查找的inode所在扇区中的起始地址
/* 判断此i结点是否跨越2个扇区 */
uint32_t left_in_sec = 512 - off_size_in_sec;
if (left_in_sec < inode_size)
{ // 若扇区内剩下的空间不足以容纳一个inode,必然是I结点跨越了2个扇区
inode_pos->two_sec = true;
}
else
{ // 否则,所查找的inode未跨扇区
inode_pos->two_sec = false;
}
inode_pos->sec_lba = inode_table_lba + off_sec;
inode_pos->off_size = off_size_in_sec;
}
inode_sync
用于将一个inode写入磁盘中inode数组对应的位置处。原理:调用inode_locate
解析出这个inode在磁盘中的位置,然后将这个inode所在的块整个读出到内存缓冲中,再将这个inode写入缓冲中对应的位置处,再将整个块写回磁盘。
修改(fs/inode.c)
#include "string.h"
/* 将inode写入到分区part */
void inode_sync(struct partition *part, struct inode *inode, void *io_buf)
{ // io_buf是用于硬盘io的缓冲区
uint8_t inode_no = inode->i_no;
struct inode_position inode_pos;
inode_locate(part, inode_no, &inode_pos); // inode位置信息会存入inode_pos
ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));
/* 硬盘中的inode中的成员inode_tag和i_open_cnts是不需要的,
* 它们只在内存中记录链表位置和被多少进程共享 */
struct inode pure_inode;
memcpy(&pure_inode, inode, sizeof(struct inode));
/* 以下inode的三个成员只存在于内存中,现在将inode同步到硬盘,清掉这三项即可 */
pure_inode.i_open_cnts = 0;
pure_inode.write_deny = false; // 置为false,以保证在硬盘中读出时为可写
pure_inode.inode_tag.prev = pure_inode.inode_tag.next = NULL;
char *inode_buf = (char *)io_buf;
if (inode_pos.two_sec)
{ // 若是跨了两个扇区,就要读出两个扇区再写入两个扇区
/* 读写硬盘是以扇区为单位,若写入的数据小于一扇区,要将原硬盘上的内容先读出来再和新数据拼成一扇区后再写入 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2); // inode在format中写入硬盘时是连续写入的,所以读入2块扇区
/* 开始将待写入的inode拼入到这2个扇区中的相应位置 */
memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
/* 将拼接好的数据再写入磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
}
else
{ // 若只是一个扇区
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
}
inode_open
用于打开一个inode,也就是根据传入的inode数组索引找到该inode并加载到内存中。由于我们在管理分区的结构体struct partition中维护了一个本分区打开文件的inode链表,所以我们优先去这个链表中查找(这个链表在内存中),找不到再去磁盘中找,也就是调用inode_locate
解析他在磁盘中的位置,然后读到内存中。由于打开的Inode链表需要对所有进程共享,所有我们存放inode的内存需要去内核堆区申请(因为所有的进程均可访问内核)
修改(fs/inode.c)
/* 根据i结点号返回相应的i结点 */
struct inode *inode_open(struct partition *part, uint32_t inode_no)
{
/* 先在已打开inode链表中找inode,此链表是为提速创建的缓冲区 */
struct list_elem *elem = part->open_inodes.head.next;
struct inode *inode_found;
while (elem != &part->open_inodes.tail)
{
inode_found = elem2entry(struct inode, inode_tag, elem);
if (inode_found->i_no == inode_no)
{
inode_found->i_open_cnts++;
return inode_found;
}
elem = elem->next;
}
/*由于open_inodes链表中找不到,下面从硬盘上读入此inode并加入到此链表 */
struct inode_position inode_pos;
/* inode位置信息会存入inode_pos, 包括inode所在扇区地址和扇区内的字节偏移量 */
inode_locate(part, inode_no, &inode_pos);
/* 为使通过sys_malloc创建的新inode被所有任务共享,
* 需要将inode置于内核空间,故需要临时
* 将cur_pbc->pgdir置为NULL */
struct task_struct *cur = running_thread();
uint32_t *cur_pagedir_bak = cur->pgdir;
cur->pgdir = NULL;
/* 以上三行代码完成后下面分配的内存将位于内核区 */
inode_found = (struct inode *)sys_malloc(sizeof(struct inode));
/* 恢复pgdir */
cur->pgdir = cur_pagedir_bak;
char *inode_buf;
if (inode_pos.two_sec)
{ // 考虑跨扇区的情况
inode_buf = (char *)sys_malloc(1024);
/* i结点表是被partition_format函数连续写入扇区的,
* 所以下面可以连续读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
}
else
{ // 否则,所查找的inode未跨扇区,一个扇区大小的缓冲区足够
inode_buf = (char *)sys_malloc(512);
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode));
/* 因为一会很可能要用到此inode,故将其插入到队首便于提前检索到 */
list_push(&part->open_inodes, &inode_found->inode_tag);
inode_found->i_open_cnts = 1;
sys_free(inode_buf);
return inode_found;
}
inode_close
用于关闭一个inode,也就是从内存中移除一个inode。由于一个inode可以被多次打开,我们需要判断此时是否还有进程/线程打开这个inode,再决定是否真正移除。移除的inode所占用的内存空间是内核的堆空间。
修改(fs/inode.c)
#include "interrupt.h"
/* 关闭inode或减少inode的打开数 */
void inode_close(struct inode *inode)
{
/* 若没有进程再打开此文件,将此inode去掉并释放空间 */
enum intr_status old_status = intr_disable();
if (--inode->i_open_cnts == 0)
{
list_remove(&inode->inode_tag); // 将I结点从part->open_inodes中去掉
/* inode_open时为实现inode被所有进程共享,
* 已经在sys_malloc为inode分配了内核空间,
* 释放inode时也要确保释放的是内核内存池 */
struct task_struct *cur = running_thread();
uint32_t *cur_pagedir_bak = cur->pgdir;
cur->pgdir = NULL;
sys_free(inode);
cur->pgdir = cur_pagedir_bak;
}
intr_set_status(old_status);
}
inode_init
通过传入inode编号初始化一个inode
修改(fs/inode.c)
/* 初始化new_inode */
void inode_init(uint32_t inode_no, struct inode *new_inode)
{
new_inode->i_no = inode_no;
new_inode->i_size = 0;
new_inode->i_open_cnts = 0;
new_inode->write_deny = false;
/* 初始化块索引数组i_sector */
uint8_t sec_idx = 0;
while (sec_idx < 13)
{
/* i_sectors[12]为一级间接块地址 */
new_inode->i_sectors[sec_idx] = 0;
sec_idx++;
}
}
函数声明:修改(fs/fs.h)
void inode_sync(struct partition* part, struct inode* inode, void* io_buf);
struct inode *inode_open(struct partition *part, uint32_t inode_no);
void inode_close(struct inode *inode);
void inode_init(uint32_t inode_no, struct inode *new_inode);
1234
接下来实现一堆与目录相关的函数,涉及目录打开、关闭,在一个目录文件中寻找指定目录项,初始化一个目录项,将目录项写入父目录中
open_root_dir
用于根据传入的分区,调用inode_open
将根目录文件的inode调入内存,并将全局变量struct root_dir
中的inode指针,指向根目录文件的inode
(fs/dir.c)
#include "dir.h"
#include "inode.h"
#include "super_block.h"
struct dir root_dir; // 根目录
/* 打开根目录 */
void open_root_dir(struct partition *part)
{
root_dir.inode = inode_open(part, part->sb->root_inode_no);
root_dir.dir_pos = 0;
}
dir_open
用于根据传入的分区与inode偏移,申请一块内存区域存放目录,调用inode_open
找到这个目录对应的inode并载入内存,然后让目录中的inode指针指向这个inode。功能和open_root_dir
类似,只不过一个是直接修改全局变量,一个要自行申请内存。
修改(fs/dir.c)
/* 在分区part上打开i结点为inode_no的目录并返回目录指针 */
struct dir *dir_open(struct partition *part, uint32_t inode_no)
{
struct dir *pdir = (struct dir *)sys_malloc(sizeof(struct dir));
pdir->inode = inode_open(part, inode_no);
pdir->dir_pos = 0;
return pdir;
}
所以我们可以发现,打开目录,就是建立目录(struct dir结构体)与inode之间的关系。为什么?因为目录是一个内存中的概念,但它本身是磁盘中的一个文件!为了建立从内存到磁盘的关系,所以我们需要建立struct dir与inode之间的关系。所以我们打开目录,自然意味着这个struct dir结构体要在内存中。再进一步思考,我们打开目录,肯定意味着要查找这个目录下什么东西(就像是你打开文件夹,是为了找到文件夹下某个文件),而只有inode记录着这个目录文件在磁盘中的位置,而目录文件就是一大堆目录项的集合,这些目录项记录着这个目录有啥。所以为了找到目录下有啥,我们肯定要找到它的inode,调入内存,并建立二者之间的关系。
search_dir_entry
用于在某个目录内找到指定名称的目录项。核心原理就是目录结构体struct dir 有一个指向自己inode的成员,该成员记录了该目录在磁盘中存储的位置(inode的i_sectors[ ])我们可以根据这个找到目录文件,然后遍历其中的目录项即可
修改(fs/dir.c)
#include "stdio-kernel.h"
#include "string.h"
/* 在part分区内的pdir目录内寻找名为name的文件或目录,
* 找到后返回true并将其目录项存入dir_e,否则返回false */
bool search_dir_entry(struct partition *part, struct dir *pdir, const char *name, struct dir_entry *dir_e)
{
uint32_t block_cnt = 140; // 12个直接块+128个一级间接块=140块
/* 12个直接块大小+128个间接块,共560字节 */
uint32_t *all_blocks = (uint32_t *)sys_malloc(48 + 512);
if (all_blocks == NULL)
{
printk("search_dir_entry: sys_malloc for all_blocks failed");
return false;
}
uint32_t block_idx = 0;
while (block_idx < 12)
{
all_blocks[block_idx] = pdir->inode->i_sectors[block_idx];
block_idx++;
}
block_idx = 0;
if (pdir->inode->i_sectors[12] != 0)
{ // 若含有一级间接块表
ide_read(part->my_disk, pdir->inode->i_sectors[12], all_blocks + 12, 1);
}
/* 至此,all_blocks存储的是该文件或目录的所有扇区地址 */
/* 写目录项的时候已保证目录项不跨扇区,
* 这样读目录项时容易处理, 只申请容纳1个扇区的内存 */
uint8_t *buf = (uint8_t *)sys_malloc(SECTOR_SIZE);
struct dir_entry *p_de = (struct dir_entry *)buf; // p_de为指向目录项的指针,值为buf起始地址
uint32_t dir_entry_size = part->sb->dir_entry_size;
uint32_t dir_entry_cnt = SECTOR_SIZE / dir_entry_size; // 1扇区内可容纳的目录项个数
/* 开始在所有块中查找目录项 */
while (block_idx < block_cnt)
{
/* 块地址为0时表示该块中无数据,继续在其它块中找 */
if (all_blocks[block_idx] == 0)
{
block_idx++;
continue;
}
ide_read(part->my_disk, all_blocks[block_idx], buf, 1);
uint32_t dir_entry_idx = 0;
/* 遍历扇区中所有目录项 */
while (dir_entry_idx < dir_entry_cnt)
{
/* 若找到了,就直接复制整个目录项 */
if (!strcmp(p_de->filename, name))
{
memcpy(dir_e, p_de, dir_entry_size);
sys_free(buf);
sys_free(all_blocks);
return true;
}
dir_entry_idx++;
p_de++;
}
block_idx++;
p_de = (struct dir_entry *)buf; // 此时p_de已经指向扇区内最后一个完整目录项了,需要恢复p_de指向为buf
memset(buf, 0, SECTOR_SIZE); // 将buf清0,下次再用
}
sys_free(buf);
sys_free(all_blocks);
return false;
}
dir_close
用于关闭目录,实质就是调用inode_close
从内存中释放该目录的inode占用的内存,并且释放目录占用的内存,也就是解绑struct dir与inode之间的关系。需要注意的是:根目录不能被释放,因为一定会反复被用到
修改(fs/dir.c)
/* 关闭目录 */
void dir_close(struct dir *dir)
{
/************* 根目录不能关闭 ***************
*1 根目录自打开后就不应该关闭,否则还需要再次open_root_dir();
*2 root_dir所在的内存是低端1M之内,并非在堆中,free会出问题 */
if (dir == &root_dir)
{
/* 不做任何处理直接返回*/
return;
}
inode_close(dir->inode);
sys_free(dir);
}
create_dir_entry
用于初始化一个目录项,也就是给目录项指向的文件一个名字与inode索引
修改(fs/dir.c)
#include "debug.h"
/* 在内存中初始化目录项p_de */
void create_dir_entry(char *filename, uint32_t inode_no, uint8_t file_type, struct dir_entry *p_de)
{
ASSERT(strlen(filename) <= MAX_FILE_NAME_LEN);
/* 初始化目录项 */
memcpy(p_de->filename, filename, strlen(filename));
p_de->i_no = inode_no;
p_de->f_type = file_type;
}
sync_dir_entry
用于将目录项写入父目录中。核心原理就是:父目录结构体struct dir有一个指向自己inode的成员,该成员记录了该父目录文件在磁盘中存储的位置(inode的i_sectors[ ])我们可以根据这个找到目录文件,然后遍历目录文件找到空位,然后将目录项插入到这个空位即可。有些麻烦的是,需要判断inode的i_sectors[ ]这些元素是否为空,如果为空,那么我们还需要申请块用于承载目录文件。
修改(fs/dir.c)
#include "file.h"
/* 将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供 */
bool sync_dir_entry(struct dir *parent_dir, struct dir_entry *p_de, void *io_buf)
{
struct inode *dir_inode = parent_dir->inode;
uint32_t dir_size = dir_inode->i_size;
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
ASSERT(dir_size % dir_entry_size == 0); // dir_size应该是dir_entry_size的整数倍
uint32_t dir_entrys_per_sec = (512 / dir_entry_size); // 每扇区最大的目录项数目
int32_t block_lba = -1;
/* 将该目录的所有扇区地址(12个直接块+ 128个间接块)存入all_blocks */
uint8_t block_idx = 0;
uint32_t all_blocks[140] = {0}; // all_blocks保存目录所有的块
/* 将12个直接块存入all_blocks */
while (block_idx < 12)
{
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
struct dir_entry *dir_e = (struct dir_entry *)io_buf; // dir_e用来在io_buf中遍历目录项
int32_t block_bitmap_idx = -1;
/* 开始遍历所有块以寻找目录项空位,若已有扇区中没有空闲位,
* 在不超过文件大小的情况下申请新扇区来存储新目录项 */
block_idx = 0;
while (block_idx < 140)
{ // 文件(包括目录)最大支持12个直接块+128个间接块=140个块
block_bitmap_idx = -1;
if (all_blocks[block_idx] == 0)
{ // 在三种情况下分配块
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("alloc block bitmap for sync_dir_entry failed\n");
return false;
}
/* 每分配一个块就同步一次block_bitmap */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != -1);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
block_bitmap_idx = -1;
if (block_idx < 12)
{ // 若是直接块
dir_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
}
else if (block_idx == 12)
{ // 若是尚未分配一级间接块表(block_idx等于12表示第0个间接块地址为0)
dir_inode->i_sectors[12] = block_lba; // 将上面分配的块做为一级间接块表地址
block_lba = -1;
block_lba = block_bitmap_alloc(cur_part); // 再分配一个块做为第0个间接块
if (block_lba == -1)
{
block_bitmap_idx = dir_inode->i_sectors[12] - cur_part->sb->data_start_lba;
bitmap_set(&cur_part->block_bitmap, block_bitmap_idx, 0);
dir_inode->i_sectors[12] = 0;
printk("alloc block bitmap for sync_dir_entry failed\n");
return false;
}
/* 每分配一个块就同步一次block_bitmap */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != -1);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
all_blocks[12] = block_lba;
/* 把新分配的第0个间接块地址写入一级间接块表 */
ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}
else
{ // 若是间接块未分配
all_blocks[block_idx] = block_lba;
/* 把新分配的第(block_idx-12)个间接块地址写入一级间接块表 */
ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}
/* 再将新目录项p_de写入新分配的间接块 */
memset(io_buf, 0, 512);
memcpy(io_buf, p_de, dir_entry_size);
ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
dir_inode->i_size += dir_entry_size;
return true;
}
/* 若第block_idx块已存在,将其读进内存,然后在该块中查找空目录项 */
ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
/* 在扇区内查找空目录项 */
uint8_t dir_entry_idx = 0;
while (dir_entry_idx < dir_entrys_per_sec)
{
if ((dir_e + dir_entry_idx)->f_type == FT_UNKNOWN)
{ // FT_UNKNOWN为0,无论是初始化或是删除文件后,都会将f_type置为FT_UNKNOWN.
memcpy(dir_e + dir_entry_idx, p_de, dir_entry_size);
ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
dir_inode->i_size += dir_entry_size;
return true;
}
dir_entry_idx++;
}
block_idx++;
}
printk("directory is full!\n");
return false;
}
支持代码:
修改(fs/fs.h)
extern struct partition* cur_part;
由于作者在这两句代码中,
(fs/fs.c/sync_dir_entry)
/* 将12个直接块存入all_blocks */
while (block_idx < 12) {
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
仅仅从目录文件中读出了前12个直接块,导致all_blocks数组仅前12项是有非0值的。假设整个目录文件,前12个直接块指向的块已经存满了目录项,那么新的目录项只能存储至一级间接块(i_sectors[12])指向的块其中某个地址指向的目录文件中。但是代码逻辑,会必然判断出all_blocks[12] == 0,然后进入block_idx == 12,就会重新申请一个块充当一级间接块,就把原有的一级间接块覆盖了。这会导致对之前存储在间接块中的所有目录项的引用丢失,这是一个严重的数据完整性问题。
修改如下:
/* 将12个直接块存入all_blocks */
while (block_idx < 12) {
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
if (dir_inode->i_sectors[12] != 0) { // 若含有一级间接块表
ide_read(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}
函数声明:
修改(fs/dir.h)
void open_root_dir(struct partition *part);
struct dir *dir_open(struct partition *part, uint32_t inode_no);
bool search_dir_entry(struct partition *part, struct dir *pdir, const char *name, struct dir_entry *dir_e);
void dir_close(struct dir *dir);
void create_dir_entry(char *filename, uint32_t inode_no, uint8_t file_type, struct dir_entry *p_de);
bool sync_dir_entry(struct dir *parent_dir, struct dir_entry *p_de, void *io_buf);
接下来我们要实现路径解析功能。比如我们要查找某个文件,我们提供路径/home/kanshan/Desktop/test.c
,然后系统为我们找到它。解析路径的核心原理我们在本章一开始就提到了,就是那个查找/home/test.c
文件系统是如何工作的例子。
path_parse
每次调用就会解析最上层路径名,并且在缓冲区name_store
中存储解析的路径名,由于根目录是所有路径的最上层,所以不用参与解析(如/home/test.c
目录层次是/
,home
,test.c
,但是/
是所有目录最上层,我们可以忽略,所以/home/test.c
目录层次就可以认为是home
,test.c
)。比如此函数解析/home/kanshan/Desktop/test.c
,第一次调用返回/kanshan/Desktop/test.c
,name_store
中存储home
。再次调用返回/Desktop/test.c
,name_store
中存储kanshan
…
修改(fs/fs.c)
/* 将最上层路径名称解析出来 */
static char *path_parse(char *pathname, char *name_store)
{
if (pathname[0] == '/')
{ // 根目录不需要单独解析
/* 路径中出现1个或多个连续的字符'/',将这些'/'跳过,如"///a/b" */
while (*(++pathname) == '/')
;
}
/* 开始一般的路径解析 */
while (*pathname != '/' && *pathname != 0)
{
*name_store++ = *pathname++;
}
if (pathname[0] == 0)
{ // 若路径字符串为空则返回NULL
return NULL;
}
return pathname;
}
path_depth_cnt
用于返回路径深度,比如/home/charliechen114514/Desktop/test.c
路径深度就是4。原理就是循环调用path_parse
,看看返回值是不是空,决定是否继续调用`path_parse
修改(fs/fs.c)
/* 返回路径深度,比如/a/b/c,深度为3 */
int32_t path_depth_cnt(char *pathname)
{
ASSERT(pathname != NULL);
char *p = pathname;
char name[MAX_FILE_NAME_LEN]; // 用于path_parse的参数做路径解析
uint32_t depth = 0;
/* 解析路径,从中拆分出各级名称 */
p = path_parse(p, name);
while (name[0])
{
depth++;
memset(name, 0, MAX_FILE_NAME_LEN);
if (p)
{ // 如果p不等于NULL,继续分析路径
p = path_parse(p, name);
}
}
return depth;
}
函数声明
修改(fs/fs.h)
int32_t path_depth_cnt(char *pathname);
接下来实现文件检索功能
search_file
用于提供文件路径,然后返回inode索引。核心原理就是不断循环:1,调用path_parse
进行地址解析得到除根目录外最上层目录的名字;2,然后调用search_dir_entry
从搜索目录中(第一次调用这个函数,其搜索目录自然是根目录)查找 1 得到的名字;3,然后更新下一次的搜索目录(就是 2 查找出来的目录项);继续解析,然后继续查找,继续更新搜索目录…
修改(fs/fs.h)增加用于记录搜索路径的结构体
#define MAX_PATH_LEN 512 // 路径最大长度
/* 用来记录查找文件过程中已找到的上级路径,也就是查找文件过程中"走过的地方" */
struct path_search_record
{
char searched_path[MAX_PATH_LEN]; // 查找过程中的父路径
struct dir *parent_dir; // 文件或目录所在的直接父目录
enum file_types file_type; // 找到的是普通文件还是目录,找不到将为未知类型(FT_UNKNOWN)
};
修改(fs/fs.c)
/* 搜索文件pathname,若找到则返回其inode号,否则返回-1 */
static int search_file(const char *pathname, struct path_search_record *searched_record)
{
/* 如果待查找的是根目录,为避免下面无用的查找,直接返回已知根目录信息 */
if (!strcmp(pathname, "/") || !strcmp(pathname, "/.") || !strcmp(pathname, "/.."))
{
searched_record->parent_dir = &root_dir;
searched_record->file_type = FT_DIRECTORY;
searched_record->searched_path[0] = 0; // 搜索路径置空
return 0;
}
uint32_t path_len = strlen(pathname);
/* 保证pathname至少是这样的路径/x且小于最大长度 */
ASSERT(pathname[0] == '/' && path_len > 1 && path_len < MAX_PATH_LEN);
char *sub_path = (char *)pathname;
struct dir *parent_dir = &root_dir;
struct dir_entry dir_e;
/* 记录路径解析出来的各级名称,如路径"/a/b/c",
* 数组name每次的值分别是"a","b","c" */
char name[MAX_FILE_NAME_LEN] = {0};
searched_record->parent_dir = parent_dir;
searched_record->file_type = FT_UNKNOWN;
uint32_t parent_inode_no = 0; // 父目录的inode号
sub_path = path_parse(sub_path, name);
while (name[0])
{ // 若第一个字符就是结束符,结束循环
/* 记录查找过的路径,但不能超过searched_path的长度512字节 */
ASSERT(strlen(searched_record->searched_path) < 512);
/* 记录已存在的父目录 */
strcat(searched_record->searched_path, "/");
strcat(searched_record->searched_path, name);
/* 在所给的目录中查找文件 */
if (search_dir_entry(cur_part, parent_dir, name, &dir_e))
{
memset(name, 0, MAX_FILE_NAME_LEN);
/* 若sub_path不等于NULL,也就是未结束时继续拆分路径 */
if (sub_path)
{
sub_path = path_parse(sub_path, name);
}
if (FT_DIRECTORY == dir_e.f_type)
{ // 如果被打开的是目录
parent_inode_no = parent_dir->inode->i_no;
dir_close(parent_dir);
parent_dir = dir_open(cur_part, dir_e.i_no); // 更新父目录
searched_record->parent_dir = parent_dir;
continue;
}
else if (FT_REGULAR == dir_e.f_type)
{ // 若是普通文件
searched_record->file_type = FT_REGULAR;
return dir_e.i_no;
}
}
else
{ // 若找不到,则返回-1
/* 找不到目录项时,要留着parent_dir不要关闭,
* 若是创建新文件的话需要在parent_dir中创建 */
return -1;
}
}
/* 执行到此,必然是遍历了完整路径并且查找的文件或目录只有同名目录存在 */
dir_close(searched_record->parent_dir);
/* 保存被查找目录的直接父目录 */
searched_record->parent_dir = dir_open(cur_part, parent_inode_no);
searched_record->file_type = FT_DIRECTORY;
return dir_e.i_no;
}
支持代码:
修改(fs/dir.h)
extern struct dir root_dir;
接下来实现文件创建功能
file_create
用于创建文件,若成功就会返回文件描述符。核心步骤:1,创建新文件的inode,其中涉及inode位图修改,inode初始化;2,文件结构创建,因为文件结构是描述进程或线程对于文件的操作状态,创建自然也属于这种操作状态。其中涉及全局打开文件结构数组更改;3、创建对应目录项;4、同步新文件的目录项到父目录中去;同步父目录inode,父节点inode其中会涉及inode->i_size,inode->i_sectors[ ](因为文件的目录项会存在父目录文件中,可能会涉及新的块申请)。同步inode也会同步磁盘中的inode数组;创建文件的inode;inode位图;5、将新文件的inode加入打开链表;6、在创建文件进程或线程的pcb中安装文件描述符;
我们可以发现文件创建,核心就是要处理inode,目录,文件结构之间的关系。
修改(fs/file.c)
#include "string.h"
/* 创建文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_create(struct dir *parent_dir, char *filename, uint8_t flag)
{
/* 后续操作的公共缓冲区 */
void *io_buf = sys_malloc(1024);
if (io_buf == NULL)
{
printk("in file_creat: sys_malloc for io_buf failed\n");
return -1;
}
uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态
/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1)
{
printk("in file_creat: allocate inode failed\n");
return -1;
}
/* 此inode要从堆中申请内存,不可生成局部变量(函数退出时会释放)
* 因为file_table数组中的文件描述符的inode指针要指向它.*/
struct inode *new_file_inode = (struct inode *)sys_malloc(sizeof(struct inode));
if (new_file_inode == NULL)
{
printk("file_create: sys_malloc for inode failded\n");
rollback_step = 1;
goto rollback;
}
inode_init(inode_no, new_file_inode); // 初始化i结点
/* 返回的是file_table数组的下标 */
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1)
{
printk("exceed max open files\n");
rollback_step = 2;
goto rollback;
}
file_table[fd_idx].fd_inode = new_file_inode;
file_table[fd_idx].fd_pos = 0;
file_table[fd_idx].fd_flag = flag;
file_table[fd_idx].fd_inode->write_deny = false;
struct dir_entry new_dir_entry;
memset(&new_dir_entry, 0, sizeof(struct dir_entry));
create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry); // create_dir_entry只是内存操作不出意外,不会返回失败
/* 同步内存数据到硬盘 */
/* a 在目录parent_dir下安装目录项new_dir_entry, 写入硬盘后返回true,否则false */
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf))
{
printk("sync dir_entry to disk failed\n");
rollback_step = 3;
goto rollback;
}
memset(io_buf, 0, 1024);
/* b 将父目录i结点的内容同步到硬盘 */
inode_sync(cur_part, parent_dir->inode, io_buf);
memset(io_buf, 0, 1024);
/* c 将新创建文件的i结点内容同步到硬盘 */
inode_sync(cur_part, new_file_inode, io_buf);
/* d 将inode_bitmap位图同步到硬盘 */
bitmap_sync(cur_part, inode_no, INODE_BITMAP);
/* e 将创建的文件i结点添加到open_inodes链表 */
list_push(&cur_part->open_inodes, &new_file_inode->inode_tag);
new_file_inode->i_open_cnts = 1;
sys_free(io_buf);
return pcb_fd_install(fd_idx);
/*创建文件需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback:
switch (rollback_step)
{
case 3:
/* 失败时,将file_table中的相应位清空 */
memset(&file_table[fd_idx], 0, sizeof(struct file));
case 2:
sys_free(new_file_inode);
case 1:
/* 如果新文件的i结点创建失败,之前位图中分配的inode_no也要恢复 */
bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
break;
}
sys_free(io_buf);
return -1;
}
代码块
/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1) {
printk("in file_creat: allocate inode failed\n");
return -1;
}
修改成
/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1) {
printk("in file_creat: allocate inode failed\n");
goto rollback;
}
函数声明:
修改(fs/file.h)
#include "dir.h"
int32_t file_create(struct dir *parent_dir, char *filename, uint8_t flag);
sys_open
用于根据传入的路径打开或创建文件,并返回文件描述符,由于现在是写个初版,我们只实现创建功能。核心原理就是:先调用search_file
来查找指定路径是否有这个文件,如果没有,直接调用file_create
来创建文件。从查找是否存在,到最后创建之间,要排除以下情况:1、找到了,但是找到的是个目录;2、查找过程中某个中间路径就不存在,比如我要创建/home/kanshan/test.c
,但是调用search_file
之后,发现,这个/kanshan
目录都不存在;3、在最后一个路径上没找到,但是没有传入表示创建文件的标志;4、要创建的文件已经存在;
修改(fs/fs.h)
/* 打开文件的选项 */
enum oflags
{
O_RDONLY, // 只读
O_WRONLY, // 只写
O_RDWR, // 读写
O_CREAT = 4 // 创建
};
修改(fs/fs.c)
#include "file.h"
/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char *pathname, uint8_t flags)
{
/* 对目录要用dir_open,这里只有open文件 */
if (pathname[strlen(pathname) - 1] == '/')
{
printk("can`t open a directory %s\n", pathname);
return -1;
}
ASSERT(flags <= 7);
int32_t fd = -1; // 默认为找不到
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
/* 记录目录深度.帮助判断中间某个目录不存在的情况 */
uint32_t pathname_depth = path_depth_cnt((char *)pathname);
/* 先检查文件是否存在 */
int inode_no = search_file(pathname, &searched_record);
bool found = inode_no != -1 ? true : false;
if (searched_record.file_type == FT_DIRECTORY)
{
printk("can`t open a direcotry with open(), use opendir() to instead\n");
dir_close(searched_record.parent_dir);
return -1;
}
uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
/* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
if (pathname_depth != path_searched_depth)
{ // 说明并没有访问到全部的路径,某个中间目录是不存在的
printk("cannot access %s: Not a directory, subpath %s is`t exist\n",
pathname, searched_record.searched_path);
dir_close(searched_record.parent_dir);
return -1;
}
/* 若是在最后一个路径上没找到,并且并不是要创建文件,直接返回-1 */
if (!found && !(flags & O_CREAT))
{
printk("in path %s, file %s is`t exist\n",
searched_record.searched_path,
(strrchr(searched_record.searched_path, '/') + 1));
dir_close(searched_record.parent_dir);
return -1;
}
else if (found && flags & O_CREAT)
{ // 若要创建的文件已存在
printk("%s has already exist!\n", pathname);
dir_close(searched_record.parent_dir);
return -1;
}
switch (flags & O_CREAT)
{
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
}
return fd;
}
函数声明:
修改(fs/fs.h)
int32_t sys_open(const char *pathname, uint8_t flags);
filesys_init
,在list_traversal
轮询分区时调用mount_partition
以挂载分区。然后调用open_root_dir
加入打开的当前分区的根目录,初始化全局打开文件结构数组
修改(fs/fs.c/filesys_init)
sys_free(sb_buf);
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
}
为
sys_free(sb_buf);
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
/* 将当前分区的根目录打开 */
open_root_dir(cur_part);
/* 初始化文件表 */
uint32_t fd_idx = 0;
while (fd_idx < MAX_FILE_OPEN)
{
file_table[fd_idx++].fd_inode = NULL;
}
}
支持代码,修改(fs/file.h)
extern struct file file_table[MAX_FILE_OPEN];
测试代码,修改(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
int main(void) {
put_str("I am kernel\n");
init_all();
sys_open("/file1", O_CREAT);
while(1);
return 0;
}
为了方便调试,我们修改(fs/fs.c/mount_partition),调试完毕需删除增加的代码
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
sys_free(sb_buf);
为
printk("mount %s done!\n", part->name);
printk("sb1 data_start_lba: %x\n", cur_part->sb->data_start_lba);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
sys_free(sb_buf);
当启动虚拟机后,会显示sdb 1的数据区起始扇区(我是A6B,如果你一直跟着我做,那么也应该是这个),由于我们的根目录文件的第一个块就放在数据区第一个扇区中,所以我们拿到这个数据后将其 * 512,然后带入以下代命令,就可以查看hd80M.img 数据区开始 512字节的数据,我们就可以得到书上的效果
xxd -s 1365504 -l 512 hd80M.img
小节d:
文件的打开与关闭
file_open
:用于打开文件,实质就是让文件结构指向一个内存中的inode。原理:传入需要打开文件的inode数组索引,调用get_free_slot_in_global
在全局打开文件结构数组中找个空位,调用inode_open
将inode调入内存中,然后让该文件结构指向这个inode,最后调用pcb_fd_install
安装文件描述符。为了同步性,以写的方式打开的文件不应该是正在写入的文件,需要判断该文件对应inode中的正在写入标志。
修改(fs/file.c)
#include "interrupt.h"
/* 打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_open(uint32_t inode_no, uint8_t flag)
{
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1)
{
printk("exceed max open files\n");
return -1;
}
file_table[fd_idx].fd_inode = inode_open(cur_part, inode_no);
file_table[fd_idx].fd_pos = 0; // 每次打开文件,要将fd_pos还原为0,即让文件内的指针指向开头
file_table[fd_idx].fd_flag = flag;
bool *write_deny = &file_table[fd_idx].fd_inode->write_deny;
if (flag & O_WRONLY || flag & O_RDWR)
{ // 只要是关于写文件,判断是否有其它进程正写此文件
// 若是读文件,不考虑write_deny
/* 以下进入临界区前先关中断 */
enum intr_status old_status = intr_disable();
if (!(*write_deny))
{ // 若当前没有其它进程写该文件,将其占用.
*write_deny = true; // 置为true,避免多个进程同时写此文件
intr_set_status(old_status); // 恢复中断
}
else
{ // 直接失败返回
intr_set_status(old_status);
printk("file can`t be write now, try again later\n");
return -1;
}
} // 若是读文件或创建文件,不用理会write_deny,保持默认
return pcb_fd_install(fd_idx);
}
以与写有关的方式打开文件,但是遇见其他线程或进程正在写这个文件,作者代码没有释放文件结构!所以,修改
else
{ // 直接失败返回
intr_set_status(old_status);
printk("file can`t be write now, try again later\n");
return -1;
}
为
else
{ // 直接失败返回
intr_set_status(old_status);
file_table[fd_idx].fd_inode = NULL;
printk("file can`t be write now, try again later\n");
return -1;
}
之前我们实现的sys_open
只有创建文件功能,现在我们将file_open
封装进入,这样sys_open
就有了打开文件的功能了
修改(fs/fs.c/sys_open)
switch (flags & O_CREAT)
{
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
}
return fd;
为
switch (flags & O_CREAT)
{
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
break;
default:
/* 其余情况均为打开已存在文件:
* O_RDONLY,O_WRONLY,O_RDWR */
fd = file_open(inode_no, flags);
}
/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
return fd;
file_close
用传入的文件结构指针关闭文件。步骤:将文件正在写标志关闭,调用inode_close
移除内存中的inode,并解除文件结构与inode的关系。
修改(fs/file.c)
/* 关闭文件 */
int32_t file_close(struct file *file)
{
if (file == NULL)
{
return -1;
}
file->fd_inode->write_deny = false;
inode_close(file->fd_inode);
file->fd_inode = NULL; // 使文件结构可用
return 0;
}
到这里我们发现,打开关闭文件,就是在处理文件结构与inode的关系。
函数声明,修改(fs/file.h)
int32_t file_open(uint32_t inode_no, uint8_t flag);
int32_t file_close(struct file *file);
fd_local2global
,用于将文件描述符转换为全局打开文件结构数组下标,原理就是去当前正在运行的进程或线程的pcb中的fd_table数组中找到文件描述符对应的元素,这里面存的就是全局打开文件结构数组下标。
修改(fs/fs.c)
/* 将文件描述符转化为文件表的下标 */
static uint32_t fd_local2global(uint32_t local_fd)
{
struct task_struct *cur = running_thread();
int32_t global_fd = cur->fd_table[local_fd];
ASSERT(global_fd >= 0 && global_fd < MAX_FILE_OPEN);
return (uint32_t)global_fd;
}
sys_close
用传入的文件描述符关闭文件,原理:调用fd_close2global
将文件描述符转换为全局打开文件结构数组下标,然后调用file_close
关闭这个文件结构对应的文件,最后清空pcb中的文件描述符位
修改(fs/fs.c)
/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd)
{
int32_t ret = -1; // 返回值默认为-1,即失败
if (fd > 2)
{
uint32_t _fd = fd_local2global(fd);
ret = file_close(&file_table[_fd]);
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
}
return ret;
}
函数声明,修改(fs/fs.h)
int32_t sys_close(int32_t fd);
测试代码,修改(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
int main(void) {
put_str("I am kernel\n");
init_all();
uint32_t fd = sys_open("/file1", O_RDONLY);
printf("fd:%d\n", fd);
sys_close(fd);
printf("%d closed now\n", fd);
while(1);
return 0;
}
小节e:
file_write
,用于把buf中的count个字节写入文件。核心原理:传进函数的文件结构struct file
指针中有个指向操作文件inode的指针,通过这个inode中的i_size与i_sectors[ ],我们可以顺利知道文件大小与存储位置信息。先将文件已有数据的最后一块数据读出来并与将要写入的数据在缓冲区中共同拼凑成一个完整的块,然后写入磁盘。剩下的数据以块为单位继续写入磁盘即可。
修改(fs/file.c)
/* 把buf中的count个字节写入file,成功则返回写入的字节数,失败则返回-1 */
int32_t file_write(struct file *file, const void *buf, uint32_t count)
{
if ((file->fd_inode->i_size + count) > (BLOCK_SIZE * 140))
{ // 文件目前最大只支持512*140=71680字节
printk("exceed max file_size 71680 bytes, write file failed\n");
return -1;
}
uint8_t *io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL)
{
printk("file_write: sys_malloc for io_buf failed\n");
return -1;
}
uint32_t *all_blocks = (uint32_t *)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL)
{
printk("file_write: sys_malloc for all_blocks failed\n");
return -1;
}
const uint8_t *src = buf; // 用src指向buf中待写入的数据
uint32_t bytes_written = 0; // 用来记录已写入数据大小
uint32_t size_left = count; // 用来记录未写入数据大小
int32_t block_lba = -1; // 块地址
uint32_t block_bitmap_idx = 0; // 用来记录block对应于block_bitmap中的索引,做为参数传给bitmap_sync
uint32_t sec_idx; // 用来索引扇区
uint32_t sec_lba; // 扇区地址
uint32_t sec_off_bytes; // 扇区内字节偏移量
uint32_t sec_left_bytes; // 扇区内剩余字节量
uint32_t chunk_size; // 每次写入硬盘的数据块大小
int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 块索引
/* 判断文件是否是第一次写,如果是,先为其分配一个块 */
if (file->fd_inode->i_sectors[0] == 0)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc failed\n");
return -1;
}
file->fd_inode->i_sectors[0] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
/* 写入count个字节前,该文件已经占用的块数 */
uint32_t file_has_used_blocks = file->fd_inode->i_size / BLOCK_SIZE + 1;
/* 存储count字节后该文件将占用的块数 */
uint32_t file_will_use_blocks = (file->fd_inode->i_size + count) / BLOCK_SIZE + 1;
ASSERT(file_will_use_blocks <= 140);
/* 通过此增量判断是否需要分配扇区,如增量为0,表示原扇区够用 */
uint32_t add_blocks = file_will_use_blocks - file_has_used_blocks;
/* 开始将文件所有块地址收集到all_blocks,(系统中块大小等于扇区大小)
* 后面都统一在all_blocks中获取写入扇区地址 */
if (add_blocks == 0)
{
/* 在同一扇区内写入数据,不涉及到分配新扇区 */
if (file_has_used_blocks <= 12)
{ // 文件数据量将在12块之内
block_idx = file_has_used_blocks - 1; // 指向最后一个已有数据的扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
}
else
{
/* 未写入新数据之前已经占用了间接块,需要将间接块地址读进来 */
ASSERT(file->fd_inode->i_sectors[12] != 0);
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
}
else
{
/* 若有增量,便涉及到分配新扇区及是否分配一级间接块表,下面要分三种情况处理 */
/* 第一种情况:12个直接块够用*/
if (file_will_use_blocks <= 12)
{
/* 先将有剩余空间的可继续用的扇区地址写入all_blocks */
block_idx = file_has_used_blocks - 1;
ASSERT(file->fd_inode->i_sectors[block_idx] != 0);
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
/* 再将未来要用的扇区分配好后写入all_blocks */
block_idx = file_has_used_blocks; // 指向第一个要分配的新扇区
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 1 failed\n");
return -1;
}
/* 写文件时,不应该存在块未使用但已经分配扇区的情况,当文件删除时,就会把块地址清0 */
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
block_idx++; // 下一个分配的新扇区
}
}
else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12)
{
/* 第二种情况: 旧数据在12个直接块内,新数据将使用间接块*/
/* 先将有剩余空间的可继续用的扇区地址收集到all_blocks */
block_idx = file_has_used_blocks - 1; // 指向旧数据所在的最后一个扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
/* 创建一级间接块表 */
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 2 failed\n");
return -1;
}
ASSERT(file->fd_inode->i_sectors[12] == 0); // 确保一级间接块表未分配
/* 分配一级间接块索引表 */
indirect_block_table = file->fd_inode->i_sectors[12] = block_lba;
block_idx = file_has_used_blocks; // 第一个未使用的块,即本文件最后一个已经使用的直接块的下一块
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 2 failed\n");
return -1;
}
if (block_idx < 12)
{ // 新创建的0~11块直接存入all_blocks数组
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
}
else
{ // 间接块只写入到all_block数组中,待全部分配完成后一次性同步到硬盘
all_blocks[block_idx] = block_lba;
}
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
block_idx++; // 下一个新扇区
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
}
else if (file_has_used_blocks > 12)
{
/* 第三种情况:新数据占据间接块*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 已经具备了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
/* 已使用的间接块也将被读入all_blocks,无须单独收录 */
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 获取所有间接块地址
block_idx = file_has_used_blocks; // 第一个未使用的间接块,即已经使用的间接块的下一块
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 3 failed\n");
return -1;
}
all_blocks[block_idx++] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
}
}
bool first_write_block = true; // 含有剩余空间的扇区标识
/* 块地址已经收集到all_blocks中,下面开始写数据 */
file->fd_pos = file->fd_inode->i_size - 1; // 置fd_pos为文件大小-1,下面在写数据时随时更新
while (bytes_written < count)
{ // 直到写完所有数据
memset(io_buf, 0, BLOCK_SIZE);
sec_idx = file->fd_inode->i_size / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_inode->i_size % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
/* 判断此次写入硬盘的数据大小 */
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes;
if (first_write_block)
{
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
first_write_block = false;
}
memcpy(io_buf + sec_off_bytes, src, chunk_size);
ide_write(cur_part->my_disk, sec_lba, io_buf, 1);
printk("file write at lba 0x%x\n", sec_lba); // 调试,完成后去掉
src += chunk_size; // 将指针推移到下个新数据
file->fd_inode->i_size += chunk_size; // 更新文件大小
file->fd_pos += chunk_size;
bytes_written += chunk_size;
size_left -= chunk_size;
}
inode_sync(cur_part, file->fd_inode, io_buf);
sys_free(all_blocks);
sys_free(io_buf);
return bytes_written;
}
函数声明,修改(fs/file.h)
int32_t file_write(struct file *file, const void *buf, uint32_t count);
我们之前在第十二章实现printf时,实现了sys_write,由于当时没有文件系统,所以它就是调用了console_put_str打印字符串,现在我们改进sys_write
sys_write
用于将buf中连续count个字节写入文件描述符fd。如果传入的fd表示标准输出,直接调用console_put_str打印即可。如果不是,直接调用file_write
即可
修改(fs/fs.c)
#include "console.h"
/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
{
if (fd < 0)
{
printk("sys_write: fd error\n");
return -1;
}
if (fd == stdout_no)
{
char tmp_buf[1024] = {0};
memcpy(tmp_buf, buf, count);
console_put_str(tmp_buf);
return count;
}
uint32_t _fd = fd_local2global(fd);
struct file *wr_file = &file_table[_fd];
if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)
{
uint32_t bytes_written = file_write(wr_file, buf, count);
return bytes_written;
}
else
{
console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
return -1;
}
}
支持代码,修改(fs/file.h)
/* 标准输入输出描述符 */
enum std_fd {
stdin_no, // 0 标准输入
stdout_no, // 1 标准输出
stderr_no // 2 标准错误
};
函数声明,修改(fs/fs.h)
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
修改(userprog/syscall-init.c),删除原有sys_write
定义
/* 打印字符串str(未实现文件系统前的版本) */
uint32_t sys_write(char* str) {
console_put_str(str);
return strlen(str);
}
然后(userprog/syscall-init.c)添加fs.h
头文件
#include "fs.h"
最后删除(userprog/syscall-init.c)的sys_write
声明
uint32_t sys_write(char* str);
然后修改用户态的入口write
修改(fs/syscall.c)
/* 打印字符串str */
uint32_t write(char* str) {
return _syscall1(SYS_WRITE, str);
}
为
/* 把buf中count个字符写入文件描述符fd */
uint32_t write(int32_t fd, const void *buf, uint32_t count)
{
return _syscall3(SYS_WRITE, fd, buf, count);
}
修改函数声明
修改(lib/user/syscall.h)
uint32_t write(char* str);
为
uint32_t write(int32_t fd, const void *buf, uint32_t count)
最后修改printf
,改为向标准输出写入替换后的字符串
修改(lib/stdio.c)
/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
va_list args;
va_start(args, format); // 使args指向format
char buf[1024] = {0}; // 用于存储拼接后的字符串
vsprintf(buf, format, args);
va_end(args);
return write(buf);
}
为
/* 格式化输出字符串format */
uint32_t printf(const char *format, ...)
{
va_list args;
va_start(args, format); // 使args指向format
char buf[1024] = {0}; // 用于存储拼接后的字符串
vsprintf(buf, format, args);
va_end(args);
return write(1, buf, strlen(buf));
}
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
int main(void) {
put_str("I am kernel\n");
init_all();
uint32_t fd = sys_open("/file1", O_RDWR);
printf("fd:%d\n", fd);
sys_write(fd, "hello,world\n", 12);
sys_close(fd);
printf("%d closed now\n", fd);
while(1);
return 0;
}
xxd -s 屏幕显示写入地址 *512 -l 512 hd80M.img
查看磁盘中的文件
小节f:
file_read
从文件file中读取count个字节到buf。核心原理:文件结构struct file
内有个fd_pos
表示当前操作的文件内容位置,其实就是要读取的内容在文件中的起始位置,比如1个1KB大小的文本文件,fd_pos = 500,那么就是表示读取当前文件中偏移500字节的这个字符开始的内容。通过struct file
中的fd_pos
与传入函数的count
,我们可以确定要读取的内容相对于整个文件的字节偏移量。然后struct file
中有个指向操作文件inode的指 针,通过这个inode中的i_size与i_sectors[ ],我们可以顺利知道文件存储位置信息。所以,自然就能知道要读取内容在磁盘中的位置。由于我们操作单位是块,所以对于起始位置,我们要抛弃这个块前面的无用数据,对于结束位置,我们要抛弃这个块后面的无用数据。
修改(fs/file.c)
/* 从文件file中读取count个字节写入buf, 返回读出的字节数,若到文件尾则返回-1 */
int32_t file_read(struct file *file, void *buf, uint32_t count)
{
uint8_t *buf_dst = (uint8_t *)buf;
uint32_t size = count, size_left = size;
/* 若要读取的字节数超过了文件可读的剩余量, 就用剩余量做为待读取的字节数 */
if ((file->fd_pos + count) > file->fd_inode->i_size)
{
size = file->fd_inode->i_size - file->fd_pos;
size_left = size;
if (size == 0)
{ // 若到文件尾则返回-1
return -1;
}
}
uint8_t *io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL)
{
printk("file_read: sys_malloc for io_buf failed\n");
}
uint32_t *all_blocks = (uint32_t *)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL)
{
printk("file_read: sys_malloc for all_blocks failed\n");
return -1;
}
uint32_t block_read_start_idx = file->fd_pos / BLOCK_SIZE; // 数据所在块的起始地址
uint32_t block_read_end_idx = (file->fd_pos + size) / BLOCK_SIZE; // 数据所在块的终止地址
uint32_t read_blocks = block_read_start_idx - block_read_end_idx; // 如增量为0,表示数据在同一扇区
ASSERT(block_read_start_idx < 139 && block_read_end_idx < 139);
int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 获取待读的块地址
/* 以下开始构建all_blocks块地址数组,专门存储用到的块地址(本程序中块大小同扇区大小) */
if (read_blocks == 0)
{ // 在同一扇区内读数据,不涉及到跨扇区读取
ASSERT(block_read_end_idx == block_read_start_idx);
if (block_read_end_idx < 12)
{ // 待读的数据在12个直接块之内
block_idx = block_read_end_idx;
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
}
else
{ // 若用到了一级间接块表,需要将表中间接块读进来
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
}
else
{ // 若要读多个块
/* 第一种情况: 起始块和终止块属于直接块*/
if (block_read_end_idx < 12)
{ // 数据结束所在的块属于直接块
block_idx = block_read_start_idx;
while (block_idx <= block_read_end_idx)
{
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
}
else if (block_read_start_idx < 12 && block_read_end_idx >= 12)
{
/* 第二种情况: 待读入的数据跨越直接块和间接块两类*/
/* 先将直接块地址写入all_blocks */
block_idx = block_read_start_idx;
while (block_idx < 12)
{
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表
/* 再将间接块地址写入all_blocks */
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
}
else
{
/* 第三种情况: 数据在间接块中*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
}
}
/* 用到的块地址已经收集到all_blocks中,下面开始读数据 */
uint32_t sec_idx, sec_lba, sec_off_bytes, sec_left_bytes, chunk_size;
uint32_t bytes_read = 0;
while (bytes_read < size)
{ // 直到读完为止
sec_idx = file->fd_pos / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_pos % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes; // 待读入的数据大小
memset(io_buf, 0, BLOCK_SIZE);
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
memcpy(buf_dst, io_buf + sec_off_bytes, chunk_size);
buf_dst += chunk_size;
file->fd_pos += chunk_size;
bytes_read += chunk_size;
size_left -= chunk_size;
}
sys_free(all_blocks);
sys_free(io_buf);
return bytes_read;
}
函数声明,修改(fs/file.h)
int32_t file_read(struct file *file, void *buf, uint32_t count);
sys_read
根据传入的文件描述符,调用fd_local2global
将文件描述符转换成指定的全局打开文件结构数组索引,然后调用file_read
读出count字节放入buf
中
修改(fs/fs.c)
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
if (fd < 0)
{
printk("sys_read: fd error\n");
return -1;
}
ASSERT(buf != NULL);
uint32_t _fd = fd_local2global(fd);
return file_read(&file_table[_fd], buf, count);
}
函数声明,修改(fs/fs.h)
int32_t sys_read(int32_t fd, void *buf, uint32_t count);
测试代码,(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
int main(void) {
put_str("I am kernel\n");
init_all();
uint32_t fd = sys_open("/file1", O_RDWR);
printf("open /file1, fd:%d\n", fd);
char buf[64] = {0};
int read_bytes = sys_read(fd, buf, 18);
printf("1_ read %d bytes:\n%s\n", read_bytes, buf);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("2_ read %d bytes:\n%s", read_bytes, buf);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("3_ read %d bytes:\n%s", read_bytes, buf);
printf("________ close file1 and reopen ________\n");
sys_close(fd);
fd = sys_open("/file1", O_RDWR);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 24);
printf("4_ read %d bytes:\n%s", read_bytes, buf);
sys_close(fd);
while(1);
return 0;
}
小节g:
sys_lseek
用于根据传入的参照物与偏移,重置传入的文件描述符对应的全局打开文件结构中的fd_pos
。核心原理:根据传入的文件描述符,调用fd_local2global
将文件描述符转换成指定的全局打开文件结构数组索引,然后switch case对传入的参照物情况进行选择,以对fd_pos
做不同处理。参照物为SEET_SET
,那么新的fd_pos
就等于传入的偏移;参照物为SEET_CUR
,那么新的fd_pos
= 原来fd_pos
+ 传入偏移;参照物为SEET_END
,那么新的fd_pos
为原来文件大小 + 偏移(此时偏移量不出意外的话是负数)
定义参照位置,修改(fs/fs.h)
/* 文件读写位置偏移量 */
enum whence
{
SEEK_SET = 1,
SEEK_CUR,
SEEK_END
};
函数sys_lseek
,修改(fs/fs.c)
/* 重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1 */
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence)
{
if (fd < 0)
{
printk("sys_lseek: fd error\n");
return -1;
}
ASSERT(whence > 0 && whence < 4);
uint32_t _fd = fd_local2global(fd);
struct file *pf = &file_table[_fd];
int32_t new_pos = 0; // 新的偏移量必须位于文件大小之内
int32_t file_size = (int32_t)pf->fd_inode->i_size;
switch (whence)
{
/* SEEK_SET 新的读写位置是相对于文件开头再增加offset个位移量 */
case SEEK_SET:
new_pos = offset;
break;
/* SEEK_CUR 新的读写位置是相对于当前的位置增加offset个位移量 */
case SEEK_CUR: // offse可正可负
new_pos = (int32_t)pf->fd_pos + offset;
break;
/* SEEK_END 新的读写位置是相对于文件尺寸再增加offset个位移量 */
case SEEK_END: // 此情况下,offset应该为负值
new_pos = file_size + offset;
}
if (new_pos < 0 || new_pos > (file_size - 1))
{
return -1;
}
pf->fd_pos = new_pos;
return pf->fd_pos;
}
函数声明(fs/fs.h)
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence);
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t fd = sys_open("/file1", O_RDWR);
printf("open /file1, fd:%d\n", fd);
char buf[64] = {0};
int read_bytes = sys_read(fd, buf, 18);
printf("1_ read %d bytes:\n%s\n", read_bytes, buf);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("2_ read %d bytes:\n%s", read_bytes, buf);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 6);
printf("3_ read %d bytes:\n%s", read_bytes, buf);
printf("________ SEEK_SET 0 ________\n");
sys_lseek(fd, 0, SEEK_SET);
memset(buf, 0, 64);
read_bytes = sys_read(fd, buf, 24);
printf("4_ read %d bytes:\n%s", read_bytes, buf);
sys_close(fd);
while (1)
;
return 0;
}
小节h:
我们接下来实现文件删除功能,其过程是创建文件的逆过程。涉及inode与目录项。
首先是实现删除inode:
inode_delete
,传入要删除的inode在inode数组中的索引,然后删除磁盘中的这个inode。这个函数可有可无,因为inode分配是依靠inode位图来完成的,我们回收一个inode,只需要回收一个inode位图中的位即可。因为下次分配这个inode位之后,新的inode数据会覆盖旧有inode数据,这样还能避免不必要的磁盘读写。函数原理:调用inode_locate
可以将inode数组索引转换为这个inode在磁盘中的起始扇区和字节偏移。我们将这个inode所在整体扇区读出内存缓冲区中,然后将这个内存缓冲区中的inode清除,再将内存缓冲区中的数据写回磁盘中即可。
修改(fs/inode.c)
/* 将硬盘分区part上的inode清空 */
void inode_delete(struct partition *part, uint32_t inode_no, void *io_buf)
{
ASSERT(inode_no < 4096);
struct inode_position inode_pos;
inode_locate(part, inode_no, &inode_pos); // inode位置信息会存入inode_pos
ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));
char *inode_buf = (char *)io_buf;
if (inode_pos.two_sec)
{ // inode跨扇区,读入2个扇区
/* 将原硬盘上的内容先读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
/* 将inode_buf清0 */
memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
/* 用清0的内存数据覆盖磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
}
else
{ // 未跨扇区,只读入1个扇区就好
/* 将原硬盘上的内容先读出来 */
ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
/* 将inode_buf清0 */
memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
/* 用清0的内存数据覆盖磁盘 */
ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
}
}
inode_release
删除文件,包含:删除磁盘中的inode,并回收文件占用的块。这个过程包含:1、回收文件所占用的所有块,通过inode中的i_sectors[ ]即可知道占用了哪些块,然后去清除对应的块位图中的位即可,也就是并没有真正删除文件数据;2、回收inode,首先清除该inode在inode位图中对应的位,然后调用inode_delete
删除在磁盘中的这个inode
修改(fs/inode.c)
#include "file.h"
/* 回收inode的数据块和inode本身 */
void inode_release(struct partition *part, uint32_t inode_no)
{
struct inode *inode_to_del = inode_open(part, inode_no);
ASSERT(inode_to_del->i_no == inode_no);
/* 1 回收inode占用的所有块 */
uint8_t block_idx = 0, block_cnt = 12;
uint32_t block_bitmap_idx;
uint32_t all_blocks[140] = {0}; // 12个直接块+128个间接块
/* a 先将前12个直接块存入all_blocks */
while (block_idx < 12)
{
all_blocks[block_idx] = inode_to_del->i_sectors[block_idx];
block_idx++;
}
/* b 如果一级间接块表存在,将其128个间接块读到all_blocks[12~], 并释放一级间接块表所占的扇区 */
if (inode_to_del->i_sectors[12] != 0)
{
ide_read(part->my_disk, inode_to_del->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;
/* 回收一级间接块表占用的扇区 */
block_bitmap_idx = inode_to_del->i_sectors[12] - part->sb->data_start_lba;
ASSERT(block_bitmap_idx > 0);
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
/* c inode所有的块地址已经收集到all_blocks中,下面逐个回收 */
block_idx = 0;
while (block_idx < block_cnt)
{
if (all_blocks[block_idx] != 0)
{
block_bitmap_idx = 0;
block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
ASSERT(block_bitmap_idx > 0);
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
block_idx++;
}
/*2 回收该inode所占用的inode */
bitmap_set(&part->inode_bitmap, inode_no, 0);
bitmap_sync(cur_part, inode_no, INODE_BITMAP);
/****** 以下inode_delete是调试用的 ******
* 此函数会在inode_table中将此inode清0,
* 但实际上是不需要的,inode分配是由inode位图控制的,
* 硬盘上的数据不需要清0,可以直接覆盖*/
void *io_buf = sys_malloc(1024);
inode_delete(part, inode_no, io_buf);
sys_free(io_buf);
/***********************************************/
inode_close(inode_to_del);
}
函数声明,修改(fs/inode.h)
void inode_delete(struct partition *part, uint32_t inode_no, void *io_buf);
void inode_release(struct partition *part, uint32_t inode_no);
接下来是删除一个目录项:
delete_dir_entry
删除指定文件对应在磁盘中的目录项。核心原理:传入的要删除文件的父目录结构体struct dir指针内有inode成员,这个inode内有i_sectors[ ]记录着这个目录文件的存储位置,我们自然可以以块为单位从磁盘中把父目录文件读取到缓冲区中,然后遍历找到要删除的目录项,删除缓冲区内对应的目录项,然后写回缓冲区数据。
在这个过程中:如果发现该目录项所在块(非.所在块)仅有要删除的这一个目录项,那么就采取回收这个块(删除块位图中的位,然后同步块位图)的方式清除目录项。回收这个块时,还要判断这个块是不是一级间接块当中的唯一一个的那个块,如果是,则还需要回收这个一级间接块。
修改(fs/dir.c)
/* 把分区part目录pdir中编号为inode_no的目录项删除 */
bool delete_dir_entry(struct partition *part, struct dir *pdir, uint32_t inode_no, void *io_buf)
{
struct inode *dir_inode = pdir->inode;
uint32_t block_idx = 0, all_blocks[140] = {0};
/* 收集目录全部块地址 */
while (block_idx < 12)
{
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
if (dir_inode->i_sectors[12])
{
ide_read(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}
/* 目录项在存储时保证不会跨扇区 */
uint32_t dir_entry_size = part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = (SECTOR_SIZE / dir_entry_size); // 每扇区最大的目录项数目
struct dir_entry *dir_e = (struct dir_entry *)io_buf;
struct dir_entry *dir_entry_found = NULL;
uint8_t dir_entry_idx, dir_entry_cnt;
bool is_dir_first_block = false; // 目录的第1个块
/* 遍历所有块,寻找目录项 */
block_idx = 0;
while (block_idx < 140)
{
is_dir_first_block = false;
if (all_blocks[block_idx] == 0)
{
block_idx++;
continue;
}
dir_entry_idx = dir_entry_cnt = 0;
memset(io_buf, 0, SECTOR_SIZE);
/* 读取扇区,获得目录项 */
ide_read(part->my_disk, all_blocks[block_idx], io_buf, 1);
/* 遍历所有的目录项,统计该扇区的目录项数量及是否有待删除的目录项 */
while (dir_entry_idx < dir_entrys_per_sec)
{
if ((dir_e + dir_entry_idx)->f_type != FT_UNKNOWN)
{
if (!strcmp((dir_e + dir_entry_idx)->filename, "."))
{
is_dir_first_block = true;
}
else if (strcmp((dir_e + dir_entry_idx)->filename, ".") &&
strcmp((dir_e + dir_entry_idx)->filename, ".."))
{
dir_entry_cnt++; // 统计此扇区内的目录项个数,用来判断删除目录项后是否回收该扇区
if ((dir_e + dir_entry_idx)->i_no == inode_no)
{ // 如果找到此i结点,就将其记录在dir_entry_found
ASSERT(dir_entry_found == NULL); // 确保目录中只有一个编号为inode_no的inode,找到一次后dir_entry_found就不再是NULL
dir_entry_found = dir_e + dir_entry_idx;
/* 找到后也继续遍历,统计总共的目录项数 */
}
}
}
dir_entry_idx++;
}
/* 若此扇区未找到该目录项,继续在下个扇区中找 */
if (dir_entry_found == NULL)
{
block_idx++;
continue;
}
/* 在此扇区中找到目录项后,清除该目录项并判断是否回收扇区,随后退出循环直接返回 */
ASSERT(dir_entry_cnt >= 1);
/* 除目录第1个扇区外,若该扇区上只有该目录项自己,则将整个扇区回收 */
if (dir_entry_cnt == 1 && !is_dir_first_block)
{
/* a 在块位图中回收该块 */
uint32_t block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
/* b 将块地址从数组i_sectors或索引表中去掉 */
if (block_idx < 12)
{
dir_inode->i_sectors[block_idx] = 0;
}
else
{ // 在一级间接索引表中擦除该间接块地址
/*先判断一级间接索引表中间接块的数量,如果仅有这1个间接块,连同间接索引表所在的块一同回收 */
uint32_t indirect_blocks = 0;
uint32_t indirect_block_idx = 12;
while (indirect_block_idx < 140)
{
if (all_blocks[indirect_block_idx] != 0)
{
indirect_blocks++;
}
}
ASSERT(indirect_blocks >= 1); // 包括当前间接块
if (indirect_blocks > 1)
{ // 间接索引表中还包括其它间接块,仅在索引表中擦除当前这个间接块地址
all_blocks[block_idx] = 0;
ide_write(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
}
else
{ // 间接索引表中就当前这1个间接块,直接把间接索引表所在的块回收,然后擦除间接索引表块地址
/* 回收间接索引表所在的块 */
block_bitmap_idx = dir_inode->i_sectors[12] - part->sb->data_start_lba;
bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
/* 将间接索引表地址清0 */
dir_inode->i_sectors[12] = 0;
}
}
}
else
{ // 仅将该目录项清空
memset(dir_entry_found, 0, dir_entry_size);
ide_write(part->my_disk, all_blocks[block_idx], io_buf, 1);
}
/* 更新i结点信息并同步到硬盘 */
ASSERT(dir_inode->i_size >= dir_entry_size);
dir_inode->i_size -= dir_entry_size;
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(part, dir_inode, io_buf);
return true;
}
/* 所有块中未找到则返回false,若出现这种情况应该是serarch_file出错了 */
return false;
}
函数声明,修改(fs/dir.h)
bool delete_dir_entry(struct partition *part, struct dir *pdir, uint32_t inode_no, void *io_buf);
sys_unlink
用于根据传入路径,删除非目录文件。原理:首先调用search_file
搜索路径以返回文件的inode,判断该inode是否对应某个打开全局文件结构,如果是,则说明此文件正在被使用,那么就不应该被删除。如果不是,调用delete_dir_entry
删除这个文件在磁盘中的目录项,调用inode_release
删除inode对应的文件,这就完成了删除。
修改(fs/fs.c)
/* 删除文件(非目录),成功返回0,失败返回-1 */
int32_t sys_unlink(const char *pathname)
{
ASSERT(strlen(pathname) < MAX_PATH_LEN);
/* 先检查待删除的文件是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(pathname, &searched_record);
ASSERT(inode_no != 0);
if (inode_no == -1)
{
printk("file %s not found!\n", pathname);
dir_close(searched_record.parent_dir);
return -1;
}
if (searched_record.file_type == FT_DIRECTORY)
{
printk("can`t delete a direcotry with unlink(), use rmdir() to instead\n");
dir_close(searched_record.parent_dir);
return -1;
}
/* 检查是否在已打开文件列表(文件表)中 */
uint32_t file_idx = 0;
while (file_idx < MAX_FILE_OPEN)
{
if (file_table[file_idx].fd_inode != NULL && (uint32_t)inode_no == file_table[file_idx].fd_inode->i_no)
{
break;
}
file_idx++;
}
if (file_idx < MAX_FILE_OPEN)
{
dir_close(searched_record.parent_dir);
printk("file %s is in use, not allow to delete!\n", pathname);
return -1;
}
ASSERT(file_idx == MAX_FILE_OPEN);
/* 为delete_dir_entry申请缓冲区 */
void *io_buf = sys_malloc(SECTOR_SIZE + SECTOR_SIZE);
if (io_buf == NULL)
{
dir_close(searched_record.parent_dir);
printk("sys_unlink: malloc for io_buf failed\n");
return -1;
}
struct dir *parent_dir = searched_record.parent_dir;
delete_dir_entry(cur_part, parent_dir, inode_no, io_buf);
inode_release(cur_part, inode_no);
sys_free(io_buf);
dir_close(searched_record.parent_dir);
return 0; // 成功删除文件
}
函数声明,修改(fs/fs.h)
int32_t sys_unlink(const char *pathname);
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
int main(void) {
put_str("I am kernel\n");
init_all();
printf("/file1 delete %s!\n", sys_unlink("/file1") == 0 ? "done" : "fail");
while(1);
return 0;
}
为了方便调试,我们修改(fs/fs.c/mount_partition),调试完毕需删除增加的代码
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
sys_free(sb_buf);
为
printk("mount %s done!\n", part->name);
printk("sdb1's block_bitmap_lba: %x\n", sb_buf->block_bitmap_lba);
printk("sdb1's inode_bitmap_lba: %x\n", sb_buf->inode_bitmap_lba);
printk("sdb1's inode_table_lba: %x\n", sb_buf->inode_table_lba);
printk("sdb1's data_start_lba: %x\n", sb_buf->data_start_lba);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
sys_free(sb_buf);
小节i:
创建目录文件涉及的工作,其实就是涉及到了inode与目录项
- 确认待创建的新目录在文件系统上不存在。
- 为新目录创建inode。
- 为新目录分配1个块存储该目录文件中的目录项。
- 在新目录中创建两个目录项“…”和“.”,这是每个目录都必须存在的两个目录项。
- 在新目录的父目录中添加新目录的目录项。
- 将以上资源的变更同步到磁盘。
sys_mkdir
用于根据传入的路径创建目录文件。核心原理:1、调用search_file
来确认待创建的新目录文件在文件系统上不存在;2、调用inode_bitmap_alloc来为新目录分配inode索引,并调用inode_init来初始化这个inode;3、调用block_bitmap_alloc来分配一个块用于承载该目录的目录文件。同时设定inode的i_sectors[0],并调用bitmap_sync同步块位图;4、清零缓冲区,然后写入.与…两个目录项,之后将缓冲区内容写入到目录文件中,这就完成了.与…两个目录项的创建;5、创建并设定自己的目录项,调用sync_dir_entry来将目录项同步到父目录中;6、inode_sync同步父目录的inode与自己的inode,bitmap_sync同步inode位图
修改(fs/fs.c)
/* 创建目录pathname,成功返回0,失败返回-1 */
int32_t sys_mkdir(const char *pathname)
{
uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态
void *io_buf = sys_malloc(SECTOR_SIZE * 2);
if (io_buf == NULL)
{
printk("sys_mkdir: sys_malloc for io_buf failed\n");
return -1;
}
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = -1;
inode_no = search_file(pathname, &searched_record);
if (inode_no != -1)
{ // 如果找到了同名目录或文件,失败返回
printk("sys_mkdir: file or directory %s exist!\n", pathname);
rollback_step = 1;
goto rollback;
}
else
{ // 若未找到,也要判断是在最终目录没找到还是某个中间目录不存在
uint32_t pathname_depth = path_depth_cnt((char *)pathname);
uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
/* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
if (pathname_depth != path_searched_depth)
{ // 说明并没有访问到全部的路径,某个中间目录是不存在的
printk("sys_mkdir: can`t access %s, subpath %s is`t exist\n", pathname, searched_record.searched_path);
rollback_step = 1;
goto rollback;
}
}
struct dir *parent_dir = searched_record.parent_dir;
/* 目录名称后可能会有字符'/',所以最好直接用searched_record.searched_path,无'/' */
char *dirname = strrchr(searched_record.searched_path, '/') + 1;
inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1)
{
printk("sys_mkdir: allocate inode failed\n");
rollback_step = 1;
goto rollback;
}
struct inode new_dir_inode;
inode_init(inode_no, &new_dir_inode); // 初始化i结点
uint32_t block_bitmap_idx = 0; // 用来记录block对应于block_bitmap中的索引
int32_t block_lba = -1;
/* 为目录分配一个块,用来写入目录.和.. */
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("sys_mkdir: block_bitmap_alloc for create directory failed\n");
rollback_step = 2;
goto rollback;
}
new_dir_inode.i_sectors[0] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
/* 将当前目录的目录项'.'和'..'写入目录 */
memset(io_buf, 0, SECTOR_SIZE * 2); // 清空io_buf
struct dir_entry *p_de = (struct dir_entry *)io_buf;
/* 初始化当前目录"." */
memcpy(p_de->filename, ".", 1);
p_de->i_no = inode_no;
p_de->f_type = FT_DIRECTORY;
p_de++;
/* 初始化当前目录".." */
memcpy(p_de->filename, "..", 2);
p_de->i_no = parent_dir->inode->i_no;
p_de->f_type = FT_DIRECTORY;
ide_write(cur_part->my_disk, new_dir_inode.i_sectors[0], io_buf, 1);
new_dir_inode.i_size = 2 * cur_part->sb->dir_entry_size;
/* 在父目录中添加自己的目录项 */
struct dir_entry new_dir_entry;
memset(&new_dir_entry, 0, sizeof(struct dir_entry));
create_dir_entry(dirname, inode_no, FT_DIRECTORY, &new_dir_entry);
memset(io_buf, 0, SECTOR_SIZE * 2); // 清空io_buf
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf))
{ // sync_dir_entry中将block_bitmap通过bitmap_sync同步到硬盘
printk("sys_mkdir: sync_dir_entry to disk failed!\n");
rollback_step = 2;
goto rollback;
}
/* 父目录的inode同步到硬盘 */
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(cur_part, parent_dir->inode, io_buf);
/* 将新创建目录的inode同步到硬盘 */
memset(io_buf, 0, SECTOR_SIZE * 2);
inode_sync(cur_part, &new_dir_inode, io_buf);
/* 将inode位图同步到硬盘 */
bitmap_sync(cur_part, inode_no, INODE_BITMAP);
sys_free(io_buf);
/* 关闭所创建目录的父目录 */
dir_close(searched_record.parent_dir);
return 0;
/*创建文件或目录需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback: // 因为某步骤操作失败而回滚
switch (rollback_step)
{
case 2:
bitmap_set(&cur_part->inode_bitmap, inode_no, 0); // 如果新文件的inode创建失败,之前位图中分配的inode_no也要恢复
case 1:
/* 关闭所创建目录的父目录 */
dir_close(searched_record.parent_dir);
break;
}
sys_free(io_buf);
return -1;
}
函数声明,修改(fs/fs.h)
int32_t sys_mkdir(const char *pathname);
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
int main(void) {
put_str("I am kernel\n");
init_all();
printf("/dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
printf("/dir1 create %s!\n", sys_mkdir("/dir1") == 0 ? "done" : "fail");
printf("now, /dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
int fd = sys_open("/dir1/subdir1/file2", O_CREAT|O_RDWR);
if (fd != -1) {
printf("/dir1/subdir1/file2 create done!\n");
sys_write(fd, "Catch me if you can!\n", 21);
sys_lseek(fd, 0, SEEK_SET);
char buf[32] = {0};
sys_read(fd, buf, 21);
printf("/dir1/subdir1/file2 says:\n%s", buf);
sys_close(fd);
}
while(1);
return 0;
}
小节j:
sys_opendir
用于根据传入的路径打开目录。原理:调用search_file
将路径转换为inode索引,然后调用dir_open
创建目录结构并将对应的inode载入内存
修改(fs/fs.c)
/* 目录打开成功后返回目录指针,失败返回NULL */
struct dir *sys_opendir(const char *name)
{
ASSERT(strlen(name) < MAX_PATH_LEN);
/* 如果是根目录'/',直接返回&root_dir */
if (name[0] == '/' && (name[1] == 0 || name[0] == '.'))
{
return &root_dir;
}
/* 先检查待打开的目录是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(name, &searched_record);
struct dir *ret = NULL;
if (inode_no == -1)
{ // 如果找不到目录,提示不存在的路径
printk("In %s, sub path %s not exist\n", name, searched_record.searched_path);
}
else
{
if (searched_record.file_type == FT_REGULAR)
{
printk("%s is regular file!\n", name);
}
else if (searched_record.file_type == FT_DIRECTORY)
{
ret = dir_open(cur_part, inode_no);
}
}
dir_close(searched_record.parent_dir);
return ret;
}
sys_closedir
就是dir_close
的封装
修改(fs/fs.c)
/* 成功关闭目录dir返回0,失败返回-1 */
int32_t sys_closedir(struct dir *dir)
{
int32_t ret = -1;
if (dir != NULL)
{
dir_close(dir);
ret = 0;
}
return ret;
}
1234567891011
函数声明(fs/fs.h)
struct dir *sys_opendir(const char *name);
int32_t sys_closedir(struct dir *dir);
12
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
int main(void)
{
put_str("I am kernel\n");
init_all();
struct dir *p_dir = sys_opendir("/dir1/subdir1");
if (p_dir)
{
printf("/dir1/subdir1 open done!\n");
if (sys_closedir(p_dir) == 0)
{
printf("/dir1/subdir1 close done!\n");
}
else
{
printf("/dir1/subdir1 close fail!\n");
}
}
else
{
printf("/dir1/subdir1 open fail!\n");
}
while (1)
;
return 0;
}
小节k:
dir_read
用于根据传入的目录指针,一次返回一个该目录下的目录项,比如该目录文件下目录项分布:a,空,b,空,c,第一次调用返回a,第二次调用返回b,第三次调用返回c…原理:目录内有个指向自己inode的指针,该inode内有i_sectors[ ],所以能在磁盘中找到该目录文件,将其读出到缓冲区然后按需设定遍历规则即可。
代码中dir_pos
实际含义是已经返回过的目录项总大小,cur_dir_entry_pos
的含义是此次调用遍历过程中,扫描过的非空目录项总大小。当扫描到的非空目录项时,此时若cur_dir_entry_pos
与dir_pos
相等,自然就可以判断出这个非空目录项应该返回。
修改(fs/dir.c)
/* 读取目录,成功返回1个目录项,失败返回NULL */
struct dir_entry *dir_read(struct dir *dir)
{
struct dir_entry *dir_e = (struct dir_entry *)dir->dir_buf;
struct inode *dir_inode = dir->inode;
uint32_t all_blocks[140] = {0}, block_cnt = 12;
uint32_t block_idx = 0, dir_entry_idx = 0;
while (block_idx < 12)
{
all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
block_idx++;
}
if (dir_inode->i_sectors[12] != 0)
{ // 若含有一级间接块表
ide_read(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;
}
block_idx = 0;
uint32_t cur_dir_entry_pos = 0; // 当前目录项的偏移,此项用来判断是否是之前已经返回过的目录项
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = SECTOR_SIZE / dir_entry_size; // 1扇区内可容纳的目录项个数
/* 因为此目录内可能删除了某些文件或子目录,所以要遍历所有块 */
while (block_idx < block_cnt)
{
if (dir->dir_pos >= dir_inode->i_size)
{
return NULL;
}
if (all_blocks[block_idx] == 0)
{ // 如果此块地址为0,即空块,继续读出下一块
block_idx++;
continue;
}
memset(dir_e, 0, SECTOR_SIZE);
ide_read(cur_part->my_disk, all_blocks[block_idx], dir_e, 1);
dir_entry_idx = 0;
/* 遍历扇区内所有目录项 */
while (dir_entry_idx < dir_entrys_per_sec)
{
if ((dir_e + dir_entry_idx)->f_type)
{ // 如果f_type不等于0,即不等于FT_UNKNOWN
/* 判断是不是最新的目录项,避免返回曾经已经返回过的目录项 */
if (cur_dir_entry_pos < dir->dir_pos)
{
cur_dir_entry_pos += dir_entry_size;
dir_entry_idx++;
continue;
}
ASSERT(cur_dir_entry_pos == dir->dir_pos);
dir->dir_pos += dir_entry_size; // 更新为新位置,即下一个返回的目录项地址
return dir_e + dir_entry_idx;
}
dir_entry_idx++;
}
block_idx++;
}
return NULL;
}
函数声明,修改(fs/dir.h)
struct dir_entry *dir_read(struct dir *dir);
1
sys_readdir
,就是dir_read
的封装
sys_rewinddir
将目录的dir_pos置为0
修改(fs/fs.c)
/* 读取目录dir的1个目录项,成功后返回其目录项地址,到目录尾时或出错时返回NULL */
struct dir_entry *sys_readdir(struct dir *dir)
{
ASSERT(dir != NULL);
return dir_read(dir);
}
/* 把目录dir的指针dir_pos置0 */
void sys_rewinddir(struct dir *dir)
{
dir->dir_pos = 0;
}
函数声明,修改(fs/fs.h)
struct dir_entry *sys_readdir(struct dir *dir);
void sys_rewinddir(struct dir *dir);
测试代码,(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
#include "dir.h"
int main(void)
{
put_str("I am kernel\n");
init_all();
/******** 测试代码 ********/
struct dir *p_dir = sys_opendir("/dir1/subdir1");
if (p_dir)
{
printf("/dir1/subdir1 open done!\ncontent:\n");
char *type = NULL;
struct dir_entry *dir_e = NULL;
while ((dir_e = sys_readdir(p_dir)))
{
if (dir_e->f_type == FT_REGULAR)
{
type = "regular";
}
else
{
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}
if (sys_closedir(p_dir) == 0)
{
printf("/dir1/subdir1 close done!\n");
}
else
{
printf("/dir1/subdir1 close fail!\n");
}
}
else
{
printf("/dir1/subdir1 open fail!\n");
}
/******** 测试代码 ********/
while (1)
;
return 0;
}
小节l:
dir_is_empty
判断目录是否为空。原理:为空的目录对应的目录文件中只有两个目录项,分别是.和…。所以,我们直接判断目录对应的inode中的i_size是否等于两个目录项大小即可
dir_remove
传入父目录与子目录指针,在指定父目录中删除子目录。原理:判断子目录为空,然后调用delete_dir_entry
删除父目录的目录文件中的子目录目录项,最后调用inode_release
删除子目录的目录文件(子目录目录文件中只应该有.与…)
修改(myso/fs/dir.c)
/* 判断目录是否为空 */
bool dir_is_empty(struct dir *dir)
{
struct inode *dir_inode = dir->inode;
/* 若目录下只有.和..这两个目录项则目录为空 */
return (dir_inode->i_size == cur_part->sb->dir_entry_size * 2);
}
/* 在父目录parent_dir中删除child_dir */
int32_t dir_remove(struct dir *parent_dir, struct dir *child_dir)
{
struct inode *child_dir_inode = child_dir->inode;
/* 空目录只在inode->i_sectors[0]中有扇区,其它扇区都应该为空 */
int32_t block_idx = 1;
while (block_idx < 13)
{
ASSERT(child_dir_inode->i_sectors[block_idx] == 0);
block_idx++;
}
void *io_buf = sys_malloc(SECTOR_SIZE * 2);
if (io_buf == NULL)
{
printk("dir_remove: malloc for io_buf failed\n");
return -1;
}
/* 在父目录parent_dir中删除子目录child_dir对应的目录项 */
delete_dir_entry(cur_part, parent_dir, child_dir_inode->i_no, io_buf);
/* 回收inode中i_secotrs中所占用的扇区,并同步inode_bitmap和block_bitmap */
inode_release(cur_part, child_dir_inode->i_no);
sys_free(io_buf);
return 0;
}
函数声明,修改(fs/dir.h)
bool dir_is_empty(struct dir *dir);
int32_t dir_remove(struct dir *parent_dir, struct dir *child_dir);
12
sys_rmdir
用于根据传入路径删除空目录。原理:先调用search_file
查找这个路径对应目录的inode,如果存在且该inode对应的确实是个目录文件,然后调用dir_open
将该inode调入内存并创建对应的struct dir,调用dir_is_empty
判断该目录为空,最后调用dir_remove
删除该目录。
修改(fs/fs.c)
/* 删除空目录,成功时返回0,失败时返回-1*/
int32_t sys_rmdir(const char *pathname)
{
/* 先检查待删除的文件是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(pathname, &searched_record);
ASSERT(inode_no != 0);
int retval = -1; // 默认返回值
if (inode_no == -1)
{
printk("In %s, sub path %s not exist\n", pathname, searched_record.searched_path);
}
else
{
if (searched_record.file_type == FT_REGULAR)
{
printk("%s is regular file!\n", pathname);
}
else
{
struct dir *dir = dir_open(cur_part, inode_no);
if (!dir_is_empty(dir))
{ // 非空目录不可删除
printk("dir %s is not empty, it is not allowed to delete a nonempty directory!\n", pathname);
}
else
{
if (!dir_remove(searched_record.parent_dir, dir))
{
retval = 0;
}
}
dir_close(dir);
}
}
dir_close(searched_record.parent_dir);
return retval;
}
函数声明,修改(fs/fs.h)
int32_t sys_rmdir(const char *pathname);
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
#include "dir.h"
int main(void)
{
put_str("I am kernel\n");
init_all();
/******** 测试代码 ********/
printf("/dir1 content before delete /dir1/subdir1:\n");
struct dir *dir = sys_opendir("/dir1/");
char *type = NULL;
struct dir_entry *dir_e = NULL;
while ((dir_e = sys_readdir(dir)))
{
if (dir_e->f_type == FT_REGULAR)
{
type = "regular";
}
else
{
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}
printf("try to delete nonempty directory /dir1/subdir1\n");
if (sys_rmdir("/dir1/subdir1") == -1)
{
printf("sys_rmdir: /dir1/subdir1 delete fail!\n");
}
printf("try to delete /dir1/subdir1/file2\n");
if (sys_rmdir("/dir1/subdir1/file2") == -1)
{
printf("sys_rmdir: /dir1/subdir1/file2 delete fail!\n");
}
if (sys_unlink("/dir1/subdir1/file2") == 0)
{
printf("sys_unlink: /dir1/subdir1/file2 delete done\n");
}
printf("try to delete directory /dir1/subdir1 again\n");
if (sys_rmdir("/dir1/subdir1") == 0)
{
printf("/dir1/subdir1 delete done!\n");
}
printf("/dir1 content after delete /dir1/subdir1:\n");
sys_rewinddir(dir);
while ((dir_e = sys_readdir(dir)))
{
if (dir_e->f_type == FT_REGULAR)
{
type = "regular";
}
else
{
type = "directory";
}
printf(" %s %s\n", type, dir_e->filename);
}
/******** 测试代码 ********/
while (1)
;
return 0;
}
小节m:
任务的工作目录
get_parent_dir_inode_nr
传入子目录inode索引返回父目录的inode索引。原理:调用inode_open将子目录对应的inode加载到内存中,取出inode其中i_sectors[0]地址,加载这个目录文件到内存中,找到…对应的目录项,从中取出父目录对应的inode索引返回即可。
修改(fs/fs.c)
/* 获得父目录的inode编号 */
static uint32_t get_parent_dir_inode_nr(uint32_t child_inode_nr, void *io_buf)
{
struct inode *child_dir_inode = inode_open(cur_part, child_inode_nr);
/* 目录中的目录项".."中包括父目录inode编号,".."位于目录的第0块 */
uint32_t block_lba = child_dir_inode->i_sectors[0];
ASSERT(block_lba >= cur_part->sb->data_start_lba);
inode_close(child_dir_inode);
ide_read(cur_part->my_disk, block_lba, io_buf, 1);
struct dir_entry *dir_e = (struct dir_entry *)io_buf;
/* 第0个目录项是".",第1个目录项是".." */
ASSERT(dir_e[1].i_no < 4096 && dir_e[1].f_type == FT_DIRECTORY);
return dir_e[1].i_no; // 返回..即父目录的inode编号
}
get_child_dir_name
通过传入的父目录inode索引与子目录inode索引,返回子目录的名字。原理:调用inode_open
将父目录的inode载入内存,然后通过inode中的i_sectors[ ]加载父目录的目录文件到内存中,遍历其中的目录项,遍历过程中比对目录项的i_nr是否与子目录的inode索引相等,如果是,则拷贝名字到缓冲区中。
修改(fs/fs.c)
/* 在inode编号为p_inode_nr的目录中查找inode编号为c_inode_nr的子目录的名字,
* 将名字存入缓冲区path.成功返回0,失败返-1 */
static int get_child_dir_name(uint32_t p_inode_nr, uint32_t c_inode_nr, char *path, void *io_buf)
{
struct inode *parent_dir_inode = inode_open(cur_part, p_inode_nr);
/* 填充all_blocks,将该目录的所占扇区地址全部写入all_blocks */
uint8_t block_idx = 0;
uint32_t all_blocks[140] = {0}, block_cnt = 12;
while (block_idx < 12)
{
all_blocks[block_idx] = parent_dir_inode->i_sectors[block_idx];
block_idx++;
}
if (parent_dir_inode->i_sectors[12])
{ // 若包含了一级间接块表,将共读入all_blocks.
ide_read(cur_part->my_disk, parent_dir_inode->i_sectors[12], all_blocks + 12, 1);
block_cnt = 140;
}
inode_close(parent_dir_inode);
struct dir_entry *dir_e = (struct dir_entry *)io_buf;
uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
uint32_t dir_entrys_per_sec = (512 / dir_entry_size);
block_idx = 0;
/* 遍历所有块 */
while (block_idx < block_cnt)
{
if (all_blocks[block_idx])
{ // 如果相应块不为空则读入相应块
ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
uint8_t dir_e_idx = 0;
/* 遍历每个目录项 */
while (dir_e_idx < dir_entrys_per_sec)
{
if ((dir_e + dir_e_idx)->i_no == c_inode_nr)
{
strcat(path, "/");
strcat(path, (dir_e + dir_e_idx)->filename);
return 0;
}
dir_e_idx++;
}
}
block_idx++;
}
return -1;
}
sys_getcwd
用于解析当前正在运行进程或线程的绝对工作路径。原理:我们之前已经为PCB中添加了cwd_inode_nr用于表示任务工作目录inode索引,假设现在这个inode索引是正确的。首先调用get_parent_dir_inode_nr
得到父目录inode索引,此时原来的cwd_inode_nr就变成了子目录inode索引,然后就可以调用get_child_dir_name
得到子目录名称,然后将父目录inode索引转换成新的子目录索引,又调用get_parent_dir_inode_nr
得到新的父目录索引…如此循环,缓冲区中就会存储反转的绝对路径。比如一个进程工作在/home/kanshan/test下,缓冲区就会存入/test/kanshan/home,所以我们最后把这个路径反转过来即可。
修改(fs/fs.c)
/* 把当前工作目录绝对路径写入buf, size是buf的大小.
当buf为NULL时,由操作系统分配存储工作路径的空间并返回地址
失败则返回NULL */
char *sys_getcwd(char *buf, uint32_t size)
{
/* 确保buf不为空,若用户进程提供的buf为NULL,
系统调用getcwd中要为用户进程通过malloc分配内存 */
ASSERT(buf != NULL);
void *io_buf = sys_malloc(SECTOR_SIZE);
if (io_buf == NULL)
{
return NULL;
}
struct task_struct *cur_thread = running_thread();
int32_t parent_inode_nr = 0;
int32_t child_inode_nr = cur_thread->cwd_inode_nr;
ASSERT(child_inode_nr >= 0 && child_inode_nr < 4096); // 最大支持4096个inode
/* 若当前目录是根目录,直接返回'/' */
if (child_inode_nr == 0)
{
buf[0] = '/';
buf[1] = 0;
return buf;
}
memset(buf, 0, size);
char full_path_reverse[MAX_PATH_LEN] = {0}; // 用来做全路径缓冲区
/* 从下往上逐层找父目录,直到找到根目录为止.
* 当child_inode_nr为根目录的inode编号(0)时停止,
* 即已经查看完根目录中的目录项 */
while ((child_inode_nr))
{
parent_inode_nr = get_parent_dir_inode_nr(child_inode_nr, io_buf);
if (get_child_dir_name(parent_inode_nr, child_inode_nr, full_path_reverse, io_buf) == -1)
{ // 或未找到名字,失败退出
sys_free(io_buf);
return NULL;
}
child_inode_nr = parent_inode_nr;
}
ASSERT(strlen(full_path_reverse) <= size);
/* 至此full_path_reverse中的路径是反着的,
* 即子目录在前(左),父目录在后(右) ,
* 现将full_path_reverse中的路径反置 */
char *last_slash; // 用于记录字符串中最后一个斜杠地址
while ((last_slash = strrchr(full_path_reverse, '/')))
{
uint16_t len = strlen(buf);
strcpy(buf + len, last_slash);
/* 在full_path_reverse中添加结束字符,做为下一次执行strcpy中last_slash的边界 */
*last_slash = 0;
}
sys_free(io_buf);
return buf;
}
sys_chdir
传入一个目录的路径,然后改变当前正在运行的进程或线程的工作目录索引。原理:调用search_file
将传入路径解析成一个对应的inode索引,然后直接去修改当前任务pcb的cwd_inode_nr为之前返回的inode索引就行了。
/* 更改当前工作目录为绝对路径path,成功则返回0,失败返回-1 */
int32_t sys_chdir(const char *path)
{
int32_t ret = -1;
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(path, &searched_record);
if (inode_no != -1)
{
if (searched_record.file_type == FT_DIRECTORY)
{
running_thread()->cwd_inode_nr = inode_no;
ret = 0;
}
else
{
printk("sys_chdir: %s is regular file or other!\n", path);
}
}
dir_close(searched_record.parent_dir);
return ret;
}
函数声明,修改(fs/fs.h)
char *sys_getcwd(char *buf, uint32_t size);
int32_t sys_chdir(const char *path);
测试代码(kernle/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
#include "dir.h"
int main(void) {
put_str("I am kernel\n");
init_all();
/******** 测试代码 ********/
char cwd_buf[32] = {0};
sys_getcwd(cwd_buf, 32);
printf("cwd:%s\n", cwd_buf);
sys_chdir("/dir1");
printf("change cwd now\n");
sys_getcwd(cwd_buf, 32);
printf("cwd:%s\n", cwd_buf);
/******** 测试代码 ********/
while(1);
return 0;
}
小节n:
获得文件属性
修改(fs/fs.h),增加记录文件属性的结构体定义
/* 文件属性结构体 */
struct stat
{
uint32_t st_ino; // inode编号
uint32_t st_size; // 尺寸
enum file_types st_filetype; // 文件类型
};
sys_stat
传入一个路径,调用search_file解析该路径,获得该路径对应的inode索引,然后调用inode_open
将该inode调入内存,然后赋值struct stat对应的成员即可
修改(fs/fs.c)
/* 在buf中填充文件结构相关信息,成功时返回0,失败返回-1 */
int32_t sys_stat(const char *path, struct stat *buf)
{
/* 若直接查看根目录'/' */
if (!strcmp(path, "/") || !strcmp(path, "/.") || !strcmp(path, "/.."))
{
buf->st_filetype = FT_DIRECTORY;
buf->st_ino = 0;
buf->st_size = root_dir.inode->i_size;
return 0;
}
int32_t ret = -1; // 默认返回值
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record)); // 记得初始化或清0,否则栈中信息不知道是什么
int inode_no = search_file(path, &searched_record);
if (inode_no != -1)
{
struct inode *obj_inode = inode_open(cur_part, inode_no); // 只为获得文件大小
buf->st_size = obj_inode->i_size;
inode_close(obj_inode);
buf->st_filetype = searched_record.file_type;
buf->st_ino = inode_no;
ret = 0;
}
else
{
printk("sys_stat: %s not found\n", path);
}
dir_close(searched_record.parent_dir);
return ret;
}
函数声明,修改(fs/fs.h)
int32_t sys_stat(const char *path, struct stat *buf);
测试代码(kernle/main.c)
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
int main(void)
{
put_str("I am kernel\n");
init_all();
/******** 测试代码 ********/
struct stat obj_stat;
sys_stat("/", &obj_stat);
printf("/`s info\n i_no:%d\n size:%d\n filetype:%s\n",
obj_stat.st_ino, obj_stat.st_size,
obj_stat.st_filetype == 2 ? "directory" : "regular");
sys_stat("/dir1", &obj_stat);
printf("/dir1`s info\n i_no:%d\n size:%d\n filetype:%s\n",
obj_stat.st_ino, obj_stat.st_size,
obj_stat.st_filetype == 2 ? "directory" : "regular");
/******** 测试代码 ********/
while (1)
;
return 0;
}
与系统交互
这一小节,我们要实现fork
fork是用于复制进程的,也就是根据父进程复制出一个子进程。但是由于他们本质是两个进程,所以还是有很多不相同的地方,比如独立的资源,单独的pid之类的。
有这样一段代码
#include <unistd.h>
#include <stdio.h>
int main() {
int pid = fork();
if (pid == -1)
return 1;
printf("who am I ? my pid is %d\n", getpid());
sleep (5) ;
return 0;
}
当执行完fork()后,fork之后的代码会由于属于两个进程(调用fork的主进程与被复制出来的子进程)而被执行两次(自然是主进程与子进程各执行一次)。
由于fork复制进程,而且复制步骤是在fork自己的代码结束前就完成(假设fork代码1000行,第800行就完成了复制),所以fork代码最后一行的return 就会被执行两次。对于父进程来说,fork会返回子进程pid。对于子进程来说,fork会返回0。我们就可以根据fork返回的不同值来区别父子进程,以让父子进程执行不同的代码。比如:
if (pid) {
printf("I am father, my pid is d\n",getpid());
sleep(5);
return 0;
}
else {
printf("I am child, my pid is d\n",getpid());
sleep(5);
return 0;
}
现在开始实现fork,先实现一些基础设施函数
fork_pid
就是封装了allocate_pid
,因为allocate_pid
之前实现的时候有关键字static,所以作者为了不去修改这个,就采取了进一步封装
修改(thread/thread.c)
/* fork进程时为其分配pid,因为allocate_pid已经是静态的,别的文件无法调用.
不想改变函数定义了,故定义fork_pid函数来封装一下。*/
pid_t fork_pid(void)
{
return allocate_pid();
}
函数声明,修改(thread/thread.h)
pid_t fork_pid(void);
get_a_page_without_opvaddrbitmap
用于为指定的虚拟地址创建物理页映射,与get_a_page
相比,少了操作进程pcb中的虚拟内存池位图
修改(kernel/memory.c)
/* 安装1页大小的vaddr,专门针对fork时虚拟地址位图无须操作的情况 */
void *get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr)
{
struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
void *page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
page_table_add((void *)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void *)vaddr;
}
函数声明,修改(kernel/memory.h)
void *get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);
copy_pcb_vaddrbitmap_stack0
用于根据传入的父子进程pcb指针,先复制整个父进程pcb内容到子进程pcb中,然后再针对设置子进程pcb内容,包含:pid, elapsed_ticks, status, ticks, parent_pid, general_tag, all_list_tag, u_block_desc, userprog_vaddr(让子进程拥有自己的用户虚拟地址空间内存池,但是其位图是拷贝父进程的)。这个过程中,内核栈中的内容被完全拷贝了。
(userprog/fork.c)
#include "fork.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "string.h"
#include "debug.h"
#include "process.h"
/* 将父进程的pcb、虚拟地址位图拷贝给子进程 */
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct *child_thread, struct task_struct *parent_thread)
{
/* a 复制pcb所在的整个页,里面包含进程pcb信息及特级0极的栈,里面包含了返回地址, 然后再单独修改个别部分 */
memcpy(child_thread, parent_thread, PG_SIZE);
child_thread->pid = fork_pid();
child_thread->elapsed_ticks = 0;
child_thread->status = TASK_READY;
child_thread->ticks = child_thread->priority; // 为新进程把时间片充满
child_thread->parent_pid = parent_thread->pid;
child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
child_thread->all_list_tag.prev = child_thread->all_list_tag.next = NULL;
block_desc_init(child_thread->u_block_desc);
/* b 复制父进程的虚拟地址池的位图 */
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
void *vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
if (vaddr_btmp == NULL)
return -1;
/* 此时child_thread->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
* 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);
child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;
/* 调试用 */
ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
strcat(child_thread->name, "_fork");
return 0;
}
copy_body_stack3
用于根据传入的父子进程pcb指针,复制进程的用户空间堆与栈中的数据。核心原理:遍历父进程的userprog_vaddr当中的虚拟地址空间位图,来判断父进程的用户虚拟地址空间中是否有数据。如果有,就拷贝到内核空间的中转区中,然后调用page_dir_activate
,切换到子进程页表,调用get_a_page_without_opvaddrbitmap
为子进程特定虚拟地址申请一个物理页(其中并不涉及子进程userprog_vaddr中的位图修改),然后从内核中转区中把数据拷贝到子进程相同的虚拟地址内。
修改(userprog/fork.c)
extern void intr_exit(void);
/* 复制子进程的进程体(代码和数据)及用户栈 */
static void copy_body_stack3(struct task_struct *child_thread, struct task_struct *parent_thread, void *buf_page)
{
uint8_t *vaddr_btmp = parent_thread->userprog_vaddr.vaddr_bitmap.bits;
uint32_t btmp_bytes_len = parent_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
uint32_t vaddr_start = parent_thread->userprog_vaddr.vaddr_start;
uint32_t idx_byte = 0;
uint32_t idx_bit = 0;
uint32_t prog_vaddr = 0;
/* 在父进程的用户空间中查找已有数据的页 */
while (idx_byte < btmp_bytes_len)
{
if (vaddr_btmp[idx_byte])
{
idx_bit = 0;
while (idx_bit < 8)
{
if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte])
{
prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
/* 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间 */
/* a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,
目的是下面切换到子进程的页表后,还能访问到父进程的数据*/
memcpy(buf_page, (void *)prog_vaddr, PG_SIZE);
/* b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中 */
page_dir_activate(child_thread);
/* c 申请虚拟地址prog_vaddr */
get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);
/* d 从内核缓冲区中将父进程数据复制到子进程的用户空间 */
memcpy((void *)prog_vaddr, buf_page, PG_SIZE);
/* e 恢复父进程页表 */
page_dir_activate(parent_thread);
}
idx_bit++;
}
}
idx_byte++;
}
}
build_child_stack
用于修改子进程的返回值和设定其内核栈。子进程返回0原理:我们之前构建系统调用机制时,系统调用的返回值会放入内核栈中的中断栈(intr_stack
)eax的位置,这样中断退出(intr_exit
)就会push eax时将返回值放入eax中。所以我们将子进程的内核栈中断栈eax的值改成0。
我们的子进程上机运行是通过让自己就绪之后,等待某个时钟中断调用switch_to函数上机
mov eax, [esp + 24]
mov esp, [eax]
pop ebp
pop ebx
pop edi
pop esi
ret
switch_to会从子进程的pcb中找到内核栈的栈顶放入esp中,然后执行switch_to的那4条pop和ret指令,我们现在经过拷贝后的子进程内核栈布局如图:
所以,我们直接去用子进程这样的内核栈布局肯定不行,要人为去修改成
也就是在intr_stack前面增加switch_to栈(也就是书p694提到的thread_stack),让pcb最顶端的esp指向switch_to栈栈顶,并且switch_to栈中返回地址要填上intr_exit
函数地址。这样执行ret之后,就能去执行intr_exit
,并利用intr_stack执行中断返回,由于intr_stack中拷贝了父进程进入中断时的用户栈信息,cs: ip 信息,所以中断退出后,子进程将会继续执行父进程之后的代码。
修改(userprog/fork.c)
/* 为子进程构建thread_stack和修改返回值 */
static int32_t build_child_stack(struct task_struct *child_thread)
{
/* a 使子进程pid返回值为0 */
/* 获取子进程0级栈栈顶 */
struct intr_stack *intr_0_stack = (struct intr_stack *)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
/* 修改子进程的返回值为0 */
intr_0_stack->eax = 0;
/* b 为switch_to 构建 struct thread_stack,将其构建在紧临intr_stack之下的空间*/
uint32_t *ret_addr_in_thread_stack = (uint32_t *)intr_0_stack - 1;
/*** 这三行不是必要的,只是为了梳理thread_stack中的关系 ***/
uint32_t *esi_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 2;
uint32_t *edi_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 3;
uint32_t *ebx_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 4;
/**********************************************************/
/* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
即esp为"(uint32_t*)intr_0_stack - 5" */
uint32_t *ebp_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 5;
/* switch_to的返回地址更新为intr_exit,直接从中断返回 */
*ret_addr_in_thread_stack = (uint32_t)intr_exit;
/* 下面这两行赋值只是为了使构建的thread_stack更加清晰,其实也不需要,
* 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */
*ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack =
*edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;
/*********************************************************/
/* 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶 */
child_thread->self_kstack = ebp_ptr_in_thread_stack;
return 0;
}
update_inode_open_cnts
由于fork出来的子进程几乎和父进程一样,所以父进程打开的文件,子进程也要打开。所以,父进程的全局打开文件结构中记录文件打开的次数都需要 + 1。原理:遍历进程pcb(父,子均可)中的文件描述符,找到对应的全局打开文件结构索引就行了
修改(user/fork.c)
#include <file.h>
/* 更新inode打开数 */
static void update_inode_open_cnts(struct task_struct *thread)
{
int32_t local_fd = 3, global_fd = 0;
while (local_fd < MAX_FILES_OPEN_PER_PROC)
{
global_fd = thread->fd_table[local_fd];
ASSERT(global_fd < MAX_FILE_OPEN);
if (global_fd != -1)
{
file_table[global_fd].fd_inode->i_open_cnts++;
}
local_fd++;
}
}
copy_process
就是fork时用于复制父进程资源的函数,就是前面函数的封装。原理:调用copy_pcb_vaddrbitmap_stack0
复制父进程的pcb、虚拟地址位图、内核栈到子进程;然后调用create_page_dir
为子进程创建页表,这个页表已经包含了内核地址空间的映射;然后调用copy_body_stack3
复制进程的用户空间堆与栈中的数据;然后调用build_child_stack
用于修改子进程的返回值和设定其内核栈;最后调用update_inode_open_cnts
更新inode的打开数。
修改(user/fork.c)
/* 拷贝父进程本身所占资源给子进程 */
static int32_t copy_process(struct task_struct *child_thread, struct task_struct *parent_thread)
{
/* 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转 */
void *buf_page = get_kernel_pages(1);
if (buf_page == NULL)
{
return -1;
}
/* a 复制父进程的pcb、虚拟地址位图、内核栈到子进程 */
if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1)
{
return -1;
}
/* b 为子进程创建页表,此页表仅包括内核空间 */
child_thread->pgdir = create_page_dir();
if (child_thread->pgdir == NULL)
{
return -1;
}
/* c 复制父进程进程体及用户栈给子进程 */
copy_body_stack3(child_thread, parent_thread, buf_page);
/* d 构建子进程thread_stack和修改返回值pid */
build_child_stack(child_thread);
/* e 更新文件inode的打开数 */
update_inode_open_cnts(child_thread);
mfree_page(PF_KERNEL, buf_page, 1);
return 0;
}
sys_fork
用于复制出一个进程,并将其加入就绪队列
(userprog/fork.c)
#include "interrupt.h"
/* fork子进程,内核线程不可直接调用 */
pid_t sys_fork(void)
{
struct task_struct *parent_thread = running_thread();
struct task_struct *child_thread = get_kernel_pages(1); // 为子进程创建pcb(task_struct结构)
if (child_thread == NULL)
{
return -1;
}
ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);
if (copy_process(child_thread, parent_thread) == -1)
{
return -1;
}
/* 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行 */
ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));
list_append(&thread_ready_list, &child_thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));
list_append(&thread_all_list, &child_thread->all_list_tag);
return child_thread->pid; // 父进程返回子进程的pid
}
函数声明(userprog/fork.h)
#ifndef __USERPROG_FORK_H
#define __USERPROG_FORK_H
#include "stdint.h"
pid_t sys_fork(void);
#endif
然后我们添加fork
系统调用
添加系统调用号,修改(lib/user/syscall.h)
#include "thread.h"
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK
};
用户态系统调用入口,修改(lib/user/syscall.c)
#include "thread.h"
/* 派生子进程,返回子进程pid */
pid_t fork(void)
{
return _syscall0(SYS_FORK);
}
函数声明,修改(lib/user/syscall.h)
pid_t fork(void);
系统调用表中添加实际系统调用函数,修改(userprog/syscall-init.c)
#include "fork.h"
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
put_str("syscall_init done\n");
}
init
进程:我们学习Linux做法,让init
作为pid为1的用户进程,所以必须要放在主线程创建之创建。后续所有的进程都是它的孩子,它还负责所有子进程的资源回收
修改(thread/thread.c/thread_init),让init
的pid为1
extern void init(void);
/* 初始化线程环境 */
void thread_init(void)
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 先创建第一个用户进程:init */
process_execute(init, "init"); // 放在第一个初始化,这是第一个进程,init进程的pid为1
/* 将当前main函数创建为线程 */
make_main_thread();
/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread_init done\n");
}
测试代码与init
进程实现
(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
while (1)
;
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{
printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);
}
else
{
printf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);
}
while (1)
;
}
编译运行会报页错误,经过排查,修改(thread/thread.c/thread_create)
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
// pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
// self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
// pthread->self_kstack -= sizeof(struct thread_stack);
pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
为
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
// self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
// pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
// pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
小节b:
获取键盘输入
sys_read
用于从指定文件描述符中获取conunt字节数据,如果文件描述符是stdin_no,那么直接循环调用ioq_getchar
从键盘获取内容,否则调用file_read
从文件中读取内容
sys_put_char
用于向屏幕输出一个字符
修改(fs/fs.c/sys_read)
#include "keyboard.h"
#include "ioqueue.h"
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
ASSERT(buf != NULL);
int32_t ret = -1;
if (fd < 0 || fd == stdout_no || fd == stderr_no)
{
printk("sys_read: fd error\n");
}
else if (fd == stdin_no)
{
char *buffer = buf;
uint32_t bytes_read = 0;
while (bytes_read < count)
{
*buffer = ioq_getchar(&kbd_buf);
bytes_read++;
buffer++;
}
ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
}
else
{
uint32_t _fd = fd_local2global(fd);
ret = file_read(&file_table[_fd], buf, count);
}
return ret;
}
/* 向屏幕输出一个字符 */
void sys_putchar(char char_asci)
{
console_put_char(char_asci);
}
函数声明,修改(fs/fs.h)
void sys_putchar(char char_asci);
cls_screen
用于清空屏幕,核心原理:向代表80列×25行,共2000个字符位置的内存写入空格符,然后设定光标位置为左上角(即位置0)
修改(lib/kernel/print.S)
global cls_screen
cls_screen:
pushad
; 由于用户程序的cpl为3,显存段的dpl为0,故用于显存段的选择子gs在低于自己特权的环境中为0,
; 导致用户程序再次进入中断后,gs为0,故直接在put_str中每次都为gs赋值.
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入gs,须由ax中转
mov gs, ax
mov ebx, 0
mov ecx, 80*25
.cls:
mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov ebx, 0
.set_cursor: ;直接把set_cursor搬过来用,省事
;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret
函数声明,修改(lib/kernel/print.h)
void cls_screen(void);
将sys_read
、sys_putchar
、cls_screen
做成系统调用
添加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR
};
准备好read
、put_char
与clear
的用户态入口,修改(lib/user/syscall.c)
/* 从文件描述符fd中读取count个字节到buf */
int32_t read(int32_t fd, void *buf, uint32_t count)
{
return _syscall3(SYS_READ, fd, buf, count);
}
/* 输出一个字符 */
void putchar(char char_asci)
{
_syscall1(SYS_PUTCHAR, char_asci);
}
/* 清空屏幕 */
void clear(void)
{
_syscall0(SYS_CLEAR);
}
然后声明函数,修改(lib/user/syscall.h)
int32_t read(int32_t fd, void* buf, uint32_t count);
void putchar(char char_asci);
void clear(void);
将系统调用实际执行程序,添加至系统调用表中,修改(lib/user/syscall.c)
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
put_str("syscall_init done\n");
}
shell是用户与操作系统之间交互的接口,我们天天使用的Linux终端就是一个shell。它的功能就是获取用户的键盘输入,然后从中解析命令,然后根据命令去执行对应的动作。
print_prompt
用于输出命令提示符,也就是我们在终端输入命令时,前面那串字符
(shell/shell.c)
#include "shell.h"
#include "stdio.h"
char cwd_cache[64] = {0};
/* 输出提示符 */
void print_prompt(void)
{
printf("[rabbit@localhost %s]$ ", cwd_cache);
}
readline
循环调用read
从键盘输入缓冲读取字符,每次读取一个,最多读入count个字节到buf。根据每次读入的值不同,处理方式也不同:/n,/r表示按下enter键,用户输入命令结束,缓冲区输入个0表示命令字符串结尾。/b表示按下退格键,就删除一个字符。普通字符就直接读入buf。每种字符都调用了putchar
进行打印,是因为我们的键盘中断处理函数已经删除打印功能。
修改(shell/shell.c)
#include "file.h"
#include "debug.h"
#include "syscall.h"
/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char *buf, int32_t count)
{
ASSERT(buf != NULL && count > 0);
char *pos = buf;
while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count)
{ // 在不出错情况下,直到找到回车符才返回
switch (*pos)
{
/* 找到回车或换行符后认为键入的命令结束,直接返回 */
case '\n':
case '\r':
*pos = 0; // 添加cmd_line的终止字符0
putchar('\n');
return;
case '\b':
if (buf[0] != '\b')
{ // 阻止删除非本次输入的信息
--pos; // 退回到缓冲区cmd_line中上一个字符
putchar('\b');
}
break;
/* 非控制键则输出字符 */
default:
putchar(*pos);
pos++;
}
}
printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}
my_shell
就是shell进程,不断循环:调用print_prompt
输出命令提示符,然后调用readline
获取用户输入
修改(shell/shell.c)
#include "string.h"
#define cmd_len 128 // 最大支持键入128个字符的命令行输入
static char cmd_line[cmd_len] = {0};
/* 简单的shell */
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(cmd_line, 0, cmd_len);
readline(cmd_line, cmd_len);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
}
PANIC("my_shell: should not be here");
}
函数声明,(shell/shell.h)
#ifndef __KERNEL_SHELL_H
#define __KERNEL_SHELL_H
void print_prompt(void);
void my_shell(void);
#endif
我们让init
来开启shell
,修改(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "debug.h"
#include "shell.h"
#include "console.h"
void init(void);
int main(void) {
put_str("I am kernel\n");
init_all();
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while(1);
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
PANIC("init: should not be here");
}
删除(device/keyboard.c/intr_keyboard_handler)中的打印语句
put_char(cur_char); // 临时的
makefile记得新增包含静态库
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/ -I fs/ -I shell/
这里的问题:
1、main函数中有打印命令提示符的语句,而init_all
中调用thread_init
调用process_execute
创建了init进程,init运行时会fork出只调用shell的进程,这个进程会调用print_prompt
打印命令提示符,这就和main当中打印是冲突了的。要想实现书上的效果,那么fork出运行shell的进程调用print_prompt
必须在main
调用cls_screen
之前。这依赖于特定的任务执行顺序,不过一般不会出错。
小节c:
添加快捷键
readline
中新增加对于组合键的处理,ctrl + l 清除除了当前行外的其他行。ctrl + u清除本行的输入,效果类似于连续按下多个退格。我们在键盘中断处理程序中已经预先写好了按下ctrl + l 与 ctrl + u 的处理
if ((ctrl_status && cur_char == 'l') || (ctrl_status && cur_char == 'u')) {
cur_char -= 'a';
}
if (!ioq_full(&kbd_buf)) {
ioq_putchar(&kbd_buf, cur_char);
}
也就是说,我们按下ctrl + l 与 ctrl + u时,放入键盘输入缓冲区的字符是ascii 码为 ‘l’ - ‘a’ 与 ‘u’ - ‘a’,这两个ascii码都属于不可见的控制字符。所以我们只需要增加readline
读出这两种情况的处理逻辑即可
修改(shell/shell.c)
/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char *buf, int32_t count)
{
ASSERT(buf != NULL && count > 0);
char *pos = buf;
while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count)
{ // 在不出错情况下,直到找到回车符才返回
switch (*pos)
{
/* 找到回车或换行符后认为键入的命令结束,直接返回 */
case '\n':
case '\r':
*pos = 0; // 添加cmd_line的终止字符0
putchar('\n');
return;
case '\b':
if (cmd_line[0] != '\b')
{ // 阻止删除非本次输入的信息
--pos; // 退回到缓冲区cmd_line中上一个字符
putchar('\b');
}
break;
/* ctrl+l 清屏 */
case 'l' - 'a':
/* 1 先将当前的字符'l'-'a'置为0 */
*pos = 0;
/* 2 再将屏幕清空 */
clear();
/* 3 打印提示符 */
print_prompt();
/* 4 将之前键入的内容再次打印 */
printf("%s", buf);
break;
/* ctrl+u 清掉输入 */
case 'u' - 'a':
while (buf != pos)
{
putchar('\b');
*(pos--) = 0;
}
break;
/* 非控制键则输出字符 */
default:
putchar(*pos);
pos++;
}
}
printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}
小节d:
解析键入的字符
cmd_parse
分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组。这个函数就是个字符串处理函数,从诸如 ‘ls dir ’ 这样的命令中拆单词,拆成 ‘ls’ 与 ‘dir’
修改(shell/shell.c)
#define MAX_ARG_NR 16 // 加上命令名外,最多支持15个参数
/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
static int32_t cmd_parse(char *cmd_str, char **argv, char token)
{
ASSERT(cmd_str != NULL);
int32_t arg_idx = 0;
while (arg_idx < MAX_ARG_NR)
{
argv[arg_idx] = NULL;
arg_idx++;
}
char *next = cmd_str;
int32_t argc = 0;
/* 外层循环处理整个命令行 */
while (*next)
{
/* 去除命令字或参数之间的空格 */
while (*next == token)
{
next++;
}
/* 处理最后一个参数后接空格的情况,如"ls dir2 " */
if (*next == 0)
{
break;
}
argv[argc] = next;
/* 内层循环处理命令行中的每个命令字及参数 */
while (*next && *next != token)
{ // 在字符串结束前找单词分隔符
next++;
}
/* 如果未结束(是token字符),使tocken变成0 */
if (*next)
{
*next++ = 0; // 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
}
/* 避免argv数组访问越界,参数过多则返回0 */
if (argc > MAX_ARG_NR)
{
return -1;
}
argc++;
}
return argc;
}
my_shell
增加测试代码, 输出每个分离出来的单词
修改(shell/shell.c)
char *argv[MAX_ARG_NR]; // argv必须为全局变量,为了以后exec的程序可访问参数
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲
int32_t argc = -1;
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
int32_t arg_idx = 0;
while (arg_idx < argc)
{
printf("%s ", argv[arg_idx]);
arg_idx++;
}
printf("\n");
}
PANIC("my_shell: should not be here");
}
小节e:
实现输入命令,然后调用对应的函数
先实现一个ps系统调用
pad_print
用于对齐输出,也就是有一个buf区长度10字节,然后我们无论要输出什么,都向这个buf中写入,然后空余部分全部填充空格,最后将整个buf输出。比如输出“hello”,经过处理就变成了”hello “
elem2thread_info
调用pad_print
来对齐输出每个pcb的pid, ppid, status, elapsed_ticks, name
sys_ps
调用list_traversal
遍历所有任务队列,在其中回调elem2thread_info
来输出进程或线程pcb中的信息
修改(thread/thread.c)
#include "stdio.h"
#include "fs.h"
#include "file.h"
/* 以填充空格的方式输出buf */
static void pad_print(char *buf, int32_t buf_len, void *ptr, char format)
{
memset(buf, 0, buf_len);
uint8_t out_pad_0idx = 0;
switch (format)
{
case 's':
out_pad_0idx = sprintf(buf, "%s", ptr);
break;
case 'd':
out_pad_0idx = sprintf(buf, "%d", *((int16_t *)ptr));
case 'x':
out_pad_0idx = sprintf(buf, "%x", *((uint32_t *)ptr));
}
while (out_pad_0idx < buf_len)
{ // 以空格填充
buf[out_pad_0idx] = ' ';
out_pad_0idx++;
}
sys_write(stdout_no, buf, buf_len - 1);
}
/* 用于在list_traversal函数中的回调函数,用于针对线程队列的处理 */
static bool elem2thread_info(struct list_elem *pelem, int arg UNUSED)
{
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
char out_pad[16] = {0};
pad_print(out_pad, 16, &pthread->pid, 'd');
if (pthread->parent_pid == -1)
{
pad_print(out_pad, 16, "NULL", 's');
}
else
{
pad_print(out_pad, 16, &pthread->parent_pid, 'd');
}
switch (pthread->status)
{
case 0:
pad_print(out_pad, 16, "RUNNING", 's');
break;
case 1:
pad_print(out_pad, 16, "READY", 's');
break;
case 2:
pad_print(out_pad, 16, "BLOCKED", 's');
break;
case 3:
pad_print(out_pad, 16, "WAITING", 's');
break;
case 4:
pad_print(out_pad, 16, "HANGING", 's');
break;
case 5:
pad_print(out_pad, 16, "DIED", 's');
}
pad_print(out_pad, 16, &pthread->elapsed_ticks, 'x');
memset(out_pad, 0, 16);
ASSERT(strlen(pthread->name) < 17);
memcpy(out_pad, pthread->name, strlen(pthread->name));
strcat(out_pad, "\n");
sys_write(stdout_no, out_pad, strlen(out_pad));
return false; // 此处返回false是为了迎合主调函数list_traversal,只有回调函数返回false时才会继续调用此函数
}
/* 打印任务列表 */
void sys_ps(void)
{
char *ps_title = "PID PPID STAT TICKS COMMAND\n";
sys_write(stdout_no, ps_title, strlen(ps_title));
list_traversal(&thread_all_list, elem2thread_info, 0);
}
添加函数声明,修改(thread/thread.h)
void sys_ps(void);
然后将上一章和本章实现的sys开头的函数,全部封装成系统调用
首先添加系统调用号,修改(lib/user/syscall.h)
#include "fs.h"
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS
};
然后实现它们的用户态入口,修改(lib/user/syscall.c)
/* 获取当前工作目录 */
char *getcwd(char *buf, uint32_t size)
{
return (char *)_syscall2(SYS_GETCWD, buf, size);
}
/* 以flag方式打开文件pathname */
int32_t open(char *pathname, uint8_t flag)
{
return _syscall2(SYS_OPEN, pathname, flag);
}
/* 关闭文件fd */
int32_t close(int32_t fd)
{
return _syscall1(SYS_CLOSE, fd);
}
/* 设置文件偏移量 */
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence)
{
return _syscall3(SYS_LSEEK, fd, offset, whence);
}
/* 删除文件pathname */
int32_t unlink(const char *pathname)
{
return _syscall1(SYS_UNLINK, pathname);
}
/* 创建目录pathname */
int32_t mkdir(const char *pathname)
{
return _syscall1(SYS_MKDIR, pathname);
}
/* 打开目录name */
struct dir *opendir(const char *name)
{
return (struct dir *)_syscall1(SYS_OPENDIR, name);
}
/* 关闭目录dir */
int32_t closedir(struct dir *dir)
{
return _syscall1(SYS_CLOSEDIR, dir);
}
/* 删除目录pathname */
int32_t rmdir(const char *pathname)
{
return _syscall1(SYS_RMDIR, pathname);
}
/* 读取目录dir */
struct dir_entry *readdir(struct dir *dir)
{
return (struct dir_entry *)_syscall1(SYS_READDIR, dir);
}
/* 回归目录指针 */
void rewinddir(struct dir *dir)
{
_syscall1(SYS_REWINDDIR, dir);
}
/* 获取path属性到buf中 */
int32_t stat(const char *path, struct stat *buf)
{
return _syscall2(SYS_STAT, path, buf);
}
/* 改变工作目录为path */
int32_t chdir(const char *path)
{
return _syscall1(SYS_CHDIR, path);
}
/* 显示任务列表 */
void ps(void)
{
_syscall0(SYS_PS);
}
添加系统调用用户态入口函数声明,修改修改(lib/user/syscall.h)
char *getcwd(char *buf, uint32_t size);
int32_t open(char *pathname, uint8_t flag);
int32_t close(int32_t fd);
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence);
int32_t unlink(const char *pathname);
int32_t mkdir(const char *pathname);
struct dir *opendir(const char *name);
int32_t closedir(struct dir *dir);
int32_t rmdir(const char *pathname);
struct dir_entry *readdir(struct dir *dir);
void rewinddir(struct dir *dir);
int32_t stat(const char *path, struct stat *buf);
int32_t chdir(const char *path);
void ps(void);
最后在系统调用表中添加真正的系统调用执行函数,修改(userprog/syscall-init.c)
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
put_str("syscall_init done\n");
}
操作系统为了方便用户使用,一般都会提供相对路径功能。比如我们当前工作路径是/home/kanshan/Desktop,我们想要运行一个编译好的程序输入./test,实际上是被操作系统解析成了/home/kanshan/Desktop/test,也就是当前工作路径 + 相对路径 = 绝对路径。
wash_path
将路径old_abs_path(这是调用者提供的绝对路径)中的…和.转换为实际路径后存入new_abs_path。例如,给定路径/a/b/..
应被转换成/a
。给定路径/a/b/.
应被转换成/a/b
。核心原理:调用path_parse
解析路径,如果是..
,则退回上一层路径。如果是.
,则什么都不做。带入一个例子,比如/a/../home/.
就可以明白次函数如何工作
make_clear_abs_path
将路径(包含相对路径与绝对路径两种)处理成不含…和.的绝对路径,存储在final_path中。核心原理:判断输入路径是相对路径还是绝对路径,如果是相对路径,调用getcwd获得当前工作目录的绝对路径,将用户输入的路径追加到工作目录路径之后形成绝对目录路径,将其作为参数传给wash_path
进行路径转换。
#include "buildin_cmd.h"
#include "debug.h"
#include "dir.h"
#include "string.h"
#include "fs.h"
#include "syscall.h"
/* 将路径old_abs_path中的..和.转换为实际路径后存入new_abs_path */
static void wash_path(char *old_abs_path, char *new_abs_path)
{
ASSERT(old_abs_path[0] == '/');
char name[MAX_FILE_NAME_LEN] = {0};
char *sub_path = old_abs_path;
sub_path = path_parse(sub_path, name);
if (name[0] == 0)
{ // 若只键入了"/",直接将"/"存入new_abs_path后返回
new_abs_path[0] = '/';
new_abs_path[1] = 0;
return;
}
new_abs_path[0] = 0; // 避免传给new_abs_path的缓冲区不干净
strcat(new_abs_path, "/");
while (name[0])
{
/* 如果是上一级目录“..” */
if (!strcmp("..", name))
{
char *slash_ptr = strrchr(new_abs_path, '/');
/*如果未到new_abs_path中的顶层目录,就将最右边的'/'替换为0,
这样便去除了new_abs_path中最后一层路径,相当于到了上一级目录 */
if (slash_ptr != new_abs_path)
{ // 如new_abs_path为“/a/b”,".."之后则变为“/a”
*slash_ptr = 0;
}
else
{ // 如new_abs_path为"/a",".."之后则变为"/"
/* 若new_abs_path中只有1个'/',即表示已经到了顶层目录,
就将下一个字符置为结束符0. */
*(slash_ptr + 1) = 0;
}
}
else if (strcmp(".", name))
{ // 如果路径不是‘.’,就将name拼接到new_abs_path
if (strcmp(new_abs_path, "/"))
{ // 如果new_abs_path不是"/",就拼接一个"/",此处的判断是为了避免路径开头变成这样"//"
strcat(new_abs_path, "/");
}
strcat(new_abs_path, name);
} // 若name为当前目录".",无须处理new_abs_path
/* 继续遍历下一层路径 */
memset(name, 0, MAX_FILE_NAME_LEN);
if (sub_path)
{
sub_path = path_parse(sub_path, name);
}
}
}
/* 将path处理成不含..和.的绝对路径,存储在final_path */
void make_clear_abs_path(char *path, char *final_path)
{
char abs_path[MAX_PATH_LEN] = {0};
/* 先判断是否输入的是绝对路径 */
if (path[0] != '/')
{ // 若输入的不是绝对路径,就拼接成绝对路径
memset(abs_path, 0, MAX_PATH_LEN);
if (getcwd(abs_path, MAX_PATH_LEN) != NULL)
{
if (!((abs_path[0] == '/') && (abs_path[1] == 0)))
{ // 若abs_path表示的当前目录不是根目录/
strcat(abs_path, "/");
}
}
}
strcat(abs_path, path);
wash_path(abs_path, final_path);
}
问题1:代码中:new_abs_path[0] = 0;意义何在?
确保了后续在new_abs_path
上的任何字符串连接(例如通过strcat
函数)都会从头开始。
函数声明,(shell/buildin_cmd.h)
#ifndef __SHELL_BUILDIN_CMD_H
#define __SHELL_BUILDIN_CMD_H
void make_clear_abs_path(char *path, char *final_path);
#endif
支持代码,修改(fs/fs.c)
static char *path_parse(char *pathname, char *name_store)
为
char *path_parse(char *pathname, char *name_store)
my_shell
增加测试代码,修改(shell/shell.c)
#include "buildin_cmd.h"
void my_shell(void)
{
cwd_cache[0] = '/';
cwd_cache[1] = 0;
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
char buf[MAX_PATH_LEN] = {0};
int32_t arg_idx = 0;
while (arg_idx < argc)
{
make_clear_abs_path(argv[arg_idx], buf);
printf("%s -> %s\n", argv[arg_idx], buf);
arg_idx++;
}
}
PANIC("my_shell: should not be here");
}
小节f:
实现一系列内建命令
shell命令分为外部命令与内部命令。执行外部命令,实际上就是执行了一个进程。而内部命令,就是执行操作系统自带的函数。我们现在来实现一系列内部命令所需要的内建函数。
每个内建函数都会传入两个参数:
uint32_t argc
: 这个参数表示传入到该函数的参数个数。在命令ls -l
中,ls
是命令,而-l
是ls
的参数。在这个例子中,argc
就是2,因为有两个参数:ls
和-l
。char** argv
: 这是一个指向字符串数组的指针,代表传入的参数值。argv
的每一个元素都是一个字符串,表示命令行上的一个参数。
buildin_pwd
就是调用了`getcwd
修改(shell/buildin_cmd.c)
#include "shell.h"
#include "stdio.h"
/* pwd命令的内建函数 */
void buildin_pwd(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("pwd: no argument support!\n");
return;
}
else
{
if (NULL != getcwd(final_path, MAX_PATH_LEN))
{
printf("%s\n", final_path);
}
else
{
printf("pwd: get current work directory failed.\n");
}
}
}
支持代码,修改(shell/shell.h)
#include "fs.h"
extern char final_path[MAX_PATH_LEN];
buildin_cd
就是调用了make_clear_abs_path
解析argv[1]
成绝对路径,然后调用chdir
来切换目录
修改(shell/buildin_cmd.c)
/* cd命令的内建函数 */
char *buildin_cd(uint32_t argc, char **argv)
{
if (argc > 2)
{
printf("cd: only support 1 argument!\n");
return NULL;
}
/* 若是只键入cd而无参数,直接返回到根目录. */
if (argc == 1)
{
final_path[0] = '/';
final_path[1] = 0;
}
else
{
make_clear_abs_path(argv[1], final_path);
}
if (chdir(final_path) == -1)
{
printf("cd: no such directory %s\n", final_path);
return NULL;
}
return final_path;
}
buildin_ls
:用于列出文件或目录
函数核心原理:
命令行参数解析:使用while循环遍历所有的命令行参数argv,并进行以下处理:
- 如果参数以
-
开头,那么它被视为一个选项。目前支持两个选项:-l
和-h
。其中-l
选项使信息以长格式输出,而-h
选项则打印帮助信息 - 如果参数不是一个选项,则被视为一个路径参数。函数只支持一个路径参数。
- 如果参数以
设置默认路径:
如果用户未提供路径参数,函数将使用当前工作目录作为默认路径。获取文件或目录状态:
使用stat
函数检查指定路径文件或目录的状态。如果路径不存在,函数将打印错误信息并返回。目录处理
如果指定的路径是一个目录:
- 打开这个目录。
- 如果使用了
-l
选项,则以长格式输出目录中的每个目录项。这包括文件类型(目录或普通文件)、i节点号、文件大小和文件名。 - 如果没有使用
-l
选项,则只输出文件名。 - 最后,关闭目录。
文件处理
如果指定的路径是一个文件:
- 如果使用了
-l
选项,则以长格式输出文件的信息。 - 如果没有使用
-l
选项,则只输出文件名。
- 如果使用了
修改(shell/buildin_cmd.c)
/* ls命令的内建函数 */
void buildin_ls(uint32_t argc, char **argv)
{
char *pathname = NULL;
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
bool long_info = false;
uint32_t arg_path_nr = 0;
uint32_t arg_idx = 1; // 跨过argv[0],argv[0]是字符串“ls”
while (arg_idx < argc)
{
if (argv[arg_idx][0] == '-')
{ // 如果是选项,单词的首字符是-
if (!strcmp("-l", argv[arg_idx]))
{ // 如果是参数-l
long_info = true;
}
else if (!strcmp("-h", argv[arg_idx]))
{ // 参数-h
printf("usage: -l list all infomation about the file.\n-h for help\nlist all files in the current dirctory if no option\n");
return;
}
else
{ // 只支持-h -l两个选项
printf("ls: invalid option %s\nTry `ls -h' for more information.\n", argv[arg_idx]);
return;
}
}
else
{ // ls的路径参数
if (arg_path_nr == 0)
{
pathname = argv[arg_idx];
arg_path_nr = 1;
}
else
{
printf("ls: only support one path\n");
return;
}
}
arg_idx++;
}
if (pathname == NULL)
{ // 若只输入了ls 或 ls -l,没有输入操作路径,默认以当前路径的绝对路径为参数.
if (NULL != getcwd(final_path, MAX_PATH_LEN))
{
pathname = final_path;
}
else
{
printf("ls: getcwd for default path failed\n");
return;
}
}
else
{
make_clear_abs_path(pathname, final_path);
pathname = final_path;
}
if (stat(pathname, &file_stat) == -1)
{
printf("ls: cannot access %s: No such file or directory\n", pathname);
return;
}
if (file_stat.st_filetype == FT_DIRECTORY)
{
struct dir *dir = opendir(pathname);
struct dir_entry *dir_e = NULL;
char sub_pathname[MAX_PATH_LEN] = {0};
uint32_t pathname_len = strlen(pathname);
uint32_t last_char_idx = pathname_len - 1;
memcpy(sub_pathname, pathname, pathname_len);
if (sub_pathname[last_char_idx] != '/')
{
sub_pathname[pathname_len] = '/';
pathname_len++;
}
rewinddir(dir);
if (long_info)
{
char ftype;
printf("total: %d\n", file_stat.st_size);
while ((dir_e = readdir(dir)))
{
ftype = 'd';
if (dir_e->f_type == FT_REGULAR)
{
ftype = '-';
}
sub_pathname[pathname_len] = 0;
strcat(sub_pathname, dir_e->filename);
memset(&file_stat, 0, sizeof(struct stat));
if (stat(sub_pathname, &file_stat) == -1)
{
printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);
return;
}
printf("%c %d %d %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);
}
}
else
{
while ((dir_e = readdir(dir)))
{
printf("%s ", dir_e->filename);
}
printf("\n");
}
closedir(dir);
}
else
{
if (long_info)
{
printf("- %d %d %s\n", file_stat.st_ino, file_stat.st_size, pathname);
}
else
{
printf("%s\n", pathname);
}
}
}
修改(shell/buildin_cmd.c)
/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("ps: no argument support!\n");
return;
}
ps();
}
buildin_clear
就是调用`clear
修改(shell/buildin_cmd.c)
/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("clear: no argument support!\n");
return;
}
clear();
}
buildin_mkdir
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用`mkdir
修改(shell/buildin_cmd.c)
/* mkdir命令内建函数 */
int32_t buildin_mkdir(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("mkdir: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若创建的不是根目录 */
if (strcmp("/", final_path))
{
if (mkdir(final_path) == 0)
{
ret = 0;
}
else
{
printf("mkdir: create directory %s failed.\n", argv[1]);
}
}
}
return ret;
}
buildin_rmdir
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用`rmdir
修改(shell/buildin_cmd.c)
/* rmdir命令内建函数 */
int32_t buildin_rmdir(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("rmdir: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path))
{
if (rmdir(final_path) == 0)
{
ret = 0;
}
else
{
printf("rmdir: remove %s failed.\n", argv[1]);
}
}
}
return ret;
}
buildin_rm
就是调用make_clear_abs_path
解析argv[1]
成绝对路径,然后调用`unlink
修改(shell/buildin_cmd.c)
/* rm命令内建函数 */
int32_t buildin_rm(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("rm: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path))
{
if (unlink(final_path) == 0)
{
ret = 0;
}
else
{
printf("rm: delete %s failed.\n", argv[1]);
}
}
}
return ret;
}
函数声明,修改(shell/buildin_cmd.h)
#include "global.h"
void buildin_pwd(uint32_t argc, char **argv UNUSED);
char *buildin_cd(uint32_t argc, char **argv);
void buildin_ls(uint32_t argc, char **argv);
void buildin_ps(uint32_t argc, char **argv UNUSED);
void buildin_clear(uint32_t argc, char **argv UNUSED);
int32_t buildin_mkdir(uint32_t argc, char **argv);
int32_t buildin_rmdir(uint32_t argc, char **argv);
int32_t buildin_rm(uint32_t argc, char **argv);
my_shell
就是增加了通过判断arg[0](这个是要调用的命令名)是什么,然后对应调用内建函数
修改(shell/shell.c)
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if (!strcmp("ls", argv[0]))
{
buildin_ls(argc, argv);
}
else if (!strcmp("cd", argv[0]))
{
if (buildin_cd(argc, argv) != NULL)
{
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
}
else if (!strcmp("pwd", argv[0]))
{
buildin_pwd(argc, argv);
}
else if (!strcmp("ps", argv[0]))
{
buildin_ps(argc, argv);
}
else if (!strcmp("clear", argv[0]))
{
buildin_clear(argc, argv);
}
else if (!strcmp("mkdir", argv[0]))
{
buildin_mkdir(argc, argv);
}
else if (!strcmp("rmdir", argv[0]))
{
buildin_rmdir(argc, argv);
}
else if (!strcmp("rm", argv[0]))
{
buildin_rm(argc, argv);
}
else
{
printf("external command\n");
}
}
PANIC("my_shell: should not be here");
}
小节g:
加载用户进程
segment_load
将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存。核心原理:我们编译程序后,编译器已经指定好了可加载段的虚拟地址,我们直接按照这个虚拟地址,把段加载到内存中对应的虚拟地址就可以了。由于这个函数是fork
之后从磁盘编译好的程序加载可加载段时使用,所以我们使用的是调用fork的进程的页表,所以我们要判断目的内存虚拟地址是否在页表中有效,如果无效,则为指定虚拟地址申请物理内存。申请内存完毕,我们调用sys_read
从磁盘中加载可加载段到指定内存虚拟地址中即可。
(userprog/exec.c)
#include "exec.h"
#include "stdint.h"
#include "global.h"
#include "memory.h"
#include "fs.h"
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;
/* 32位elf头 */
struct Elf32_Ehdr
{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
};
/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr
{
Elf32_Word p_type; // 见下面的enum segment_type
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
};
/* 段类型 */
enum segment_type
{
PT_NULL, // 忽略
PT_LOAD, // 可加载程序段
PT_DYNAMIC, // 动态加载信息
PT_INTERP, // 动态加载器名称
PT_NOTE, // 一些辅助信息
PT_SHLIB, // 保留
PT_PHDR // 程序头表
};
/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr)
{
uint32_t vaddr_first_page = vaddr & 0xfffff000; // vaddr地址所在的页框
uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
uint32_t occupy_pages = 0;
/* 若一个页框容不下该段 */
if (filesz > size_in_first_page)
{
uint32_t left_size = filesz - size_in_first_page;
occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
}
else
{
occupy_pages = 1;
}
/* 为进程分配内存 */
uint32_t page_idx = 0;
uint32_t vaddr_page = vaddr_first_page;
while (page_idx < occupy_pages)
{
uint32_t *pde = pde_ptr(vaddr_page);
uint32_t *pte = pte_ptr(vaddr_page);
/* 如果pde不存在,或者pte不存在就分配内存.
* pde的判断要在pte之前,否则pde若不存在会导致
* 判断pte时缺页异常 */
if (!(*pde & 0x00000001) || !(*pte & 0x00000001))
{
if (get_a_page(PF_USER, vaddr_page) == NULL)
{
return false;
}
} // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
vaddr_page += PG_SIZE;
page_idx++;
}
sys_lseek(fd, offset, SEEK_SET);
sys_read(fd, (void *)vaddr, filesz);
return true;
}
load
根据传入的路径,加载磁盘中的程序的可加载段,最后返回程序入口地址。原理:编译好的程序在磁盘中,起始就是ELF header,我们去把这个读出来,从中得到program header的偏移、数量、每个大小。然后我们根据这些信息去循环读出program header,根据每个program header信息去调用segment_load
将可加载段加载到内存中。
修改(userprog/exec.c)
#include "string.h"
/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char *pathname)
{
int32_t ret = -1;
struct Elf32_Ehdr elf_header;
struct Elf32_Phdr prog_header;
memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));
int32_t fd = sys_open(pathname, O_RDONLY);
if (fd == -1)
{
return -1;
}
if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr))
{
ret = -1;
goto done;
}
/* 校验elf头 */
if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) || elf_header.e_type != 2 || elf_header.e_machine != 3 || elf_header.e_version != 1 || elf_header.e_phnum > 1024 || elf_header.e_phentsize != sizeof(struct Elf32_Phdr))
{
ret = -1;
goto done;
}
Elf32_Off prog_header_offset = elf_header.e_phoff;
Elf32_Half prog_header_size = elf_header.e_phentsize;
/* 遍历所有程序头 */
uint32_t prog_idx = 0;
while (prog_idx < elf_header.e_phnum)
{
memset(&prog_header, 0, prog_header_size);
/* 将文件的指针定位到程序头 */
sys_lseek(fd, prog_header_offset, SEEK_SET);
/* 只获取程序头 */
if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size)
{
ret = -1;
goto done;
}
/* 如果是可加载段就调用segment_load加载到内存 */
if (PT_LOAD == prog_header.p_type)
{
if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr))
{
ret = -1;
goto done;
}
}
/* 更新下一个程序头的偏移 */
prog_header_offset += elf_header.e_phentsize;
prog_idx++;
}
ret = elf_header.e_entry;
done:
sys_close(fd);
return ret;
}
在C和C++中,使用\xHH
格式的十六进制转义序列时,需要特别小心,因为这个序列会继续解析所有有效的十六进制数字,直到遇到一个非十六进制数字或序列的长度达到其最大值。字符串 "\x7fELF"
会被解析为一个字符 \x7fE
,然后是 LF
,而不是我们预期的 \x7f
和 ELF
。
sys_execv
用path指向的程序替换当前进程,注意,这个函数是fork
之后调用的。原理:先调用load
加载程序可执行段到内存中,并得到了程序入口地址。然后修改pcb中的数据即可,包括:程序名字、内核栈中中断栈中用于传参的寄存器(该函数运行在内核态下,通过intr_exit
返回到用户态执行新的进程,所以中断栈中的数据会被intr_exit
的push操作送入寄存器,以此达到传参目的)、中断栈中eip用于跳转程序入口、中断栈esp用于设定新进程的栈顶位置(fork
中拷贝了父进程页表、重新申请了物理地址空间用于拷贝用户栈数据,所以并不用担心新进程用户栈用到的虚拟地址没有映射物理地址)。最后通过内联汇编设定esp为中断栈的位置,然后跳转执行intr_exit
,就可以执行新的进程了。
修改(userprog/exec.c)
#include "thread.h"
/* 用path指向的程序替换当前进程 */
int32_t sys_execv(const char *path, const char *argv[])
{
uint32_t argc = 0;
while (argv[argc])
{
argc++;
}
int32_t entry_point = load(path);
if (entry_point == -1)
{ // 若加载失败则返回-1
return -1;
}
struct task_struct *cur = running_thread();
/* 修改进程名 */
memcpy(cur->name, path, TASK_NAME_LEN);
cur->name[TASK_NAME_LEN - 1] = 0;
struct intr_stack *intr_0_stack = (struct intr_stack *)((uint32_t)cur + PG_SIZE - sizeof(struct intr_stack));
/* 参数传递给用户进程 */
intr_0_stack->ebx = (int32_t)argv;
intr_0_stack->ecx = argc;
intr_0_stack->eip = (void *)entry_point;
/* 使新用户进程的栈地址为最高用户空间地址 */
intr_0_stack->esp = (void *)0xc0000000;
/* exec不同于fork,为使新进程更快被执行,直接从中断返回 */
asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(intr_0_stack) : "memory");
return 0;
}
支持代码,修改(thread/thread.h)
#define TASK_NAME_LEN 16
将sys_execv
做成系统调用
添加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS,
SYS_EXECV
};
用户态系统调用入口,修改(lib/user/syscall.c)
int execv(const char *pathname, char **argv)
{
return _syscall2(SYS_EXECV, pathname, argv);
}
声明用户态系统调用入口,修改(lib/user/syscall.h)
int execv(const char* pathname, char** argv);
系统调用表修改,修改(userprog/syscall-init.c)
#include "exec.h"
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
syscall_table[SYS_EXECV] = sys_execv;
put_str("syscall_init done\n");
}
修改my_shell
增加对于外部命令去磁盘加载编译好的二进制进程序并执行的代码,核心就是先fork
创建子进程,然后子进程调用make_clear_abs_path
解析传入的路径,然后调用execv
去执行
修改(shell/shell.c/my_shell)
#include "syscall.h"
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if (!strcmp("ls", argv[0]))
{
buildin_ls(argc, argv);
}
else if (!strcmp("cd", argv[0]))
{
if (buildin_cd(argc, argv) != NULL)
{
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
}
else if (!strcmp("pwd", argv[0]))
{
buildin_pwd(argc, argv);
}
else if (!strcmp("ps", argv[0]))
{
buildin_ps(argc, argv);
}
else if (!strcmp("clear", argv[0]))
{
buildin_clear(argc, argv);
}
else if (!strcmp("mkdir", argv[0]))
{
buildin_mkdir(argc, argv);
}
else if (!strcmp("rmdir", argv[0]))
{
buildin_rmdir(argc, argv);
}
else if (!strcmp("rm", argv[0]))
{
buildin_rm(argc, argv);
}
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
/* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
while (1)
;
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
while (1)
;
}
}
int32_t arg_idx = 0;
while (arg_idx < MAX_ARG_NR)
{
argv[arg_idx] = NULL;
arg_idx++;
}
}
PANIC("my_shell: should not be here");
}
编译好一个用户程序prog_no_arg
,我们需要自行写入hd60M.img,然后用操作系统从hd60M.img中取到编译好的用户程序到内存,再写入有文件系统的hd80M.img,最后再执行prog_no_arg
(command/prog_no_arg.c)
#include "stdio.h"
int main(void)
{
printf("prog_no_arg from disk\n");
while (1)
;
return 0;
}
由于这个程序复用了printf,而printf调用了vsprintf,而vsprintf调用了strcpy,而strcpy调用了ASSERT宏,而ASSERT使用了PANIC,而PANIC使用了panic_spin,而panic_spin使用了,intr_disable()。也就是说,我们调用用户态printf的程序,如果中间strcpy的ASSERT出错,将会直接在用户态调用intr_disable,这是绝对不能允许的,运行会报特权级保护错误!正确的做法是,先通过系统调用切换至内核态,然后再调用intr_disable。
所以,我们先实现用户态使用的assert
(lib/user/assert.c)
#include "assert.h"
#include "stdio.h"
void user_spin(char *filename, int line, const char *func, const char *condition)
{
printf("\n\n\n\nfilename %s\nline %d\nfunction %s\ncondition %s\n", filename, line, func, condition);
while (1)
;
}
(lib/user/assert.h)
#ifndef __LIB_USER_ASSERT_H
#define __LIB_USER_ASSERT_H
#include "global.h"
void user_spin(char *filename, int line, const char *func, const char *condition);
#define panic(...) user_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
#ifdef NDEBUG
#define assert(CONDITION) ((void)0)
#else
#define assert(CONDITION) \
if (!(CONDITION)) \
{ \
panic(#CONDITION); \
}
#endif /*NDEBUG*/
#endif /*__LIB_USER_ASSERT_H*/
如此一来,我们的assert
判断出错,将会通过printf
内的write
系统调用正常进入内核态
我们去把内核中用户态程序用到的ASSERT与PANIC都改掉
修改(lib/string.c)中所有的ASSERT
为assert
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(shell/buildin_cmd.c)中所有的ASSERT
为assert
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(shell/shell.c)中所有的ASSERT
为assert
,修改所有的PANIC
为panic
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
修改(kernel/main.c)中所有的PANIC
为panic
,然后将头文件#include "debug.h"
修改为#incldue "assert.h"
给出操作prog_no_arg.c的脚本,该脚本主要功能:编译prog_no_arg.c,然后将其与使用到的.o文件进行链接(这里我们复用了给操作系统用的.o文件,按道理来说,我们需要单独实现用户程序的.o文件,但是我们偷个懒吧),最后写入磁盘hd60M.img偏移300扇区的位置
注意:相较于作者脚本,已经修改好了用到的编译器为gcc-4.4,CFLAGS,ld后面的参数,一定要自行修改DD_OUT
为自己环境中的hd60M.img路径!!!运行脚本需要在操作系统make all之后(这样用到的.o文件才会出现在build目录下),运行脚本需要在command目录下,脚本运行前需要添加可执行权限,命令:chmod +x compile.sh
,执行脚本命令:./compile.sh
(command/compile.sh)
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_no_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIB="../lib/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
$CC $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
ld -e main $BIN".o" $OBJS -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
########## 以上核心就是下面这三条命令 ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
# bs=512 count=10 seek=300 conv=notrunc
测试代码,(kernel/main.c),注意:file_size
这个变量请自行修改成自己的prog_no_arg大小,在command目录下ls -l即可查看prog_no_arg大小。我们前后会make all两次,第一次是为了让prog_no_arg有.o文件可以用,第二次是修改main.c以从hd60M.img中加载prog_no_arg到hd80M.img中
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 20684; //这个变量请自行修改成自己的prog_no_arg大小
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_no_arg", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while (1)
;
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
运行出错,经过排查,修改(fs/fs.c/sys_getcwd)为
if (child_inode_nr == 0)
{
buf[0] = '/';
buf[1] = 0;
return buf;
}
为
if (child_inode_nr == 0)
{
buf[0] = '/';
buf[1] = 0;
sys_free(io_buf);
return buf;
}
修改(kernel/memory.c/get_a_page)
...
if (cur->pgdir != NULL && pf == PF_USER)
{
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
...
if (page_phyaddr == NULL)
return NULL;
...
为
...
if (cur->pgdir != NULL && pf == PF_USER)
{
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx >= 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
...
if (page_phyaddr == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
...
小节h:
使用户进程支持传参
_start
这个函数将会被execv
执行,由它去代为执行真正的用户程序,因为这个函数会在链接用户程序前,成为真正的程序入口,这样可以做到给用户程序传参的目的。因为我们在sys_exec
中有设定要开启的程序内核栈中的中断栈ebx,ecx的代码,intr_exit
会将ebx中放入参数字符串指针数组的地址,ecx中放入参数个数,然后_start
又将这两个寄存器入栈,而标准main
函数的声明都是int mian(int argc, char** argv)
,所以main
就可以自然在栈中找到自己要的参数。而且由于_start
入栈传参符合main
的函数声明,也就是从左到右依次入栈,所以这个main
的运行无需而外设定,就可以正常运行,就像任何普通有参数的c语言函数一样。
(command/start.S)
[bits 32]
extern main
section .text
global _start
_start:
;下面这两个要和execv中load之后指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main
prog_arg.c
中的main
开启了个子进程,去执行argv[1]
中指明的程序。
(commadn/prog_arg.c)
#include "stdio.h"
#include "syscall.h"
#include "string.h"
int main(int argc, char **argv)
{
int arg_idx = 0;
while (arg_idx < argc)
{
printf("argv[%d] is %s\n", arg_idx, argv[arg_idx]);
arg_idx++;
}
int pid = fork();
if (pid)
{
int delay = 900000;
while (delay--)
;
printf("\n I`m father prog, my pid:%d, I will show process list\n", getpid());
ps();
}
else
{
char abs_path[512] = {0};
printf("\n I`m child prog, my pid:%d, I will exec %s right now\n", getpid(), argv[1]);
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
execv(abs_path, argv);
}
else
{
execv(argv[1], argv);
}
}
while (1)
;
return 0;
}
处理prog_arg.c脚本(command/compile.sh)
相比于上一小节脚本,修改了要编译的程序BIN,包含头文件LIB,链接.o文件OBJS,编译start.S的命令,创建静态库命令,链接命令。
ar rcs simple_crt.a $OBJS start.o
的解释:
ar
: 这是一个用来创建、修改和提取静态库的程序。静态库通常用于将多个目标文件(object files)打包成一个文件,这样在链接时就可以一次性链接多个目标文件。rcs
:r
: 替换或添加指定的目标文件到库中。如果库中已经有了同名的目标文件,那么这个文件会被新文件替换。c
: 如果库文件不存在,那么创建一个新的库文件。s
: 创建目标文件的索引。这可以加速链接时的速度。
simple_crt.a
: 这是你想要创建或修改的静态库的名字。$OBJS start.o
: 这是一个目标文件列表,将被添加或替换到静态库中。
所以,整个命令的意思是:将 $OBJS
和 start.o
中列出的所有目标文件添加或替换到 simple_crt.a
静态库中,并为这些目标文件创建一个索引。如果 simple_crt.a
还不存在,那么会创建一个新的静态库文件。
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib -I ../lib/user -I ../fs -I ../thread -I ../lib/kernel -I ../kernel"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
########## 以上核心就是下面这三条命令 ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
# -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
# ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
# bs=512 count=10 seek=300 conv=notrunc
测试代码(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 20840;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_arg", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
while (1)
;
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
prog_arg.c
会与start.S
中的代码编译后共同链接成一个新的程序,然后如同上一个小节一样,先写入裸盘hd60M.img,然后main.c
中从裸盘加载新的应用程序prog_arg
进入内存,然后写入有文件系统的hd80M.img。当我们启动操作系统,在shell中输入./prog_arg /prog_no_arg
后,argv[0] = prog_arg, argv[1] = prog_no_arg
。首先exec
启动prog_arg
(因为argv[0]
才是要运行的程序,而后续的argv[1+n]
是我们传递给这个程序的参数)。我们在sys_exec(exec的真正实现)
中已经将程序要用的到参数字符串指针数组地址传递给了ebx
,且_start
才是prog_arg
的真正入口,而_start
中有一句push_ebx
的代码,也就是说参数字符串指针数组地址已经传递给了prog_arg
程序,自然prog_arg
程序能够通过argv[1]
去启动`prog_no_arg
小节i:
进程终止与资源回收
首先介绍几个重要的概念:
exit
系统调用:此调用用于终止进程。当一个进程调用 exit
时,它会释放除进程控制块(pcb)以外的所有资源。pcb需要被特别处理,因为它包含了进程的重要信息,如退出状态。特别注意:exit
系统调用属于程序运行库内容,无论进程是否主动调用,都会执行。就像我们那个_start
函数一样。
wait
系统调用:这是一个与进程同步和资源回收相关的调用。具体来说,它有以下功能:
- 阻塞父进程,直到一个子进程退出,并接收子进程的返回值。
- 回收子进程使用过的pcb资源,从而确保没有资源浪费。
当一个父进程创建一个子进程来执行某项任务时,父进程可能需要知道子进程的退出状态。子进程完成其任务后,会将其退出状态保存在pcb中并调用exit
退出。此时,子进程的pcb不会被立即回收,因为它包含了子进程的退出状态。只有当父进程通过wait
系统调用来查询子进程的状态时,子进程的pcb才会被回收。
孤儿进程:如果一个父进程在其子进程结束之前退出,那么这些子进程将被称为孤儿进程,也就是说没有父进程来回收他们的pcb资源。为了防止资源浪费,这些孤儿进程会被init
进程“领养”,即成为init
进程的子进程,由init
来回收他们的pcb。
僵尸进程:当一个子进程终止,但其父进程没有调用wait
来回收其资源时,此时这个子进程也无法过继给init,于是这个子进程就变成了僵尸进程。它们仍然占用pcb,但不执行任何操作。僵尸进程的存在可能会导致资源浪费。
pcb增加表示退出状态的成员,比如正常退出还是其他啥的。修改(thread/thread.h)
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{
uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
pid_t pid;
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; // 用于存储自己的线程的名字
uint8_t ticks; // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t *pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打开文件数组
uint32_t cwd_inode_nr; // 进程所在的工作目录的inode编号
int16_t parent_pid; // 父进程pid
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
int8_t exit_status; // 进程结束时自己调用exit传入的参数
uint32_t stack_magic; // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
free_a_phy_page
用于回收物理地址,实质就是回收了物理地址池位图对应的位。如此,这个物理地址下次就会被再次分配。
修改(kernel/memory.c)
/* 根据物理页框地址pg_phy_addr在相应的内存池的位图清0,不改动页表*/
void free_a_phy_page(uint32_t pg_phy_addr)
{
struct pool *mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start)
{
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
}
else
{
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}
添加函数声明,修改(kernel/memory.h)
void free_a_phy_page(uint32_t pg_phy_addr);
由于我们的进程在退出后要释放自己的pid,然而原有的pid管理只有分配,而无回收。所以我们要实现用pid位图来管理pid的分配与回收,修改(thread/thread.c)(以下新allocate_pid
函数需要替代原有的allocate_pid
函数)
allocate_pid
用于根据pid位图中空余位的偏移 + 起始pid来分配pid
pid_pool_init
用于初始化pid位图,并在thread_init
内调用
release_pid
来释放pid,实质就是将释放pid对应的pid位图中的位置0
thread_init
增加pid池初始化代码
/* pid的位图,最大支持1024个pid */
uint8_t pid_bitmap_bits[128] = {0};
/* pid池 */
struct pid_pool
{
struct bitmap pid_bitmap; // pid位图
uint32_t pid_start; // 起始pid
struct lock pid_lock; // 分配pid锁
} pid_pool;
/* 分配pid */
static pid_t allocate_pid(void)
{
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = bitmap_scan(&pid_pool.pid_bitmap, 1);
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 1);
lock_release(&pid_pool.pid_lock);
return (bit_idx + pid_pool.pid_start);
}
/* 初始化pid池 */
static void pid_pool_init(void)
{
pid_pool.pid_start = 1;
pid_pool.pid_bitmap.bits = pid_bitmap_bits;
pid_pool.pid_bitmap.btmp_bytes_len = 128;
bitmap_init(&pid_pool.pid_bitmap);
lock_init(&pid_pool.pid_lock);
}
/* 释放pid */
void release_pid(pid_t pid)
{
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = pid - pid_pool.pid_start;
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 0);
lock_release(&pid_pool.pid_lock);
}
void thread_init(void)
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
pid_pool_init();
/* 先创建第一个用户进程:init */
process_execute(init, "init"); // 放在第一个初始化,这是第一个进程,init进程的pid为1
/* 将当前main函数创建为线程 */
make_main_thread();
/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread_init done\n");
}
thread_exit
用于回收指定任务的pcb和页表,并将其从就绪队列中删除
pid_check
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的pid是不是要找的传入pid
pid2thread
根据传入pid找pcb,原理是使用list_traversal
调用pid_check
,当pid_check
找到了会返回true,于是list_traversal
会返回pcb指针
修改(thread/thread.c)
/* 回收thread_over的pcb和页表,并将其从调度队列中去除 */
void thread_exit(struct task_struct *thread_over, bool need_schedule)
{
/* 要保证schedule在关中断情况下调用 */
intr_disable();
thread_over->status = TASK_DIED;
/* 如果thread_over不是当前线程,就有可能还在就绪队列中,将其从中删除 */
if (elem_find(&thread_ready_list, &thread_over->general_tag))
{
list_remove(&thread_over->general_tag);
}
if (thread_over->pgdir)
{ // 如是进程,回收进程的页表
mfree_page(PF_KERNEL, thread_over->pgdir, 1);
}
/* 从all_thread_list中去掉此任务 */
list_remove(&thread_over->all_list_tag);
/* 回收pcb所在的页,主线程的pcb不在堆中,跨过 */
if (thread_over != main_thread)
{
mfree_page(PF_KERNEL, thread_over, 1);
}
/* 归还pid */
release_pid(thread_over->pid);
/* 如果需要下一轮调度则主动调用schedule */
if (need_schedule)
{
schedule();
PANIC("thread_exit: should not be here\n");
}
}
/* 比对任务的pid */
static bool pid_check(struct list_elem *pelem, int32_t pid)
{
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->pid == pid)
{
return true;
}
return false;
}
/* 根据pid找pcb,若找到则返回该pcb,否则返回NULL */
struct task_struct *pid2thread(int32_t pid)
{
struct list_elem *pelem = list_traversal(&thread_all_list, pid_check, pid);
if (pelem == NULL)
{
return NULL;
}
struct task_struct *thread = elem2entry(struct task_struct, all_list_tag, pelem);
return thread;
}
函数声明,修改(thread/thread.h)
void thread_exit(struct task_struct* thread_over, bool need_schedule);
struct task_struct* pid2thread(int32_t pid);
void release_pid(pid_t pid);
release_prog_resource
用于根据传入的pcb指针,释放任务的资源,包括1、页表中对应的物理页面(这里用的方法是遍历页表);2、虚拟内存池占用的物理页框;3、关闭打开的文件
(userprog/wait_exit.c)
#include "wait_exit.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "fs.h"
/* 释放用户进程资源:
* 1 页表中对应的物理页
* 2 虚拟内存池占物理页框
* 3 关闭打开的文件 */
static void release_prog_resource(struct task_struct *release_thread)
{
uint32_t *pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t *v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分
uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t *v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分
uint32_t *first_pte_vaddr_in_pde = NULL; // 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;
/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr)
{
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001)
{ // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr)
{
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001)
{
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}
/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t *user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);
/* 关闭进程打开的文件 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC)
{
if (release_thread->fd_table[fd_idx] != -1)
{
sys_close(fd_idx);
}
fd_idx++;
}
}
fild_child
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的parient_id是不是要找的传入ppid
修改(userprog/wait_exit.c)
/* list_traversal的回调函数,
* 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem *pelem, int32_t ppid)
{
/* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid)
{ // 若该任务的parent_pid为ppid,返回
return true; // list_traversal只有在回调函数返回true时才会停止继续遍历,所以在此返回true
}
return false; // 让list_traversal继续传递下一个元素
}
find_hanging_child
会被list_traversal
调用,用于对比传入的all_list_tag
指针对应任务的ppid是不是传入的ppid,且状态要是不是TASK_HANGING(进程没有完全退出就是这个状态)。此函数用于父进程来找到自己退出的子进程以回收它的剩余资源
修改(userprog/wait_exit.c)
/* list_traversal的回调函数,
* 查找状态为TASK_HANGING的任务 */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
return true;
}
return false;
}
init_adopt_a_child
将传入的all_list_tag
指针对应任务parent_pid
改为1,也就是将一个子进程过继给init
修改(userprog/wait_exit.c)
/* list_traversal的回调函数,
* 将一个子进程过继给init */
static bool init_adopt_a_child(struct list_elem *pelem, int32_t pid)
{
struct task_struct *pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == pid)
{ // 若该进程的parent_pid为pid,返回
pthread->parent_pid = 1;
}
return false; // 让list_traversal继续传递下一个元素
}
sys_wait
等待子进程调用exit,将子进程的退出状态保存到status指向的变量,并回收子进程的pcb与页表,最后返回子进程pid。如果子进程都在运行,那么就阻塞自己。这个函数有两种用法,一种是init
while(1)不断调用,来不断回收子进程的资源;一种是父进程fork之后调用,然后等待子进程退出后继续运行,然后回收子进程剩余资源。
修改(userprog/wait_exit.c)
/* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量.
* 成功则返回子进程的pid,失败则返回-1 */
pid_t sys_wait(int32_t *status)
{
struct task_struct *parent_thread = running_thread();
while (1)
{
/* 优先处理已经是挂起状态的任务 */
struct list_elem *child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
/* 若有挂起的子进程 */
if (child_elem != NULL)
{
struct task_struct *child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
*status = child_thread->exit_status;
/* thread_exit之后,pcb会被回收,因此提前获取pid */
uint16_t child_pid = child_thread->pid;
/* 2 从就绪队列和全部队列中删除进程表项*/
thread_exit(child_thread, false); // 传入false,使thread_exit调用后回到此处
/* 进程表项是进程或线程的最后保留的资源, 至此该进程彻底消失了 */
return child_pid;
}
/* 判断是否有子进程 */
child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
if (child_elem == NULL)
{ // 若没有子进程则出错返回
return -1;
}
else
{
/* 若子进程还未运行完,即还未调用exit,则将自己挂起,直到子进程在执行exit时将自己唤醒 */
thread_block(TASK_WAITING);
}
}
}
sys_exit
子进程用来结束自己,退出时的事项:1、在自己的pcb中留下退出状态;2、将自己的子进程全部过继给init;3、回收自己除pcb与页表外的资源;4、可能有父进程在等待自己调用exit
,所以还要唤醒等待的父进程;5、阻塞自己,也就是换下cpu。这个函数会被运行库调用,进程即使不主动调用,也会执行
#include "debug.h"
/* 子进程用来结束自己时调用 */
void sys_exit(int32_t status)
{
struct task_struct *child_thread = running_thread();
child_thread->exit_status = status;
if (child_thread->parent_pid == -1)
{
PANIC("sys_exit: child_thread->parent_pid is -1\n");
}
/* 将进程child_thread的所有子进程都过继给init */
list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);
/* 回收进程child_thread的资源 */
release_prog_resource(child_thread);
/* 如果父进程正在等待子进程退出,将父进程唤醒 */
struct task_struct *parent_thread = pid2thread(child_thread->parent_pid);
if (parent_thread->status == TASK_WAITING)
{
thread_unblock(parent_thread);
}
/* 将自己挂起,等待父进程获取其status,并回收其pcb */
thread_block(TASK_HANGING);
}
将sys_wait
与sys_exit
封装成系统调用
添加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT
};
创建用户态系统调用入口,修改(lib/user/syscall.c)
/* 以状态status退出 */
void exit(int32_t status)
{
_syscall1(SYS_EXIT, status);
}
/* 等待子进程,子进程状态存储到status */
pid_t wait(int32_t *status)
{
return _syscall1(SYS_WAIT, status);
}
声明,用户态系统调用函数入口,修改(lib/user/syscall.h)
void exit(int32_t status);
pid_t wait(int32_t* status);
12
系统调用表中,添加实际系统调用处理函数,修改(userprog/syscall-init.c)
#include "wait_exit.h"
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
put_str("syscall_init done\n");
}
将exit
函数集成到运行库中。这样,即使程序中没有明确调用exit
,它也会在程序结束时自动被调用,与_start
相同。需要特别注意的是,子进程的退出机制与普通的函数返回机制不同。当子进程终止时,它并不是“返回”给其父进程;相反,它只是简单地结束了自己的执行。父进程和子进程在内存地址空间和执行上下文中是完全独立的,子进程不可能按照常规的函数调用方式“返回”一个值给父进程(做到这点需要其他的进程间通信机制支持)。取而代之的是,子进程提供一个退出状态,来描述其终止的方式或原因。因此,这里的push eax
并不是我们在普通函数调用中看到的那种返回值——比如一个指针或某种计算结果。实际上,它代表了子进程的结束状态,就像我们在每个main
函数中写的return 0
一样。
修改(command/start.S)
[bits 32]
extern main
extern exit
section .text
global _start
_start:
;下面这两个要和execv中load之后指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main
;将main的返回值通过栈传给exit,gcc用eax存储返回值,这是ABI规定的
push eax
call exit
;exit不会返回
cat
用于读取文件内容,不是以系统调用的方式存在,而是以用户进程的方式存在。核心原理就是调用read
系统调用,然后调用write
系统调用来打印
(command/cat.c)
#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char **argv)
{
if (argc > 2 || argc == 1)
{
printf("cat: only support 1 argument.\neg: cat filename\n");
exit(-2);
}
int buf_size = 1024;
char abs_path[512] = {0};
void *buf = malloc(buf_size);
if (buf == NULL)
{
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
}
else
{
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1)
{
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes = 0;
while (1)
{
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1)
{
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}
处理cat.c的脚本
(command/compile.sh)
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
现在我们有了能够退出程序的机制,终于不用再来用while(1)让程序卡住而不乱跳啦!
修改(shell/shell.c/my_shell)
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
/* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
while (1)
;
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
while (1)
;
}
}
为
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
int32_t status;
int32_t child_pid = wait(&status); // 此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1)
{ // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
panic("my_shell: no child\n");
}
printf("child_pid %d, it's status: %d\n", child_pid, status);
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
}
else
{
execv(argv[0], argv);
}
}
}
修改(kernel/main.c),main函数要完成cat程序从hd60M.img到hd80M.img的加载,并且要退出;init进程不断调用wait
来回收过继的僵尸进程资源
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21196;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("cat.c", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
int status;
int child_pid;
/* init在此处不停的回收僵尸进程 */
while (1)
{
child_pid = wait(&status);
printf("I`m init, My pid is 1, I recieve a child, It`s pid is %d, status is %d\n", child_pid, status);
}
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
支持代码,删除(userprog/fork.c/copy_pcb_vaddrbitmap_stack0)
ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
strcat(child_thread->name, "_fork");
小节j:
本节要支持管道,管道是用于父子进程通信的机制
管道本质上是位于内核空间的环形缓冲区。遵循Linux的设计哲学——一切皆文件,我们将管道也视为一个文件。这样,我们就可以通过文件描述符进行对管道的读写操作。在进行父子进程之间的通信时,父进程首先创建一个管道,从而得到两个文件描述符,一个用于读,另一个用于写。随后,父进程使用fork
创建子进程。子进程继承了父进程打开的文件,因此也可以通过这些文件描述符与管道进行通信,从而实现与父进程的交互。
- 匿名管道:仅对创建它的进程及其子进程可见,其他进程无法访问。
- 有名管道:可以被系统中的所有进程访问。
我们的管道机制将会复用现有文件系统
ioq_length
返回环形缓冲区内的数据长度
修改(device/ioqueue.c)
/* 返回环形缓冲区中的数据长度 */
uint32_t ioq_length(struct ioqueue *ioq)
{
uint32_t len = 0;
if (ioq->head >= ioq->tail)
{
len = ioq->head - ioq->tail;
}
else
{
len = bufsize - (ioq->tail - ioq->head);
}
return len;
}
函数声明,修改(device/ioqueue.h)
uint32_t ioq_length(struct ioqueue *ioq);
is_pipe
判断文件描述符对应的文件是不是管道
(shell/pipe.c)
#include "pipe.h"
#include "stdint.h"
#include "global.h"
#include "file.h"
#include "fs.h"
/* 判断文件描述符local_fd是否是管道 */
bool is_pipe(uint32_t local_fd)
{
uint32_t global_fd = fd_local2global(local_fd);
return file_table[global_fd].fd_flag == PIPE_FLAG;
}
函数声明,(shell/pipe.h)
#ifndef __SHELL_PIPE_H
#define __SHELL_PIPE_H
#include "global.h"
#define PIPE_FLAG 0xFFFF
bool is_pipe(uint32_t local_fd);
#endif
支持代码,修改(fs/fs.c)
static uint32_t fd_local2global(uint32_t local_fd)
为
uint32_t fd_local2global(uint32_t local_fd)
并添加函数声明,修改(fs/fs.h)
uint32_t fd_local2global(uint32_t local_fd);
sys_pipe
用于创建管道,核心就是创建了个全局打开文件结构,然后申请一页内核页,并让之前的文件结构内的fd_inode
成员指向这个内核页(之前的文件系统中,该成员指向一个struct inode),之后再将这个内核页起始位置创建struct ioqueue
并初始化。然后在进程中安装两个文件描述符,指向这个文件结构。最后记录下这两个文件描述符。
修改(shell/pipe.c)
#include "ioqueue.h"
/* 创建管道,成功返回0,失败返回-1 */
int32_t sys_pipe(int32_t pipefd[2])
{
int32_t global_fd = get_free_slot_in_global();
/* 申请一页内核内存做环形缓冲区 */
file_table[global_fd].fd_inode = get_kernel_pages(1);
/* 初始化环形缓冲区 */
ioqueue_init((struct ioqueue *)file_table[global_fd].fd_inode);
if (file_table[global_fd].fd_inode == NULL)
{
return -1;
}
/* 将fd_flag复用为管道标志 */
file_table[global_fd].fd_flag = PIPE_FLAG;
/* 将fd_pos复用为管道打开数 */
file_table[global_fd].fd_pos = 2;
pipefd[0] = pcb_fd_install(global_fd);
pipefd[1] = pcb_fd_install(global_fd);
return 0;
}
pipe_read
传入管道的文件描述符、一个缓冲地址、读取字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue
,然后调用ioq_getchar
从中读取数据即可。
修改(shell/pipe.c)
/* 从管道中读数据 */
uint32_t pipe_read(int32_t fd, void *buf, uint32_t count)
{
char *buffer = buf;
uint32_t bytes_read = 0;
uint32_t global_fd = fd_local2global(fd);
/* 获取管道的环形缓冲区 */
struct ioqueue *ioq = (struct ioqueue *)file_table[global_fd].fd_inode;
/* 选择较小的数据读取量,避免阻塞 */
uint32_t ioq_len = ioq_length(ioq);
uint32_t size = ioq_len > count ? count : ioq_len;
while (bytes_read < size)
{
*buffer = ioq_getchar(ioq);
bytes_read++;
buffer++;
}
return bytes_read;
}
pipe_write
传入管道的文件描述符、一个缓冲地址、写入字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue
,然后调用ioq_putchar
向其写入数据即可。
修改(shell/pipe.c)
/* 往管道中写数据 */
uint32_t pipe_write(int32_t fd, const void *buf, uint32_t count)
{
uint32_t bytes_write = 0;
uint32_t global_fd = fd_local2global(fd);
struct ioqueue *ioq = (struct ioqueue *)file_table[global_fd].fd_inode;
/* 选择较小的数据写入量,避免阻塞 */
uint32_t ioq_left = bufsize - ioq_length(ioq);
uint32_t size = ioq_left > count ? count : ioq_left;
const char *buffer = buf;
while (bytes_write < size)
{
ioq_putchar(ioq, *buffer);
bytes_write++;
buffer++;
}
return bytes_write;
}
函数声明,修改(shell/pipe.h)
int32_t sys_pipe(int32_t pipefd[2]);
uint32_t pipe_read(int32_t fd, void *buf, uint32_t count);
uint32_t pipe_write(int32_t fd, const void *buf, uint32_t count);
将sys_pipe
做成系统调用
增加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT,
SYS_PIPE
};
增加用户态系统调用入口,修改(lib/user/syscall.c)
/* 生成管道,pipefd[0]负责读入管道,pipefd[1]负责写入管道 */
int32_t pipe(int32_t pipefd[2])
{
return _syscall1(SYS_PIPE, pipefd);
}
声明用户态系统调用函数,修改(lib/user/syscall.h)
int32_t pipe(int32_t pipefd[2]);
系统调用表中,增加实际系统调用处理函数,修改(userprog/syscall-init.c)
#include "pipe.h"
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
syscall_table[SYS_PIPE] = sys_pipe;
put_str("syscall_init done\n");
}
sys_close
增加对管道文件的关闭代码,先调用is_pipe
判断文件描述符对应的文件结构是管道文件,然后文件结构中的fd_pos
-1(该成员记录管道文件的打开次数,在之前文件系统中,该成员记录文件当前操作的位置),如果此时fd_pos
为0,那么直接释放环形缓冲区对应的那页内存即可。
修改(fs/fs.c/sys_close)
#include "pipe.h"
/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd)
{
int32_t ret = -1; // 返回值默认为-1,即失败
if (fd > 2)
{
uint32_t global_fd = fd_local2global(fd);
if (is_pipe(fd))
{
/* 如果此管道上的描述符都被关闭,释放管道的环形缓冲区 */
if (--file_table[global_fd].fd_pos == 0)
{
mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
file_table[global_fd].fd_inode = NULL;
}
ret = 0;
}
else
{
ret = file_close(&file_table[global_fd]);
}
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
}
return ret;
}
sys_write
增加对管道文件的写入代码,在fd == stdout_no
增加调用is_pipe
判断文件描述符对应的文件是不是管道文件(标准输出有可能会被重定向为管道文件),如果是,则调用pipe_write
。然后增加fd即使不是标准输出判断是不是管道文件,如果是,调用pipe_write
写入。
修改(fs/fs.c/sys_write)
/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
{
if (fd < 0)
{
printk("sys_write: fd error\n");
return -1;
}
if (fd == stdout_no)
{
/* 标准输出有可能被重定向为管道缓冲区, 因此要判断 */
if (is_pipe(fd))
{
return pipe_write(fd, buf, count);
}
else
{
char tmp_buf[1024] = {0};
memcpy(tmp_buf, buf, count);
console_put_str(tmp_buf);
return count;
}
}
else if (is_pipe(fd))
{ /* 若是管道就调用管道的方法 */
return pipe_write(fd, buf, count);
}
else
{
uint32_t _fd = fd_local2global(fd);
struct file *wr_file = &file_table[_fd];
if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)
{
uint32_t bytes_written = file_write(wr_file, buf, count);
return bytes_written;
}
else
{
console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
return -1;
}
}
}
sys_read
增加对管道文件的读入代码,在fd == stdoin_no
增加调用is_pipe
判断文件描述符对应的文件是不是管道文件(标准输入有可能会被重定向为管道文件),如果是,则调用pipe_read
。然后增加fd即使不是标准输入判断是不是管道文件,如果是,调用pipe_read
读出。
修改(fs/fs.c/sys_read)
/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
ASSERT(buf != NULL);
int32_t ret = -1;
uint32_t global_fd = 0;
if (fd < 0 || fd == stdout_no || fd == stderr_no)
{
printk("sys_read: fd error\n");
}
else if (fd == stdin_no)
{
/* 标准输入有可能被重定向为管道缓冲区, 因此要判断 */
if (is_pipe(fd))
{
ret = pipe_read(fd, buf, count);
}
else
{
char *buffer = buf;
uint32_t bytes_read = 0;
while (bytes_read < count)
{
*buffer = ioq_getchar(&kbd_buf);
bytes_read++;
buffer++;
}
ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
}
}
else if (is_pipe(fd))
{ /* 若是管道就调用管道的方法 */
ret = pipe_read(fd, buf, count);
}
else
{
global_fd = fd_local2global(fd);
ret = file_read(&file_table[global_fd], buf, count);
}
return ret;
}
update_inode_open
增加对于管道文件的处理代码,如果是,那么fd_pos
+ 1
修改(userprog/fork.c/update_inode_open)
#include "pipe.h"
/* 更新inode打开数 */
static void update_inode_open_cnts(struct task_struct *thread)
{
int32_t local_fd = 3, global_fd = 0;
while (local_fd < MAX_FILES_OPEN_PER_PROC)
{
global_fd = thread->fd_table[local_fd];
ASSERT(global_fd < MAX_FILE_OPEN);
if (global_fd != -1)
{
if (is_pipe(local_fd))
{
file_table[global_fd].fd_pos++;
}
else
{
file_table[global_fd].fd_inode->i_open_cnts++;
}
}
local_fd++;
}
}
release_prog_resource
增加程序退出时对于打开的管道文件资源的处理代码,原理与sys_close
增加的代码一样
修改(userprog/wait_exit.c/release_prog_resource)
#include "pipe.h"
#include "file.h"
static void release_prog_resource(struct task_struct *release_thread)
{
uint32_t *pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t *v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分
uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t *v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分
uint32_t *first_pte_vaddr_in_pde = NULL; // 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;
/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr)
{
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001)
{ // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr)
{
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001)
{
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}
/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t *user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);
/* 关闭进程打开的文件 */
uint8_t local_fd = 3;
while (local_fd < MAX_FILES_OPEN_PER_PROC)
{
if (release_thread->fd_table[local_fd] != -1)
{
if (is_pipe(local_fd))
{
uint32_t global_fd = fd_local2global(local_fd);
if (--file_table[global_fd].fd_pos == 0)
{
mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
file_table[global_fd].fd_inode = NULL;
}
}
else
{
sys_close(local_fd);
}
}
local_fd++;
}
}
测试管道的用户进程(command/prog_pipe.c)
#include "stdio.h"
#include "syscall.h"
#include "string.h"
#include "stdint.h"
int main(int argc, char **argv)
{
int32_t fd[2] = {-1};
pipe(fd);
int32_t pid = fork();
if (pid)
{ // 父进程
close(fd[0]); // 关闭输入
write(fd[1], "Hi, my son, I love you!", 24);
printf("\nI`m father, my pid is %d\n", getpid());
return 8;
}
else
{
close(fd[1]); // 关闭输出
char buf[32] = {0};
read(fd[0], buf, 24);
printf("\nI`m child, my pid is %d\n", getpid());
printf("I`m child, my father said to me: \"%s\"\n", buf);
return 9;
}
}
处理prog_pipe的脚本
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="prog_pipe"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
测试代码,用于将prog_pipe从hd60M.img加载到hd80M.img中
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21432;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/prog_pipe", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
int status;
int child_pid;
/* init在此处不停的回收僵尸进程 */
while (1)
{
child_pid = wait(&status);
printf("I`m init, My pid is 1, I recieve a child, It`s pid is %d, status is %d\n", child_pid, status);
}
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
小节k:
在shell中支持管道
一般来说,键盘充当程序的输入源,而屏幕则是程序的输出目标,这被称为标准输入和输出。然而,程序也可以从文件接收输入或将其输出发送到文件中,这种方式被称为非标准输入和输出。当我们想从标准输入输出切换到文件输入输出时,我们使用输入输出重定向。通过这种方式,我们可以将一个命令的输出用作另一个命令的输入,这正是管道的功能。在Linux中,这种操作通常是通过命令行的管道符“|”完成的。例如,在命令ls | grep kanshan
中,ls
命令列出当前目录下的所有文件并原本会将其输出到屏幕,但由于存在管道符|
,它的输出会利用管道重定向为grep
命令的输入。
sys_fd_redirect
其功能是将一个已有的文件描述符old_local_fd
重定向为另一个文件描述符new_local_fd
。实际用法如:fd_redirect(1,fd[1]);
(fd[1]是管道文件对应的文件描述符,其全局文件结构索引一定大于2)用于标准输入重定位到管道文件(结合sys_write
理解);fd_redirect(1,1);
用于恢复标准输出(结合sys_write
理解)
修改(shell/pipe.c)
/* 将文件描述符old_local_fd重定向为new_local_fd */
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
struct task_struct *cur = running_thread();
/* 针对恢复标准描述符 */
if (new_local_fd < 3)
{
cur->fd_table[old_local_fd] = new_local_fd;
}
else
{
uint32_t new_global_fd = cur->fd_table[new_local_fd];
cur->fd_table[old_local_fd] = new_global_fd;
}
}
函数声明,修改(shell/pipe.h)
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);
sys_help
打印系统支持的内部命令
修改(fs/fs.c)
/* 显示系统支持的内部命令 */
void sys_help(void)
{
printk("\
buildin commands:\n\
ls: show directory or file information\n\
cd: change current work directory\n\
mkdir: create a directory\n\
rmdir: remove a empty directory\n\
rm: remove a regular file\n\
pwd: show current work directory\n\
ps: show process information\n\
clear: clear screen\n\
shortcut key:\n\
ctrl+l: clear screen\n\
ctrl+u: clear input\n\n");
}
添加声明,修改(fs/fs.h)
void sys_help(void);
将sys_fd_redirect
与sys_help
做成系统调用
添加系统调用号,修改(lib/user/syscall.h)
enum SYSCALL_NR
{
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK,
SYS_READ,
SYS_PUTCHAR,
SYS_CLEAR,
SYS_GETCWD,
SYS_OPEN,
SYS_CLOSE,
SYS_LSEEK,
SYS_UNLINK,
SYS_MKDIR,
SYS_OPENDIR,
SYS_CLOSEDIR,
SYS_CHDIR,
SYS_RMDIR,
SYS_READDIR,
SYS_REWINDDIR,
SYS_STAT,
SYS_PS,
SYS_EXECV,
SYS_EXIT,
SYS_WAIT,
SYS_PIPE,
SYS_FD_REDIRECT,
SYS_HELP
};
实现用户态系统调用入口,修改(lib/user/syscall.c)
/* 将文件描述符old_local_fd重定向到new_local_fd */
void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
_syscall2(SYS_FD_REDIRECT, old_local_fd, new_local_fd);
}
/* 显示系统支持的命令 */
void help(void)
{
_syscall0(SYS_HELP);
}
声明用户态系统调用函数,修改(lib/user/syscall.h)
void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);
void help(void);
系统调用表添加实际系统调用处理函数,修改(userprog/syscall-init.c)
/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
syscall_table[SYS_READ] = sys_read;
syscall_table[SYS_PUTCHAR] = sys_putchar;
syscall_table[SYS_CLEAR] = cls_screen;
syscall_table[SYS_GETCWD] = sys_getcwd;
syscall_table[SYS_OPEN] = sys_open;
syscall_table[SYS_CLOSE] = sys_close;
syscall_table[SYS_LSEEK] = sys_lseek;
syscall_table[SYS_UNLINK] = sys_unlink;
syscall_table[SYS_MKDIR] = sys_mkdir;
syscall_table[SYS_OPENDIR] = sys_opendir;
syscall_table[SYS_CLOSEDIR] = sys_closedir;
syscall_table[SYS_CHDIR] = sys_chdir;
syscall_table[SYS_RMDIR] = sys_rmdir;
syscall_table[SYS_READDIR] = sys_readdir;
syscall_table[SYS_REWINDDIR] = sys_rewinddir;
syscall_table[SYS_STAT] = sys_stat;
syscall_table[SYS_PS] = sys_ps;
syscall_table[SYS_EXECV] = sys_execv;
syscall_table[SYS_EXIT] = sys_exit;
syscall_table[SYS_WAIT] = sys_wait;
syscall_table[SYS_PIPE] = sys_pipe;
syscall_table[SYS_FD_REDIRECT] = sys_fd_redirect;
syscall_table[SYS_HELP] = sys_help;
put_str("syscall_init done\n");
}
将help
封装成内建命令,修改(shell/buildin_cmd.c)
/* 显示内建命令列表 */
void buildin_help(uint32_t argc UNUSED, char **argv UNUSED)
{
help();
}
123456
声明,修改(shell/buildin_cmd.h)
void buildin_help(uint32_t argc UNUSED, char **argv UNUSED);
cmd_execute
去取代原有shell中执行内部与外部命令功能
修改(shell/shell.c)
/* 执行命令 */
static void cmd_execute(uint32_t argc, char **argv)
{
if (!strcmp("ls", argv[0]))
{
buildin_ls(argc, argv);
}
else if (!strcmp("cd", argv[0]))
{
if (buildin_cd(argc, argv) != NULL)
{
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
}
else if (!strcmp("pwd", argv[0]))
{
buildin_pwd(argc, argv);
}
else if (!strcmp("ps", argv[0]))
{
buildin_ps(argc, argv);
}
else if (!strcmp("clear", argv[0]))
{
buildin_clear(argc, argv);
}
else if (!strcmp("mkdir", argv[0]))
{
buildin_mkdir(argc, argv);
}
else if (!strcmp("rmdir", argv[0]))
{
buildin_rmdir(argc, argv);
}
else if (!strcmp("rm", argv[0]))
{
buildin_rm(argc, argv);
}
else if (!strcmp("help", argv[0]))
{
buildin_help(argc, argv);
}
else
{ // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid)
{ // 父进程
int32_t status;
int32_t child_pid = wait(&status); // 此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1)
{ // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
panic("my_shell: no child\n");
}
printf("child_pid %d, it's status: %d\n", child_pid, status);
}
else
{ // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1)
{
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
exit(-1);
}
else
{
execv(argv[0], argv);
}
}
}
}
新的my_shell
,主要功能是从用户获取命令行输入,解析并执行命令,尤其支持管道|
命令的功能。
主要新增部分:检查用户输入中是否包含管道符号|
- 如果有管道命令:
- 创建一个管道。
- 重定向标准输出到管道的写端。
- 解析并执行第一个命令。
- 重定向标准输入到管道的读端。
- 对于每一个中间的命令(除了最后一个):
- 解析并执行命令。
- 恢复标准输出到屏幕。
- 执行管道中的最后一个命令。
- 恢复标准输入为键盘。
- 关闭管道。
- 如果没有管道命令:
- 解析用户输入的命令。
- 如果参数数量超过了设定的最大值,则提示错误。
- 否则执行命令。
修改(shell/shell.c)
#include "pipe.h"
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{ // 若只键入了一个回车
continue;
}
/* 针对管道的处理 */
char *pipe_symbol = strchr(cmd_line, '|');
if (pipe_symbol)
{
/* 支持多重管道操作,如cmd1|cmd2|..|cmdn,
* cmd1的标准输出和cmdn的标准输入需要单独处理 */
/*1 生成管道*/
int32_t fd[2] = {-1}; // fd[0]用于输入,fd[1]用于输出
pipe(fd);
/* 将标准输出重定向到fd[1],使后面的输出信息重定向到内核环形缓冲区 */
fd_redirect(1, fd[1]);
/*2 第一个命令 */
char *each_cmd = cmd_line;
pipe_symbol = strchr(each_cmd, '|');
*pipe_symbol = 0;
/* 执行第一个命令,命令的输出会写入环形缓冲区 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
/* 跨过'|',处理下一个命令 */
each_cmd = pipe_symbol + 1;
/* 将标准输入重定向到fd[0],使之指向内核环形缓冲区*/
fd_redirect(0, fd[0]);
/*3 中间的命令,命令的输入和输出都是指向环形缓冲区 */
while ((pipe_symbol = strchr(each_cmd, '|')))
{
*pipe_symbol = 0;
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
each_cmd = pipe_symbol + 1;
}
/*4 处理管道中最后一个命令 */
/* 将标准输出恢复屏幕 */
fd_redirect(1, 1);
/* 执行最后一个命令 */
argc = -1;
argc = cmd_parse(each_cmd, argv, ' ');
cmd_execute(argc, argv);
/*5 将标准输入恢复为键盘 */
fd_redirect(0, 0);
/*6 关闭管道 */
close(fd[0]);
close(fd[1]);
}
else
{ // 一般无管道操作的命令
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
cmd_execute(argc, argv);
}
}
panic("my_shell: should not be here");
}
cat
新增无参数时从键盘获得输入(记得删除原有hd80M.img中的cat)
(command/cat.c)
#include "syscall.h"
#include "stdio.h"
#include "string.h"
#include "fs.h"
int main(int argc, char **argv)
{
if (argc > 2)
{
printf("cat: argument error\n");
exit(-2);
}
if (argc == 1)
{
char buf[512] = {0};
read(0, buf, 512);
printf("%s", buf);
exit(0);
}
int buf_size = 1024;
char abs_path[512] = {0};
void *buf = malloc(buf_size);
if (buf == NULL)
{
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/')
{
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
}
else
{
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1)
{
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes = 0;
while (1)
{
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1)
{
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}
处理cat的脚本
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
../kernel/ -I ../device/ -I ../thread/ -I \
../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
测试代码,(kernel/main.c)
#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"
void init(void);
int main(void)
{
put_str("I am kernel\n");
init_all();
uint32_t file_size = 21816;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk *sda = &channels[0].devices[0];
void *prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/cat", O_CREAT | O_RDWR);
if (fd != -1)
{
if (sys_write(fd, prog_buf, file_size) == -1)
{
printk("file write error!\n");
while (1)
;
}
}
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
int status;
int child_pid;
/* init在此处不停的回收僵尸进程 */
while (1)
{
child_pid = wait(&status);
printf("I`m init, My pid is 1, I recieve a child, It`s pid is %d, status is %d\n", child_pid, status);
}
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
运行出错,排查后需要修改(device/ioqueue.h)
#define bufsize 64 //定义缓冲区大小.
为
#define bufsize 2048 //定义缓冲区大小.
下班下班