引言
操作系統(tǒng)的目標(biāo)
- abstract H/W
抽象化硬件
- multiplex
多路復(fù)用
- isolation
隔離性
- sharing
共享(進(jìn)程通信,數(shù)據(jù)共享)
- security / access control
安全性/權(quán)限控制
- performance
性能/內(nèi)核開銷
- range of applications
多應(yīng)用場景
操作系統(tǒng)概覽
操作系統(tǒng)應(yīng)該提供的功能:1. 多進(jìn)程支持 2. 進(jìn)程間隔離 3. 受控制的進(jìn)程間通信
-
xv6:一種在本課程中使用的類UNIX的教學(xué)操作系統(tǒng),運(yùn)行在RISC-V指令集處理器上,本課程中將使用QEMU模擬器代替
-
kernel(內(nèi)核):為運(yùn)行的程序提供服務(wù)的一種特殊程序。每個(gè)運(yùn)行著的程序叫做進(jìn)程,每個(gè)進(jìn)程的內(nèi)存中存儲(chǔ)指令、數(shù)據(jù)和堆棧。一個(gè)計(jì)算機(jī)可以擁有多個(gè)進(jìn)程,但是只能有一個(gè)內(nèi)核
每當(dāng)進(jìn)程需要調(diào)用內(nèi)核時(shí),它會(huì)觸發(fā)一個(gè)system call(系統(tǒng)調(diào)用),system call進(jìn)入內(nèi)核執(zhí)行相應(yīng)的服務(wù)然后返回。
操作系統(tǒng)的組織結(jié)構(gòu)如圖1所示

內(nèi)核提供的一系列系統(tǒng)調(diào)用就是用戶程序可見的操作系統(tǒng)接口,xv6 內(nèi)核提供了 Unix 傳統(tǒng)系統(tǒng)調(diào)用的一部分,它們是:

進(jìn)程和內(nèi)存
每個(gè)進(jìn)程擁有自己的用戶空間內(nèi)存以及內(nèi)核空間狀態(tài),當(dāng)進(jìn)程不再執(zhí)行時(shí)xv6將存儲(chǔ)和這些進(jìn)程相關(guān)的CPU寄存器直到下一次運(yùn)行這些進(jìn)程。kernel將每一個(gè)進(jìn)程用一個(gè)PID(process identifier)指代。在進(jìn)程執(zhí)行中,常常會(huì)使用fork
和exec
系統(tǒng)調(diào)用來創(chuàng)建新的進(jìn)程。如下面代碼所示:
fork and wait
-
fork
:形式:int fork()
。其作用是讓一個(gè)進(jìn)程生成另外一個(gè)和這個(gè)進(jìn)程的內(nèi)存內(nèi)容相同的子進(jìn)程。在父進(jìn)程中,fork
的返回值是這個(gè)子進(jìn)程的PID,在子進(jìn)程中,返回值是0 -
exit
:形式:int exit(int status)
。讓調(diào)用它的進(jìn)程停止執(zhí)行并且將內(nèi)存等占用的資源全部釋放。需要一個(gè)整數(shù)形式的狀態(tài)參數(shù),0代表以正常狀態(tài)退出,1代表以非正常狀態(tài)退出 -
wait
:形式:int wait(int *status)
。等待子進(jìn)程退出,返回子進(jìn)程PID,子進(jìn)程的退出狀態(tài)存儲(chǔ)到int *status
這個(gè)地址中。如果調(diào)用者沒有子進(jìn)程,wait
將返回-1 -
pipe
:形式:int pipe(int p[])
。創(chuàng)建一個(gè)管道,將讀/寫文件描述符放在p[0]和p[1]中 -
sbrk
:形式:char *sbrk(int n)
。將進(jìn)程的內(nèi)存增加n字節(jié)。返回新內(nèi)存的起始位置。
int pid = fork();
if (pid > 0) {
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if (pid == 0) {
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}
前兩行輸出可能是
parent: child=1234
child: exiting
也可能是
child: exiting
parent: child=1234
這是因?yàn)樵趂ork了之后,父進(jìn)程和子進(jìn)程將同時(shí)開始判斷PID的值,在父進(jìn)程中,PID為1234,而在子進(jìn)程中,PID為0。看哪個(gè)進(jìn)程先判斷好PID的值,以上輸出順序才會(huì)被決定。
最后一行輸出為
parent: child 1234 is done
子進(jìn)程在判斷完pid == 0
之后將exit
,父進(jìn)程發(fā)現(xiàn)子進(jìn)程exit
之后,wait
執(zhí)行完畢,打印輸出。
盡管fork
了之后子進(jìn)程和父進(jìn)程有相同的內(nèi)存內(nèi)容,但是內(nèi)存地址和寄存器是不一樣的,也就是說在一個(gè)進(jìn)程中改變變量并不會(huì)影響另一個(gè)進(jìn)程。
fork and exec
exec
:形式:int exec(char *file, char *argv[])
。加載一個(gè)文件,獲取執(zhí)行它的參數(shù),執(zhí)行。如果執(zhí)行錯(cuò)誤返回-1,執(zhí)行成功則不會(huì)返回,而是開始從文件入口位置開始執(zhí)行命令。文件必須是ELF格式。
xv6 shell使用以上四個(gè)system call來為用戶執(zhí)行程序。在shell進(jìn)程的main
中主循環(huán)先通過getcmd
來從用戶獲取命令,然后調(diào)用fork
來運(yùn)行一個(gè)和當(dāng)前shell進(jìn)程完全相同的子進(jìn)程。父進(jìn)程調(diào)用wait
等待子進(jìn)程exec
執(zhí)行完(在runcmd
中調(diào)用exec
)
/* sh.c */
int
main(void)
{
static char buf[100];
int fd;
// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
// Read and run input commands.
while(getcmd(buf, sizeof(buf)) >= 0){
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
// Chdir must be called by the parent, not the child.
buf[strlen(buf)-1] = 0; // chop \n
if(chdir(buf+3) < 0)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
// parent wait the child exit
wait(0);
}
exit(0);
}
你可能會(huì)想,既然fork
和 exec
總是一起使用,為什么不合并成一個(gè)呢?實(shí)際上我們可以在fork
之后,對子進(jìn)程進(jìn)行一些設(shè)置,比如輸入/輸出重定向,然后再執(zhí)行exec
,注意exec
并不會(huì)改變子進(jìn)程的file table。
當(dāng)我們不需要進(jìn)行額外的設(shè)置時(shí),fork 復(fù)制內(nèi)存,exec替換內(nèi)存,這意味著內(nèi)存的浪費(fèi),有什么辦法可以優(yōu)化這種情況么?答案是肯定的,之后的4.6節(jié)我們會(huì)講到 COW (copy-on-write)機(jī)制。
I/O 和文件描述符
-
file descriptor:文件描述符,用來表示一個(gè)被內(nèi)核管理的、可以被進(jìn)程讀/寫的對象的一個(gè)整數(shù),表現(xiàn)形式類似于字節(jié)流,通過打開文件、目錄、設(shè)備等方式獲得。一個(gè)文件被打開得越早,文件描述符就越小。
每個(gè)進(jìn)程都擁有自己獨(dú)立的文件描述符列表,其中0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯(cuò)誤。shell將保證總是有3個(gè)文件描述符是可用的
while((fd = open("console", O_RDWR)) >= 0) { if(fd >= 3) { close(fd); break; } }
-
read
和write
:形式int write(int fd, char *buf, int n)
和int read(int fd, char *bf, int n)
。從/向文件描述符fd
讀/寫n字節(jié)bf
的內(nèi)容,返回值是成功讀取/寫入的字節(jié)數(shù)。每個(gè)文件描述符有一個(gè)offset,read
會(huì)從這個(gè)offset開始讀取內(nèi)容,讀完n個(gè)字節(jié)之后將這個(gè)offset后移n個(gè)字節(jié),下一個(gè)read
將從新的offset開始讀取字節(jié)。write
也有類似的offset/* essence of cat program */ char buf[512]; int n; for (;;) { n = read(0, buf, sizeof buf); if (n == 0) break; if (n < 0) { fprintf(2, "read error\n"); exit(1); } if (write(1, buf, n) != n) { fprintf(2, "write error\n"); exit(1); } }
-
close
。形式是int close(int fd)
,將打開的文件fd
釋放,使該文件描述符可以被后面的open
、pipe
等其他system call使用。使用
close
來修改file descriptor table能夠?qū)崿F(xiàn)I/O重定向/* implementation of I/O redirection, * more specifically, cat < input.txt */ char *argv[2]; argv[0] = "cat"; argv[1] = 0; if (fork() == 0) { // in the child process close(0); // this step is to release the stdin file descriptor open("input.txt", O_RDONLY); // the newly allocated fd for input.txt is 0, since the previous fd 0 is released exec("cat", argv); // execute the cat program, by default takes in the fd 0 as input, which is input.txt }
父進(jìn)程的
fd table
將不會(huì)被子進(jìn)程fd table
的變化影響,但是文件中的offset
將被共享。 -
dup
。形式是int dup(int fd)
,復(fù)制一個(gè)新的fd
指向的I/O對象,返回這個(gè)新fd值,兩個(gè)I/O對象(文件)的offset
相同e.g.
fd = dup(1); write(1, "hello ", 6); write(fd, "world\n", 6); // outputs hello world
除了
dup
和fork
之外,其他方式不能使兩個(gè)I/O對象的offset相同,比如同時(shí)open
相同的文件
Pipes
pipe:管道,暴露給進(jìn)程的一對文件描述符,一個(gè)文件描述符用來讀,另一個(gè)文件描述符用來寫,將數(shù)據(jù)從管道的一端寫入,將使其能夠被從管道的另一端讀出。
我們之前有提到過pipe
是一個(gè)system call,形式為int pipe(int p[])
,p[0]
為讀取的文件描述符,p[1]
為寫入的文件描述符。
xv6中有這樣一個(gè)例子,通過寫管道將參數(shù)傳遞給wc
程序
/* run the program wc with stdin connected to the read end of pipe, parent process able to communicate with child process */
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p); // read fd put into p[0], write fd put into p[1]
if (fork() == 0) {
close(0);
dup(p[0]); // make the fd 0 refer to the read end of pipe
close(p[0]); // original read end of pipe is closed
close(p[1]); // fd p[1] is closed in child process, but not closed in the parent process. 注意這里關(guān)閉p[1]非常重要,因?yàn)槿绻魂P(guān)閉p[1],管道的讀取端會(huì)一直等待讀取,wc就永遠(yuǎn)也無法等到EOF
exec("/bin/wc", argv); // by default wc will take fd 0 as the input, which is the read end of pipe in this case
} else {
close(p[0]); // close the read end of pipe in parent process will not affect child process
write(p[1], "hello world\n", 12);
close(p[1]); // write end of pipe closed, the pipe shuts down
}
在xv6的shell實(shí)現(xiàn)中即sh.c
也是類似的實(shí)現(xiàn),關(guān)于pipe系統(tǒng)調(diào)用的源碼閱讀,我也總結(jié)了一份代碼講解。
case PIPE:
pcmd = (struct pipecmd*)cmd;
if(pipe(p) < 0)
panic("pipe");
if(fork1() == 0){
// in child process
close(1); // close stdout
dup(p[1]); // make the fd 1 as the write end of pipe
close(p[0]);
close(p[1]);
runcmd(pcmd->left); // run command in the left side of pipe |, output redirected to the write end of pipe
}
if(fork1() == 0){
// in child process
close(0); // close stdin
dup(p[0]); // make the fd 0 as the read end of pipe
close(p[0]);
close(p[1]);
runcmd(pcmd->right); // run command in the right side of pipe |, input redirected to the read end of pipe
}
close(p[0]);
close(p[1]);
wait(0); // wait for child process to finish
wait(0); // wait for child process to finish
break;
文件系統(tǒng)
xv6文件系統(tǒng)包含了文件(byte arrays)和目錄(對其他文件和目錄的引用)。目錄生成了一個(gè)樹,樹從根目錄/
開始。對于不以/
開頭的路徑,認(rèn)為是是相對路徑文章來源:http://www.zghlxwxcb.cn/news/detail-576489.html
-
mknod
:創(chuàng)建設(shè)備文件,一個(gè)設(shè)備文件有一個(gè)major device #和一個(gè)minor device #用來唯一確定這個(gè)設(shè)備。當(dāng)一個(gè)進(jìn)程打開了這個(gè)設(shè)備文件時(shí),內(nèi)核會(huì)將read
和write
的system call重新定向到設(shè)備上。 - 一個(gè)文件的名稱和文件本身是不一樣的,文件本身,也叫inode,可以有多個(gè)名字,也叫link,每個(gè)link包括了一個(gè)文件名和一個(gè)對inode的引用。一個(gè)inode存儲(chǔ)了文件的元數(shù)據(jù),包括該文件的類型(file, directory or device)、大小、文件在硬盤中的存儲(chǔ)位置以及指向這個(gè)inode的link的個(gè)數(shù)
-
fstat
。一個(gè)system call,形式為int fstat(int fd, struct stat *st)
,將inode中的相關(guān)信息存儲(chǔ)到st
中。 -
link
。一個(gè)system call,將創(chuàng)建一個(gè)指向同一個(gè)inode的文件名。unlink
則是將一個(gè)文件名從文件系統(tǒng)中移除,只有當(dāng)指向這個(gè)inode的文件名的數(shù)量為0時(shí)這個(gè)inode以及其存儲(chǔ)的文件內(nèi)容才會(huì)被從硬盤上移除
注意:Unix提供了許多在用戶層面的程序來執(zhí)行文件系統(tǒng)相關(guān)的操作,比如mkdir
、ln
、rm
等,而不是將其放在shell或kernel內(nèi),這樣可以使用戶比較方便地在這些程序上進(jìn)行擴(kuò)展。但是cd
是一個(gè)例外,它是在shell程序內(nèi)構(gòu)建的,因?yàn)樗仨氁淖冞@個(gè)calling shell本身指向的路徑位置,如果是一個(gè)和shell平行的程序,那么它必須要調(diào)用一個(gè)子進(jìn)程,在子進(jìn)程里起一個(gè)新的shell,再進(jìn)行cd
,這是不符合常理的。文章來源地址http://www.zghlxwxcb.cn/news/detail-576489.html
到了這里,關(guān)于MIT6.S081學(xué)習(xí)筆記--lec 1的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!