6.s081-note01
Introduction
系统调用跳到内核与函数调用跳到另一个函数的区别:
Kernel的代码有特殊权限,能直接访问硬件
read, write, exit系统调用
XV6是基于Unix的操作系统,并运行在RISC-V微处理器上。
read: 接受三个参数
- 第一个为文件描述符,指向一个之前打开的文件。0连接到console的输入,1则连接到了console的输出。许多的Unix系统都会从文件描述符0读取数据,然后向文件描述符1写入数据。
- 第二个参数是指向某段内存的指针,可以通过指针对应的地址读内存中的数据
- 第三个参数是像想读取的最大长度。所以这里的read最多只能从连接到文件描述符0的设备,也就是console中,读取64字节的数据。
open系统调用
- 第一个参数是用来创建文件用的
- 第二个参数是一些标志位,来告诉open系统调用再内核中的实现
Shell
大致讲了一些xv6Shell的内容
fork系统调用
fork会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据。之后,我们就有了两个拥有完全一样内存的进程(除了返回值不一样)
fork创建了一个新的进程。当我们在Shell中运行东西的时候,Shell实际上会创建一个新的进程来运行你输入的每一个指令。所以,当我输入ls时,我们需要Shell通过fork创建一个进程来运行ls,这里需要某种方式来让这个新的进程来运行ls程序中的指令,加载名为ls的文件中的指令(也就是后面的exec系统调用)。
exec, wait系统调用
代码会执行exec系统调用,这个系统调用会从指定的文件中读取并加载指令,并替代当前调用进程的指令. 操作系统从名为echo的文件中加载指令到当前的进程中,并替换了当前进程的内存,之后开始执行这些新加载的指令。
Unix并没有一个直接的方法让子进程等待父进程。wait系统调用只能等待当前进程的子进程。所以wait的工作原理是,如果当前进程有任何子进程,并且其中一个已经退出了,那么wait会返回。
如果一个进程调用fork两次,如果它想要等两个子进程都退出,它需要调用wait两次。每个wait会在一个子进程退出时立即返回。当wait返回时,你实际上没有必要知道哪个子进程退出了,但是wait返回了子进程的进程号,所以在wait返回之后,你就可以知道是哪个子进程退出了。
xv6 chapter 1 操作系统接口
传统型态内核:一个向其他运行中的程序提供服务的特殊程序 每一个正在运行的程序(称为进程),都拥有自己的包含指令、数据、栈的内存空间。指令实现程序的运算,数据是用于运算过程的变量,栈则管理程序的过程调用。一台计算机通常有许多进程,但只有一个内核。
当一个进程需要调用一个内核服务时,它就会调用系统调用,这是操作系统接口中的一个调用。系统调用会进入内核,让内核执行服务然后返回。所以进程会在用户空间和内核空间之间交替运行。
shell是一个普通的程序,它从用户读取命令并执行它们。shell是一个用户程序,而不是内核的一部分,这一事实说明了系统调用接口的强大:shell没有什么特别之处。这也意味着shell是很容易被替换的;因此,事实上现代Unix系统中有各种各样的shell,每个都有自己的用户界面和脚本特性。xv6 shell是Unix Bourne shell的一个简单实现。它的实现可以在(user/sh.c:1
)找到。
进程和内存
一个xv6进程由用户空间内存(指令、数据和堆栈)和内核私有的进程状态组成。Xv6对进程提供分时特性:它透明地切换当前cpu正在执行的进程。当一个进程暂时不使用cpu时,xv6会保存它的CPU寄存器,在下次运行该进程时恢复它们。内核为每个进程关联一个PID(进程标识符)。
可以使用fork系统调用创建一个新的进程。fork创建的新进程被称为子进程,其内存内容与调用的进程完全相同,原进程被称为父进程。在父进程和子进程中,fork都会返回。在父进程中,fork返回子进程的PID;在子进程中,fork返回0。
当exec成功时,它并不返回到调用程序;相反,从文件中加载的指令在ELF头声明的入口点开始执行。exec需要两个参数:包含可执行文件的文件名和一个字符串参数数组。
shell的主结构很简单,参见main(user/sh.c:145)。主循环用getcmd读取用户的一行输入,然后调用fork,创建shell副本。父进程调用wait,而子进程则运行命令。例如,如果用户向shell输入了echo hello,那么就会调用runcmd,参数为echo hello。runcmd (user/sh.c:58) 运行实际的命令。对于echo hello,它会调用exec (user/sh.c:78)。如果exec成功,那么子进程将执行echo程序的指令,而不是runcmd的。在某些时候,echo会调用exit,这将使父程序从main(user/sh.c:145)中的wait返回。
为什么fork和exec没有结合在一次调用中?我们后面会看到shell在实现I/O重定向时利用了这种分离的特性。为了避免创建相同进程并立即替换它(使用exec)所带来的浪费,内核通过使用虚拟内存技术(如copy-on-write)来优化这种用例的fork实现。
Xv6隐式分配大部分用户空间内存:fork复制父进程的内存到子进程,exec分配足够的内存来容纳可执行文件。一个进程如果在运行时需要更多的内存(可能是为了malloc),可以调用sbrk(n)将其数据内存增长n个字节;sbrk返回新内存的位置。
I/O和文件描述符
文件描述符是一个小整数,代表一个可由进程读取或写入的内核管理对象.
一个进程可以通过打开一个文件、目录、设备,或者通过创建一个管道,或者通过复制一个现有的描述符来获得一个文件描述符。
我们通常将文件描述符所指向的对象称为文件;文件描述符接口将文件、管道和设备之间的差异抽象化,使它们看起来都像字节流。我们把输入和输出称为I/O。
xv6内核为每一个进程单独维护一个以文件描述符为索引的表,因此每个进程都有一个从0开始的文件描述符私有空间。
一个进程从文件描述符0(标准输入)读取数据,向文件描述符1(标准输出)写入输出,向文件描述符2(标准错误)写入错误信息。
read/write系统调用可以从文件描述符指向的文件读写数据。(详见上方有关笔记)
- read(fd, buf, n)
- write(fd, buf, n)
若写入字节数小于n则该次写入发生错误。和read一样,write在当前文件偏移量处写入数据,然后按写入的字节数将偏移量向前推进:每次write都从上一次写入的地方开始。
文件描述符的使用和0代表输入,1代表输出的约定,使得cat可以很容易实现。
close系统调用会释放一个文件描述符,使它可以被以后的open、pipe或dup系统调用所重用(见下文)。新分配的文件描述符总是当前进程中最小的未使用描述符。
open的第二个参数由一组用位表示的标志组成,用来控制open的工作。可能的值在文件控制(fcntl)头(kernel/fcntl.h:1-5)中定义。O_RDONLY, O_WRONLY, O_RDWR, O_CREATE, 和 O_TRUNC, 它们分别指定open打开文件时的功能,读、写、读和写、如果文件不存在则创建文件、将文件长度截断为0。
dup系统调用复制一个现有的文件描述符,返回一个新的描述符,它指向同一个底层I/O对象。
Pipes 管道
管道是一个小的内核缓冲区,作为一对文件描述符提供给进程,一个用于读,一个用于写。将数据写入管道的一端就可以从管道的另一端读取数据。管道为进程提供了一种通信方式。
File system 文件系统
xv6文件系统包含了数据文件(拥有字节数组)和目录(拥有对数据文件和其他目录的命名引用)。这些目录形成一棵树,从一个被称为根目录的特殊目录开始。像/a/b/c这样的路径指的是根目录/中的a目录中的b目录中的名为c的文件或目录。不以/开头的路径是相对于调用进程的当前目录进行计算其绝对位置的,可以通过chdir系统调用来改变进程的当前目录。下面两个open**打开了同一个文件(假设所有涉及的目录都存在)。
文件名称与文件是不同的;底层文件(非磁盘上的文件)被称为inode,一个inode可以有多个名称,称为链接。每个链接由目录中的一个项组成;该项包含一个文件名和对inode的引用。inode保存着一个文件的metadata(元数据),包括它的类型(文件或目录或设备),它的长度,文件内容在磁盘上的位置,以及文件的链接数量。
unlink系统调用会从文件系统中删除一个文件名。只有当文件的链接数为零且没有文件描述符引用它时,文件的inode和存放其内容的磁盘空间才会被释放。
1
2
fd = open("/tmp/xyz", O_CREATE | O_RDWR);
unlink("/tmp/xyz");
这段代码是创建一个临时文件的一种惯用方式,它创建了一个无名称inode,故会在进程关闭fd或者退出时删除文件。