# 引言 ## 操作系統的目標 + abstract H/W `抽象化硬體` + multiplex `多路復用` + isolation `隔離性` + sharing `共用(進程通信,數據共用)` + security / access control `安全性/許可權控制` + perform ...
引言
操作系統的目標
- abstract H/W
抽象化硬體
- multiplex
多路復用
- isolation
隔離性
- sharing
共用(進程通信,數據共用)
- security / access control
安全性/許可權控制
- performance
性能/內核開銷
- range of applications
多應用場景
操作系統概覽
操作系統應該提供的功能:1. 多進程支持 2. 進程間隔離 3. 受控制的進程間通信
-
xv6:一種在本課程中使用的類UNIX的教學操作系統,運行在RISC-V指令集處理器上,本課程中將使用QEMU模擬器代替
-
kernel(內核):為運行的程式提供服務的一種特殊程式。每個運行著的程式叫做進程,每個進程的記憶體中存儲指令、數據和堆棧。一個電腦可以擁有多個進程,但是只能有一個內核
每當進程需要調用內核時,它會觸發一個system call(系統調用),system call進入內核執行相應的服務然後返回。
操作系統的組織結構如圖1所示
內核提供的一系列系統調用就是用戶程式可見的操作系統介面,xv6 內核提供了 Unix 傳統系統調用的一部分,它們是:
進程和記憶體
每個進程擁有自己的用戶空間記憶體以及內核空間狀態,當進程不再執行時xv6將存儲和這些進程相關的CPU寄存器直到下一次運行這些進程。kernel將每一個進程用一個PID(process identifier)指代。在進程執行中,常常會使用fork
和exec
系統調用來創建新的進程。如下麵代碼所示:
fork and wait
fork
:形式:int fork()
。其作用是讓一個進程生成另外一個和這個進程的記憶體內容相同的子進程。在父進程中,fork
的返回值是這個子進程的PID,在子進程中,返回值是0exit
:形式:int exit(int status)
。讓調用它的進程停止執行並且將記憶體等占用的資源全部釋放。需要一個整數形式的狀態參數,0代表以正常狀態退出,1代表以非正常狀態退出wait
:形式:int wait(int *status)
。等待子進程退出,返回子進程PID,子進程的退出狀態存儲到int *status
這個地址中。如果調用者沒有子進程,wait
將返回-1pipe
:形式:int pipe(int p[])
。創建一個管道,將讀/寫文件描述符放在p[0]和p[1]中sbrk
:形式:char *sbrk(int n)
。將進程的記憶體增加n位元組。返回新記憶體的起始位置。
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
這是因為在fork了之後,父進程和子進程將同時開始判斷PID的值,在父進程中,PID為1234,而在子進程中,PID為0。看哪個進程先判斷好PID的值,以上輸出順序才會被決定。
最後一行輸出為
parent: child 1234 is done
子進程在判斷完pid == 0
之後將exit
,父進程發現子進程exit
之後,wait
執行完畢,列印輸出。
儘管fork
了之後子進程和父進程有相同的記憶體內容,但是記憶體地址和寄存器是不一樣的,也就是說在一個進程中改變變數並不會影響另一個進程。
fork and exec
exec
:形式:int exec(char *file, char *argv[])
。載入一個文件,獲取執行它的參數,執行。如果執行錯誤返回-1,執行成功則不會返回,而是開始從文件入口位置開始執行命令。文件必須是ELF格式。
xv6 shell使用以上四個system call來為用戶執行程式。在shell進程的main
中主迴圈先通過getcmd
來從用戶獲取命令,然後調用fork
來運行一個和當前shell進程完全相同的子進程。父進程調用wait
等待子進程exec
執行完(在runcmd
中調用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);
}
你可能會想,既然fork
和 exec
總是一起使用,為什麼不合併成一個呢?實際上我們可以在fork
之後,對子進程進行一些設置,比如輸入/輸出重定向,然後再執行exec
,註意exec
並不會改變子進程的file table。
當我們不需要進行額外的設置時,fork 複製記憶體,exec替換記憶體,這意味著記憶體的浪費,有什麼辦法可以優化這種情況麽?答案是肯定的,之後的4.6節我們會講到 COW (copy-on-write)機制。
I/O 和文件描述符
-
file descriptor:文件描述符,用來表示一個被內核管理的、可以被進程讀/寫的對象的一個整數,表現形式類似於位元組流,通過打開文件、目錄、設備等方式獲得。一個文件被打開得越早,文件描述符就越小。
每個進程都擁有自己獨立的文件描述符列表,其中0是標準輸入,1是標準輸出,2是標準錯誤。shell將保證總是有3個文件描述符是可用的
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位元組bf
的內容,返回值是成功讀取/寫入的位元組數。每個文件描述符有一個offset,read
會從這個offset開始讀取內容,讀完n個位元組之後將這個offset後移n個位元組,下一個read
將從新的offset開始讀取位元組。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能夠實現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 }
父進程的
fd table
將不會被子進程fd table
的變化影響,但是文件中的offset
將被共用。 -
dup
。形式是int dup(int fd)
,複製一個新的fd
指向的I/O對象,返回這個新fd值,兩個I/O對象(文件)的offset
相同e.g.
fd = dup(1); write(1, "hello ", 6); write(fd, "world\n", 6); // outputs hello world
除了
dup
和fork
之外,其他方式不能使兩個I/O對象的offset相同,比如同時open
相同的文件
Pipes
pipe:管道,暴露給進程的一對文件描述符,一個文件描述符用來讀,另一個文件描述符用來寫,將數據從管道的一端寫入,將使其能夠被從管道的另一端讀出。
我們之前有提到過pipe
是一個system call,形式為int pipe(int p[])
,p[0]
為讀取的文件描述符,p[1]
為寫入的文件描述符。
xv6中有這樣一個例子,通過寫管道將參數傳遞給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. 註意這裡關閉p[1]非常重要,因為如果不關閉p[1],管道的讀取端會一直等待讀取,wc就永遠也無法等到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.c
也是類似的實現,關於pipe系統調用的源碼閱讀,我也總結了一份代碼講解。
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;
文件系統
xv6文件系統包含了文件(byte arrays)和目錄(對其他文件和目錄的引用)。目錄生成了一個樹,樹從根目錄/
開始。對於不以/
開頭的路徑,認為是是相對路徑
mknod
:創建設備文件,一個設備文件有一個major device #和一個minor device #用來唯一確定這個設備。當一個進程打開了這個設備文件時,內核會將read
和write
的system call重新定向到設備上。- 一個文件的名稱和文件本身是不一樣的,文件本身,也叫inode,可以有多個名字,也叫link,每個link包括了一個文件名和一個對inode的引用。一個inode存儲了文件的元數據,包括該文件的類型(file, directory or device)、大小、文件在硬碟中的存儲位置以及指向這個inode的link的個數
fstat
。一個system call,形式為int fstat(int fd, struct stat *st)
,將inode中的相關信息存儲到st
中。link
。一個system call,將創建一個指向同一個inode的文件名。unlink
則是將一個文件名從文件系統中移除,只有當指向這個inode的文件名的數量為0時這個inode以及其存儲的文件內容才會被從硬碟上移除
註意:Unix提供了許多在用戶層面的程式來執行文件系統相關的操作,比如mkdir
、ln
、rm
等,而不是將其放在shell或kernel內,這樣可以使用戶比較方便地在這些程式上進行擴展。但是cd
是一個例外,它是在shell程式內構建的,因為它必須要改變這個calling shell本身指向的路徑位置,如果是一個和shell平行的程式,那麼它必須要調用一個子進程,在子進程里起一個新的shell,再進行cd
,這是不符合常理的。