Orange'S:一个操作系统的实现

文件系统

Posted by BINBIN Blog on November 10, 2019

内存管理

fork

这章节打算做个shell,shell的原理就是用一个子进程来执行一个命令。 目前系统所有的进程都是编译的时候确定好的,每个进程的入口地址、堆栈等等都是在global.c中制定的,除了权限不同,所有进程都是并列的,没有层次关系。

根据添加任务步骤的总结,一个新的进程需要的要素有:

  • 自己的代码,数据和堆栈
  • 在proc_table[]中占用一个位置
  • 在GDT中占用一个位置,用以存放进程对应的LDT 描述符

那么这就又一个问题,第一项的代码 数据 堆栈从哪里来呢?

传统上,生成一个新的进程时,这些都是直接从某个已有的进程那里继承或者复制。这也能解释子进程这一概念:如果新的进程c的代码 数据 和堆栈式从已有的进程P而来,那么P被称为父进程(parent),c被称为子进程(child)。

生成一个子进程的系统调用被称为fork(),操作系统接到一个fork请求后,会将调用者复制一份,这时候就会有两个一模一样的进程同时运行。

备注,有时候为了提高效率,又是并不是真的复制,而是采取copy-on-write策略!

那么有第一个问题,是拿谁作为父进程来fork()?,你可能知道,在linux中所有的进程都有同一个祖先,那就是init进程,是跟minix学习的,minix中也有一个INIT进程,作为用户进程的祖先,既然如此,模仿一个init进程!

我们不光要增加一个init进程,还要增加一个MM 进程-本章说的就是内存管理 ,这个进程五一是最重要的。MM 将负责从用户进程接受消息,完成fork()等操作。

它分为三个部分,分别是GDT 进程表 进程体。 GDT 和进程表之间有两种联系,一是进程表的ldt_sel 指向的GDT中的某一项(2),而这一项目反过来指向进程表内的两个LDT描述符(1)。 两个LDT描述符所描述的内存范围,即为进程在内存中所占的空间,用(3)表示

之前所有的进程都是提前计算好的,比如进程的入口地址和堆栈长度等信息首先是记载global.c,然后关系(1)在init_port()中确定,关系(2)和关系(3)在kernel_main()中确定。

但是没办法由init进程fork出来的子进程的入口地址放在global.c中,也不可能实现分配好某个未知进程的内存,所以有些内容肯定要动态决定,不过也不需要都要fork来做。总结如下,这些工作可以提前做好

  • 在proc_table[]中预留一些空项,供新进程使用
  • 将proc_table[]中每一个进程表项中的ldt_sel项都设定好(关系(2))
  • 将进程所需的GDT表项都初始化好(关系(1))。

而新进程需要的内存空间,以及关系(3),则都需要MM来完成!

跟FS 一样 ,MM 也要一个主消息循环! 内存分配也参考书上所说,书上直接弄了一个简单直接的内存分配机制!

编译的第十章a章节代码

报错 sys/cdefs.h 的情况,输入以下命令安装标c库

sudo apt-get install build-essential libc6-dev libc6-dev-i386

自然编译通过了!

9

因为进程应该也有exit,wait

wait — 我们执行一个程序之后,需要判断其返回值,这个返回值通常是通过$?得到的!

所以是我们执行的进程返回给shell的,是子进程返回给父进程的!父进程得到的返回方法,就是执行一个wait()挂起!,等子进程退出的时候,wait()调用方结束,并且父进程因此得到返回值!

make 运行一下, 增加了exit wait

exec ,它将当前的进程映像替换成另外一个。也就是说我们可以从硬盘上读取另外一个可执行的文件,用它替换掉刚刚被fork出来的子进程,于是被替换的子进程摇身一变,就成了彻底彻尾的新鲜进程!

以shell中常见的echo命令为例子,我们输入 “echo hello world”,shell就会fork出一个子进程A ,这时候A 跟shell一模一样,fork结束后父进程和子进程分别判断自己的fork()返回值,如果是0是子进程A,这时候A马上执行一个exec(),于是进程A的内存映像被echo替换,他就变成了echo了!

在c章节中 kliba.asm 从原先的lib\kliba.asm移动到 kernel\kliba.asm中

为自己的操作系统编写应用程序

echo将以操作系统中普通应用程序的身份出现,它跟擦做系统的接口是系统调用。

本质上一个应用程序只能调用两种东西 : 属于自己的函数以及中断 (系统调用其实就是软中断)

根据经验,一个应用程序都会调用一些现成的函数,很少见写程序时里面满是中断调用的。

这是因为编译器偷偷的为我们链接了c运行时库 CRT,库里面有已经编译好的库函数代码。这样两者链接起来,应用程序就能正确运行了!

使用ar rcs 指令编译生成库! 本书的例子是 lib/orangescrt.a

假设应用程序时echo.c 编译的时候会报错,需要找到_start。编译链接器需要找到_start作为入口!


;; start.asm

extern	main
extern	exit

bits 32

[section .text]

global _start

_start:
	push	eax
	push	ecx
	call	main
	;; need not clean up the stack here

	push	eax
	call	exit

	hlt	; should never arrive here

_start虽然只有这几行,做了三件事

  • 为main函数准备参数 argc 和 argv
  • 调用main()
  • 将main() 函数的返回值通过exit传递给父进程

安装应用程序

将应用程序打成一个tar包,做成一个文件,然后放进去,在操作系统期待能够的时候将这个包解开,问题就解决了。 自然也要写个小程序来解开tar包。

  • 编写应用程序,并编译链接
  • 将链接好的应用程序达成一个tar包inst.tar。
  • 将inst.tar用工具dd写入磁盘(映像)的某段特定扇区(假设这一段的首山区的扇区号为X)。
  • 启动系统,这时候mkfs()会在文件系统中建立一个新的文件cmd.tar,他的inode中i_start_sect成员会被设为X。
  • 在某个进程中– 比如Init—将cmd.tar解包,将其中的包含的文件存入文件系统。

除了把echo打入包中,还加入了内核文件kernel.bin和另外一个简单的程序pwd。 写入dd命令还嵌入了其他命令!

打包tar文件,其实就是在每个文件的前面加一个512字节的文件头,并把所有文件叠放在一起。解包就是读取文件头,根据文件头里的记录的文件大小读出文件,然后是下一个文件头和下一个文件,如此循环。tar文件的文件头定义是从GNU tar的源代码借用过来的,我们用到两项,name 和 size 。需要注意的是size一项存放的是不是个整数,而是个字符串,而且用的是八进制,这就需要都出来之后转换一下!

先make os,在make 应用程序!

运行make install 之后的界面

会发下跟书上的例子不一样,暂时不深纠,初步判断pwd 那边出现问题,可能是dd写入的问题,也有可能是代码的问题,但是后续开发和学习是不会走这个make install

在makefile 文件里面 BIN = echo pwd 删除了pwd即可!

make一下,可以了!

实现exec

d章节

代码分为9个部分:

  1. 从消息体中获取各种参数。由于调用者和MM处在不同的地址空间,所以对于文件名这样的一段内存,需要通过获取其物理地址并进行物理地址复制。
  2. 通过一个新的系统调用stat() 获取被执行文件的大小。
  3. 将被执行我呢见全部读入MM自己的缓冲区,(MM 缓冲区有1MB,我们姑且假设这个空间足够了。等有一天真的不够了,会触发一个assert,到时候在做打算)
  4. 根据ELF文件的程序头 Program Header 信息,将被执行文件的各个段放置到合适的位置
  5. 建立参数栈—- 这个栈在execv()中已经备好了,但由于内存空间发生变化,所以里面所有的指针都需要重新定位,这个过程不难,通过一个delta变量可完成
  6. 为被执行成勋的eax 和 ecx赋值 —- 还记得_start中,我们将eax 和 ecx 压入栈吗,压的就是argv 和 argc的。他们是在这里被赋值的!
  7. 为程序的eip赋值,这是程序的入口地址,即_start出
  8. 为程序的esp赋值,注意要闪出刚才我们准备好的堆栈的位置。
  9. 最后是将进程的名字改成被执行程序的名字

make 运行一下!

只显示hello 这个问题后续在说!

下一步就是编写简单的shell

用Init进程启动两个shell ,分别运行在TTY1和TTY2,两个shell都是Init进程的子进程,同时它们也将生成自己的子进程。作者起了名称叫做shabby_shell。

后面就是书的结尾

11章节a部分,就是mkfs只运行一次