MIT6.S081 Introduction and Examples Note
xv6操作系统官方解释翻译 https://th0ar.gitbooks.io/xv6-chinese/content/index.html
xv6课程翻译 https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081
Youtube课程视频链接 https://www.youtube.com/watch?v=J3LCzufEYt0&list=PLTsf9UeqkReZHXWY9yJvTwLJWYYPcKEqK
操作系统目标
- 抽象硬件
- 多个应用程序之间共用硬件资源
- 隔离性,同时运行应用程序但互不干扰
- 在需要时实现共享
- 安全性,不能在任何时候都能共享
- 帮助应用程序高效利用硬件资源
- 能支持多种场景,包括应用程序等
Kernel内置服务
- 文件系统
- 管理文件内容并找出文件具体在磁盘中的哪个位置
- 独立的命名空间,包括层级目录
- Access Control
- 进程管理系统
xv6
xv6项目是运行在QEMU模拟器上的RISC-V微处理器上的小型操作系统,本系列的文章都是在该背景下写的笔记知识
系统调用接口
应用程序与Kernel交互,使用Kernel的API,即通过系统调用(System Call)来完成。
在xv6中系统调用接口的实现是通过在用户空间声明系统调用函数,在内核空间实际实现系统调用函数内部逻辑。
当用户在用户空间内想要进行系统调用时,会通过汇编语言编写的跳板部分将用户想要调用的系统调用名传入到内核空间,由操作系统在内核空间内执行系统调用函数,再将函数调用结果返回到用户空间中。
Shell中的指令运行,该指令是以文件的方式写在了文件管理系统中
read / wirte / exit
以应用程序 copy 为例,该程序实现了在Shell中对read, wirte, exit的系统调用
应用程序 copy 内容
int main(){
char buf[64];
while(1){
int n = read(0, buf, sizeof(buf));
if(n <= 0) break;
write(1, buf, n);
}
exit(0);
}
read(0, buf, sizeof(buf)) 中出现的参数0,write(1, buf, n)中出现的参数1,二者均为文件描述符
在默认情况下,当一个应用程序启动时,文件描述符0连接到Shell console的输入,文件描述符1连接到了Shell console的输出,文件描述符2连接到了Shell console的标准错误输出。
另外,以read函数为例
- 第二个参数是指向某段内存的指针
- 第三个参数是代码想读取的最大长度
open创建文件描述符
使用open系统调用,open系统调用会返回当前进程未使用的最小文件描述符序号
文件描述符本质上对应了内核中的一个表单数据,相同的文件描述符可能对应不同的文件
文件描述符(File Descriptor,fd)是一个用于标识已打开文件或 I/O 设备的非负整数。
它是进程与文件系统或其他 I/O 资源之间进行交互的抽象接口。
Shell内部的指令执行过程
用户通过Shell与机器交互
fork创建新的进程 -> 将要运行的指令作为参数传入exec系统调用
fork系统调用
调用fork后,操作系统将拷贝当前进程的所有东西(包括进程资源),创建并运行子进程,子进程结束后返回到父进程(shell)
在shell执行指令时,需要先fork创建一个进程出来来执行指令
先frok当前shell进程后exec某个指令的原因:
exec不会返回任何值,执行完对应指令后就会自动终止,但一般不希望shell也会被终止(shell本身也是一个进程),因此要新创建出一个子进程
调用fork()函数会返回当前的进程ID,可以通过fork的返回值区分旧进程和新进程
- 在原始的进程中,fork系统调用会返回大于0的整数
- 在新创建的进程中,fork系统调用会返回0
fork整个父进程会导致性能低下,如果父进程的资源内存占用较大的话,因此有优化选项,只拷贝执行exec所需要的资源
exec系统调用执行后会完全替换当前进程的内存
exec系统调用从指定文件中读取指令,执行这些指令,并不会返回,除非出错时(在kernel不能运行相应的程序文件时会报错)返回
exec / wait
以一个简单的应用程序为例,调用exec/wait的系统调用
int main(){
int pid, status;
pid = fork();
if(pid == 0){
char *argv[] = {"This", "is", "echo", 0};
exec('echo', argv);
printf("exec Wrong!");
exit(1);
}else{
printf("waiting for child process");
wait(&status);
printf("the child process exited with status:%d\n", status);
}
}
子进程执行的部分,有exit系统调用,设置其参数是1。如果执行到这里(但指令能正常运行的时候不会运行到这里),操作系统会将1从退出的子进程传递到wait调用,也就是等待的父进程中的wait()函数。
父进程中wait()函数的参数&status,是将status变量对应的地址传递给内核,内核会向这个地址写入子进程对exit()函数传入的参数。
wait系统调用只能等待当前进程的子进程。
- 如果有多个子进程,其中只要有一个子进程退出,那么单个wait就会返回
- 如果当前进程退出时没有子进程了,那么wait会返回
-1
如果一个程序成功的退出了,那么exit的参数会自动返回为0,如果出现了错误,会指定向exit传递1。
因此父进程可以读取wait的参数,并查看子进程是否成功的完成了。
从父进程拷贝的资源是什么?
C程序在编译之后,是一些在内存中的指令,这些指令存在于内存中。所以这些指令可以被拷贝,因为它们就是内存中的字节,它们可以被拷贝到别处。
重定向实现
Unix中的常见的用来重定向指令的输入输出的方法:
文件描述符1通常是进程用来作为输出的,Shell会将文件描述符1改为output文件,之后再运行指令。同时,父进程的文件描述符1并没有改变。所以先fork,再更改子进程的文件描述。
这种方法不会影响父进程的输入输出。一般只想重定向子进程的输出。
close(1)函数作用:让文件描述符1指向一个其他的位置,不使用原本指向console输出的文件描述符1。
int main(){
int pid = fork();
if(pid == 0){
close(1);
open("xxx.txt", XXX);
char *argv[] = {"This", "is", "echo"};
exec('echo', argv);
printf("exec failed!");
exit(1);
}else{
...
}
}