MIT操作系统神课 - 6.828

想系统学习操作系统的朋友们一定听说过MIT 6.828,这个项目是 MIT大学开源的一个操作系统课程,该课程由浅入深,以理论与实践结合的方式讲解了操作系统的原理以及通过实验循序渐进的方式实现了一个简单的xv6内核。从2020年开始,6.828这门课被拆分为了6.828和6.S081两个独立的课程,6.S081作为本科生的操作系统导论课, 而6.828作为研究生水平的课程专注于对操作系统更深入的研究。

所以本系列博客将以6.S081: Operating System Engineering的主要内容作为参考来进行学习。因为操作系统的知识点较多且相对复杂,所以本人在写学习笔记的时候难免会出现错误,如果内容有不完整或错误的地方,希望大家能够指出。官网提供的xv6参考手册内容很短,但是含金量很高,所以该系列博客主要包括了对手册的内容进行精读以及Lab的内容。

听说你会C语言了?一起来写个操作系统吧!

第一章 操作系统接口

操作系统这个词对大家来说肯定都不陌生,Windows、Linux、macOS以及iOS、Android…这些操作系统其实每天都和我们进行交互,它们在我们每天使用的手机、电脑、平板中。那么究竟什么是操作系统?这些不同名字的操作系统有什么共同之处?希望通过这门课程的学习对操作系统能够有一个比较全面的认识。

总的来说,操作系统能够管理并对底层的硬件进行抽象,然后为使用的用户提供更多的服务。这样,运行在操作系统上的软件不需要考虑自己具体运行的底层硬件架构。操作系统通过管理硬件资源,可以让多个软件同时运行,也可以控制软件之间的交互,可以让它们之间进行数据的共享和传输。

内核,是一个特殊的应用程序,目的是为运行在操作系统上的程序提供服务。在操作系统之上运行的每个程序,都被称之为进程,当一个进程需要调用内核服务的时候,它必须通过系统调用system call来实现。每一个系统调用就是操作系统的接口。通过系统调用可以进入内核,然后在内核提供服务并返回。所以一个进程可以在用户空间user space和内核空间kernel space进行执行命令.

所有系统调用的集合就是一个内核提供给用户运行程序时可使用的接口。xv6是一个简化的,类Unix的操作系统,所·以它提供了Unix内核里部分重要的接口,所有xv6的系统调用在下表中给出。接下来将主要从从进程和内存文件描述符管道文件系统几个部分,借一些代码片段来展示shell是如何通过系统调用作为接口实现自己功能的。

shell这个词的本意是“外壳”,用于比喻内核外面的一层,实际上就是用户跟内核进行交互的界面。所以shell其实也是一个用户程序,这个应用程序运行时,通过接收用户输入的命令,然后将命令交给操作系统执行,最后将结果返回。所以shell也被称为命令行环境(command line interface,简写为 CLI)。

系统调用和函数调用看起来很像,但是他们还是有很多不同点。真正去进行系统调是一个个的进程,进程需要调用内核服务的时候,就需要通过调用系统调用来完成。实际上在调用系统函数时,当前的进程执行就从用户态变成了内核态,进而可以调用操作系统下的硬件资源以及其他在用户态下无法完成的功能。一般情况下,操作系统以上的运行在用户态的进程是无法控制除了自己运行内存之外的内存。但是系统调用相当于转交了优先级,让进程可以完成更多的功能,同时也带来了更多待解决的问题。

1.1 进程和内存

int fork()

内核为每个在运行的进程都分配了一个标志符,被称为PID。fork系统调用用于创建新的进程,在子进程,函数返回值为0,而在父进程,返回子进程的PID。在运行fork系统调用之后,将由父进程创建了子进程,然后两个进程同时运行。

因为fork在不同的进程里有不同的返回值,说明fork函数同时运行在两个进程中,而在fork函数之前的语句只运行在父进程里,之后的语句在两个进程中同时运行。

int exit(int status)

exit系统调用用于告诉当前进程停止运行,并释放所占用的资源,其中包括内存资源以及被打开的文件*(这里打开的文件一般是指文件标志符,下面会进行详细的解释)*。exit函数将一个整数值作为传入参数,其中0表示运行成功,1表示运行失败。

里面传进去的status参数其实是在当前进程终止后传给父进程看的,例如当在子进程里某个运行分支出现问题时,可以通过运行exit(1),在停止运行子进程的同时,告诉父进程子进程运行出现问题,以便父进程对当前返回的错误状态进行处理。

int wait(int *status)

wait返回当前进程已经exit的子进程的PID,同时把子进程exit传入的status参数传递到某一个地址,该地址即为wait函数的传入参数status。

所以,wait函数一般在父进程里使用,当父进程运行到wait函数时,会在当前一直等待,直到自己的某个子进程exit,并传回子进程exit的状态,然后将该状态传到某一地址status里保存。否则,父进程将会阻塞在wait函数,一直等待某个子进程exit。而如果当前调用wait的进程没有子进程,那么wait函数马上返回-1。如果父进程并不在意子进程exit时的状态,可以将直接将0传入wait函数中。

尽管子进程最初拥有与父进程相同的内存内容,但父进程和子进程是以不同的内存和不同的寄存器执行的:改变一个进程中的变量并不影响另一个进程。例如,当wait的返回值被存储到父进程的pid中时,它并没有改变子进程中的变量pid。子进程中的pid的值仍然是0。

int exec(char *file, char *argv[])

通过调用exec函数,可以将另一个新的进程放到该调用函数当前运行内存空间上来运行。也就是说,让另一个进程进入到当前调用函数的进程的运行内存里,并从当前点开始运行自己的程序。这个程序被放在某个文件里,并保存在的内存的某个地址上。文件有专门的格式,例如ELF格式,其中文件里规定了哪一部分是命令,哪一部分是数据,指令从哪里开始等等。

exec需要传入两个参数,第一个参数为某一个可执行文件的地址,第二个为第一个执行文件执行时所需的字符串参数数组。在Linux系统中,exec是一个函数族,其中包括不同的exec函数有不同的传入参数方式:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

**对于exec函数族来说,它的作用通俗来说就是使另一个可执行程序替换当前的进程。当我们在执行一个进程的过程中,通过exec函数使得另一个可执行程序A的数据段、代码段和堆栈段取代当前进程B的数据段、代码段和堆栈段,那么当前的进程就开始执行A中的内容,而这一过程中不会创建新的进程,而且PID也没有改变。**也就是说,对系统而言,还是同一个进程,不过此时运行的已经是另一个程序了。

shell其实也是一个程序,程序代码在sh.c

在sh.c的main函数里,在while循环里,用getcmd函数读取用户的输入,然后用fork创建一个进程来运行该命令,然后作为父进程,wait等待子进程结束运行并返回。此时在子进程里就会运行命令行里输入的命令,例如echo hello,实际上runcmd是调用了exec函数,这样就不会浪费额外的地址空间,让echo函数运行在当下子进程里。

runcmd最后一句话就是exit(0),所以当echo运行在当前进程,然后结束时,就会把成功结束的状态返回给main函数的wait,也就是父进程里。

1.2 I/O 和文件描述符

如果对文件描述符不是太熟悉的话,推荐去看看《C程序设计语言》,其中第7章-输入与输出第8章-UNIX系统接口对这部分内容有更详细和全面的介绍。

在Unix操作系统中非常重要的一点,就是把所有东西都视为文件,比如外围设备(键盘和显示器),以及每一个运行的程序其实在系统中都是一个文件。而一个程序如果要和环境产生反应和交互,就必要要有输入输出。在Unix操作系统中,所有的输入和输出都是对文件进行的,即进行所有输入输出时所面向的对象都是文件。

大部分情况下的操作都是对文件进行的,所以说你对文件进行处理时,你可能是读取read内容,也可能是对内容修改write写入。所以最开始你要告知系统你将要对文件进行处理,这一过程就叫opening the file

此时系统会检查你是否能够对文件进行处理,比如文件是否存在?你是否对文件有读取修改权限?如果可以的话,就返回一个非负整数叫file descriptor,即fd。当需要对文件输入输出时,都需要用文件描述符fd来标识文件。

前面说过,当shell在运行的时候,输出输入是发生在屏幕和键盘之间,所以在shell打开运行时,就有了三个文件被打开,同时用文件描述符012表示。分别叫做标准输入标准输出标准错误。所以在shell的代码里需要保证已经为面板打开了三个文件。

所以此时当一个程序在读取文件0,写入1和2的时候,如果没有打开其他的文件,就会默认将输入输出内容放到上面三个由文件描述符标识的文件里。

对文件进行读写

对文件进行读写需要用到read和write系统调用函数。

第一个参数是文件描述符,表示是对当前open的哪个文件进行操作,第二个参数是字符串数组,是需要写入或者读出的内容地址。第三个参数是传输的字节数。

int n_read = read(int fd, char* buf, int n)int n_written = write(int fd, char* buf, int n)

返回值为传输内容的字节数,但是在读read操作时,返回的值可能比实际要求传输的字节数int n要少,因为文件本身可能就没有那么多字节。返回值为0,表示读到了文件末尾,返回值为-1时表示读取错误。

当进行write操作时,返回值为最后写进文件的字节数,如果最终写入的数不等于int n,表明写入发生了错误。

int open(char *file, int flags)

前面说了默认的012已经标识了三个文件,所以如果要对其他的文件进行操作的话。可以用open,但是用open去打开一个不存在的文件会发生错误,所以用create可以创建文件或者重写已经存在的文件。

open的第二个参数是标志符,用于标识文件只读,只写,可读写,可创建,具体值在fcntl.h中定义。

在xv6里,内核为每个进程的文件描述符都有一张索引表。在每个进程里,都有一个独立的空间用于存放文件描述符以及打开文件的索引,都是从0开始。

fork的子进程继承了父进程的文件描述符表,而exec的进程虽然占用了调用进程的内存,但是拥有的还是自己的文件描述符表。

int close(fd)

close(fd)释放一个文件描述符fd,然后等待open创建分配给下一个文件。一般来说,新分配的fd一般是还未使用的最小值,意思就是从0开始寻找没有使用的最小整数作为打开文件的fd。

下面用一段代码进行举例:

char* argv[2];argv[0] = "cat";
argv[1] = 0;if(fork() == 0) { close(0); open("input.txt", O_RDONLY); exec("cat", argv);
}

上面这一段代码的功能其实就是cat < input.txt

因为在子进程里,继承了父进程的fd表,所以需要现释放fd=0,然后open input.txt,因为fd为0的文件被释放,所以此时为input.txt分配的fd为0。前面提到过,尽管子进程最初拥有与父进程相同的内存内容,但父进程和子进程是以不同的内存和不同的寄存器执行的:改变一个进程中的变量并不影响另一个进程。所以只会改变子进程的fd表,而父进程的fd表不会被改变。

然后cat对fd=0的文件,也就是input.txt进行操作。exec第一个参数表明执行cat函数,然后就是cat运行的参数数组,包括命令本身,也就是执行cat 0,即cat input.txt

所以fork和exec的另一个区别的好处除了对资源的利用,还能让程序在调用fork或exec时,可以选择是否继续使用当前的输入输出状态,也就是fd表。

下面另一个例子:

if(fork() == 0) { write(1, "hello ", 6); exit(0); } else {wait(0); write(1, "world\n", 6); }

这里在父进程里的wait函数,会一直等待子进程结束后才会继续运行后面的代码,所以说这样保证了hello出现在world的前面。如果没有这一句,可就不一定了。

int dup(int fd)

dup系统调用复制一个当前存在的fd,然后返回一个新的fd指向同一个文件。两个fd指向同一个文件,于是对不同的fd进行读取写入可以得到相同的效果,就类似于两个指针同时指向了同一个内容。

1.3 管道

pipe对进程来说就是一对文件描述符,一个用于读,一个用于写。就像一根管道,在pipe的一端写入数据,在另一端就可以读到。管道是单向的,在内核中通常需要一定的缓冲区来缓冲消息,

pipe用于进程之间的通信,一次只建立一个通信,且是单向的, 一个进程在一端写入,另一个进程在一端读取。

pipe(int fd[])

该函数创建一个pipe,fd[0]表示读取端, fd[1]表示写入端

pipe的端口其实就像是文件

管道在Unix系统中会被当作文件,用pipe建立起一个管道的时候,为fd[0]和fd[1]分配了两个文件描述符fd,一个pipe的两端相当于是两个文件,一个文件用于读取,一个文件用于写入。

因为分配了fd,所以pipe相当于是打开的,只不过特别的fd[0]是读取端,fd[1]是写入端。所以当我们需要向写入端口fd[1](文件fd[1])时可以用write(fd[1], buf[], n),从端口fd[0]读取时read(fd[0], buf[], n)

pipe经由fd返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。

因为前面说了,当父进程fork的时候,子进程会继承父进程的fd表,pipe是一种特殊类型的文件,所以在fork的时候,子进程也会继承父进程的pipe,那就是说,子进程和父进程都共享了一个pipe,因此可以通过pipe进行通信。虽然在父进程fork的时候,子进程和父进程的fd表是一样的,但是之后的修改不会相互影响。

一次pipe产生一个管道,但是在父进程和子进程里都有fd,所以说是都知道的,所以都可以在pipe上读取和写入内容。但是fork之后,我们要决定数据的流向。管道不会限制谁写入谁读取,所以在父进程里关闭读取端fd[0],子进程关闭写入端fd[1]时,这样就可以在父进程通过fd[1]写入,子进程通过fd[0]读取。反过来,如果在父进程关闭写入端fd[1],子进程关闭读取端fd[0],子进程就可以在fd[1]写入,父进程在fd[0]读出。

下面的程序,因为是要在子进程里运行wc程序,所用到的参数需要父进程传递给子进程。所以需要在父进程里关闭读取端fd[0],子进程关闭写入端fd[1]时,就可以在父进程通过fd[1]写入,子进程通过fd[0]读取。

int p[2]; //大小为2的数组用于存放2个fd
char * argv[2]; //wc的参数argv[0] = "wc";
argv[1] = 0;pipe(p); //创建pipe
if(fork() == 0) { //在子进程里close(0); //释放fd=0dup(p[0]);  //让0指向p[0],因为从最小fd的开始,让fd=0变成读取端,目的是为了关闭p[0]后,还能通过fd=0从pipe读取端读到信息close(p[0]); //释放p[0],然后就只剩下fd=0指向pipe读取端close(p[1]); //释放p[1]exec("/bin/wc", argv); //执行wc 0,也就是wc <读取内容>} else { close(p[0]); //关闭读取端write(p[1], "hello world\n", 12); //像p[1]写入 close(p[1]); //关闭写入端
}

shell里的|其实就是pipe,将左右两边的程序联系起来,可以互相通信交流

如果在pipe端口上read的时候,没有写入,那么read会一直等待,知道有数据写入,或者等到所有fd关闭,如果所有fd都被关闭的话,此时read会返回0,就好像之前的数据已经全部接收到了,并到了文件结束。

事实上,当不可能再有信息写入的时候,read就会自动结束。或者当pipe不可能再有新数据写入的时候,read就会结束。所以在上面的代码里,在子进程里一定要把写入端关闭,如果不关闭,子进程就可能通过fd[1]向pipe写入数据,那么read就永远无法读到文件末尾,程序就会一直等待下去。

1.4 文件系统

在xv6里,chdir也就是cd命令,可以用这个函数调用实现对当前文件目录的切换,参数可以使用相对路径或绝对路径。

chdir("/a");
chdir("b");
open("c", O_RDONLY);
open("/a/b/c", O_RDONLY);

mkdir创建一个新的目录

有很多的系统调用可以创建一个新的文件或者目录:mkdir 创建一个新的目录,open 加上 O_CREATE 标志打开一个新的文件,mknod 创建一个新的设备文件。下面这个例子说明了这3种调用:

mkdir("/dir");
fd = open("/dir/file", O_CREATE|O_WRONGLY);
close(fd);
mknod("/console", 1, 1);

mknod 在文件系统中创建一个文件,但是这个文件没有任何内容。相反,这个文件的元信息标志它是一个设备文件,并且记录主设备号和辅设备号(mknod 的两个参数),这两个设备号唯一确定一个内核设备。当一个进程之后打开这个文件的时候,内核将读、写的系统调用转发到内核设备的实现上,而不是传递给文件系统。

因为前面提到过,在Unix系统里,所有的资源都被抽象成了文件,硬件设备也不例外。以Linux操作系统举例,所有的设备都以文件的形式存放在了/dev目录下,也被称为设备文件。所以可以通过对这些文件打开关闭进行读写操作。为了管理这些设备,系统需要为这些设备进行编号,其中每个设备有一个主设备号和次设备号。主设备号用于区分不同种类的设备,次设备号用于区分同类型的不同设备。

fstat可以通过一个文件的文件描述符得到文件的信息,信息是一个stat结构,在stat.h中定义为

关于inode

inode本质上是一个关于文件的数据结构,它存储了所有关于文件的信息,除了其名字和文件里的数据。

文件名和这个文件本身是有很大的区别。同一个文件(称为 inode)可能有多个名字,称为连接 (links)。系统调用 link 创建另一个文件系统的名称,它指向同一个 inode。下面的代码创建了一个既叫做 a 又叫做 b 的新文件,对文件a进行读取和写入等同于对b进行读取和写入。每个inode都由一个唯一的节点号来标识,在ftat里nlink用于记录当前inode被多少文件链接,对于当前代码,nlink应该等于2。

open("a", O_CREATE|O_WRONLY);
link("a", "b");

系统调用 unlink可以 从文件系统移除一个文件名,但是open一个file并删除链接的话,是创建一个临时 inode 的最佳方式,这个 inode 会在进程关闭 fd 或者退出的时候被清空。

fd = open("/tmp/xyz", O_CREATE|O_RDWR);unlink("/tmp/xyz");

欢迎关注我的公众号Coderoger,原创文章第一时间推送。如果你觉得这篇文章对你有用的话,记得点赞分享一下呀。

MIT操作系统神课 - 6.828相关推荐

  1. 操作系统结课论文——进程的概念与状态

    操作系统结课论文 题 目:进程的概念与状态 摘 要 本文主要是介绍进程的一些基本概念与管理应用.进程是操作系统中最重要.最基本的概念之一.它是系统分配资源的基本单位,是一个具有独立功能的程序段对某个数 ...

  2. MIT 操作系统 jos make grade出现no jos.out

    问题描述: 在MIT操作系统作业2012版中(貌似是..),在lab3中,只要make grade,出现no jos.out,并没有分数的出现. 问题原因: 启动qemu时,没有启动对应的tcp端口2 ...

  3. 儒猿秒杀季!ZooKeeper从0基础到源码级大神课

    疯狂秒杀季:238元秒杀 原价 2198元 的 <ZooKeeper从0基础到源码级大神课> 今天 上午10点,仅 36 套,先到先得! === 课程内容 === 由于疫情影响了线下的流量 ...

  4. 麻省理工MIT大神解说数学体系;2012年计算机博士港中大林达华简历(公号回复“MIT林达华”下载彩标PDF论文)

    麻省理工MIT大神解说数学体系:2012年计算机博士港中大林达华简历(公号回复"MIT林达华"下载彩标PDF论文) 原创: 林达华 数据简化DataSimp 今天 数据简化Data ...

  5. ❤️MIT大神写给女神的Q版Python画图库—Cutecharts

    MIT大神写给女神的Q版Python画图库-Cutecharts 画图不好看?不可爱?不萌?本文二哥教大家来进行Q版绘图. [建议先点赞.再收藏] 还记得那是一个月黑风高的晚上,一位女同事让我给他讲解 ...

  6. linux操作系统说课稿,信息技术《揭开LINUX的神秘面纱》教案范文

    信息技术<揭开LINUX的神秘面纱>教案范文 教学目标: 1.会启动LINUX系统: 2.会关闭LINUX系统: 3.LINUX基本界面的认识. 教学重点: 1.会启动LINUX系统: 2 ...

  7. MIT 操作系统实验 MIT JOS lab1

    JOS lab1 首先向MIT还有K&R致敬! 没有很好的开源环境我不可能拿到这么好的东西. 向每一个与我一起交流讨论的programmer致谢!没有道友一起死磕,我也可能会中途放弃. 跟丫死 ...

  8. 计算机操作系统强化课笔记(文件系统)(考研)

    强化课笔记(文件系统) 一.MP3文件结构: 1.MP3frame相当于文件记录(相当于一个struct结构体),属于有结构文件,Mp3Fream由Mp3Header(头信息)和Mp3Data(数据信 ...

  9. MIT操作系统实验lab1(pingpong案例:附代码、详解)

    1.题目描述:在xv6上实现pingpong程序,即两个进程在管道两侧来回通信.父进程将"ping"写入管道,子进程从管道将其读出并打印<pid>:received p ...

  10. 王道操作系统网课笔记合集

    文章目录 介绍 操作系统是什么? 操作系统几大特征 操作系统历史 OS 运行机制和体系结构 中断 系统调用 进程 进程的几种状态 进程控制 进程通信 线程 进程调度和切换 调度时机 调度方式 调度算法 ...

最新文章

  1. android悬浮动态权限,android应用内悬浮窗-自动贴边,不需要权限!
  2. Android自定义控件之轮播图控件
  3. dilink智能网联系统鸿蒙系统,【图】秦Pro DM DiLink智能网联系统实测解读_汽车江湖...
  4. 根据netmask快速判断是否在一个网域
  5. 开启Nginx压缩,解决前端访问慢问题
  6. GIL锁,线程锁(互斥锁)和递归锁
  7. SHELL 分析 列出当天访问次数最多的IP
  8. lodop转到其他html页面,vue项目中使用Lodop实现批量打印html页面和pdf文件
  9. touch 创建一个普通文件或更新已有文件的时间
  10. 信息论基础 原书第二版 中文版
  11. 华为笔试题:根据子网掩码判断两个IP地址是否在同一子网,并输出IP1的网络号
  12. [转] DevExpress GridView 排序状态下新增行不参与排序
  13. 小牛M+怎么样 看过你才知道
  14. python+mysql逆向_Python js逆向 爬取X天下数据,好好看,好好学
  15. 微信公众平台学习笔记
  16. HDU4417_树状数组加离线
  17. Surface reconstruction from unorganized points
  18. computer browser服务无法启动 错误1068 依存服务或组无法启动
  19. python图片内容识别_TensorFlow从1到2(五)图片内容识别和自然语言语义识别
  20. 数据结构之队列queue

热门文章

  1. 算法左神左程云耗尽5年心血分享程序员代码面试指南第2版文档
  2. PMP备考资料整理、模拟试题、章节练习
  3. python如何上传文件_python请求文件上传
  4. MyBatis架构图
  5. OptiSystem应用:激光雷达系统设计
  6. cad缩放_mac有没有好用的cad看图软件?CAD迷你看图 for Mac4.4.1激活版分享给大家...
  7. 【AD18新手入门】从零开始制造自己的PCB
  8. excel多元线性拟合_[求助]excel里面的linest函数中多元回归怎么用啊?
  9. Eclipse安装包官网无法下载,需修改镜像地址
  10. 堆排序(Java语言实现)