1. 程式人生 > WINDOWS開發 >《作業系統導論》第5章 | 程序API

《作業系統導論》第5章 | 程序API

本章主要討論UNIX系統中的程序建立。UNIX系統採用了一種非常有趣的建立新程序的方式,即通過一對系統呼叫:fork()exec()。程序還可以通過第三個系統呼叫wait(),來等待其建立的子程序執行完成。

fork()系統呼叫

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc,char *argv[]) {
  printf("hello world (pid:%d)\n",(int)getpid());
  int rc = fork();
  if (rc < 0) {  // fork failed; exit
    fprintf(stderr,"fork failed\n");
    exit(1);
  } else if (rc == 0) {  // child (new process)
    printf("hello,I am child (pid:%d)\n",(int)getpid());
  } else {  // parent goes down this path (main)
    printf("hello,I am parent of %d (pid:%d)\n",rc,(int)getpid());
  }
  return 0;
}

執行這段程式,得到如下輸出:

hello world (pid:62775)
hello,I am parent of 62779 (pid:62775)
hello,I am child (pid:62779)

當它剛開始執行時,程序輸出一條hello world資訊,以及自己的程序描述符(process identifier,PID)。該程序的PID是62775。在UNIX系統中,如果要操作某個程序(如終止程序),就要通過PID來指明。緊接著程序呼叫了fork()系統呼叫,這是作業系統提供的建立新程序的方法。新建立的程序幾乎與呼叫程序完全一樣,對作業系統來說,這時看起來有兩個完全一樣的p1程式在執行,並都從fork()系統呼叫中返回。新建立的程序稱為子程序(child),原來的程序稱為父程序(parent)。子程序不會從main()函式開始執行(因此hello world資訊只輸出了一次),而是直接從fork()系統呼叫返回,就好像是它自己呼叫了fork()。

子程序並不是完全拷貝了父程序。雖然它擁有自己的地址空間(即擁有自己的私有記憶體)、暫存器、程式計數器等,但是它從fork()返回的值是不同的。父程序獲得的返回值是新建立子程序的PID,而子程序獲得的返回值是0。這個差別非常重要,因為這樣就很容易編寫程式碼處理兩種不同的情況。這段程式的輸出不是確定的,因為CPU的排程程式決定了某個時刻哪個程序被執行,而排程程式又比較複雜,所以我們不能假設哪個程序先執行。

wait()系統呼叫

有時候,父程序需要等待子程序執行完畢。這項任務由wait()系統呼叫(或者更完整的介面waitpid())來完成:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,(int)getpid());
  } else {  // parent goes down this path (main)
    int wc = wait(NULL);
    printf("hello,I am parent of %d (wc:%d) (pid:%d)\n",wc,(int)getpid());
  }
  return 0;
}

上面的程式碼增加了wait()呼叫,因此輸出結果也變得確定了。因為子程序可能先執行,先於父程序輸出結果。但是,如果父程序碰巧先執行,它會馬上呼叫wait()。該系統呼叫會在子程序執行結束後才返回。因此,即使父程序先執行,它也會等待子程序執行完畢,然後wait()返回,接著父程序才輸出自己的資訊。

hello world (pid:10532)
hello,I am child (pid:10533)
hello,I am parent of 10533 (wc:10533) (pid:10532)

exec()系統呼叫

exec()系統呼叫可以讓子程序執行與父程序不同的程式。如下所示,子程序呼叫execvp()來執行字元計數程式wc。實際上,它針對原始碼檔案p3.c執行wc,從而告訴我們該檔案有多少行、多少單詞,以及多少位元組。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc,(int)getpid());
    char *myargs[3];
    myargs[0] = strdup("wc");    // program: "wc" (word count)
    myargs[1] = strdup("p3.c");  // argument: file to count
    myargs[2] = NULL;            // marks end of array
    execvp(myargs[0],myargs);   // runs word count
    printf("this shouldn‘t print out");
  } else {  // parent goes down this path (main)
    int wc = wait(NULL);
    printf("hello,(int)getpid());
  }
  return 0;
}

上述程式碼的輸出結果:

hello world (pid:25414)
hello,I am child (pid:25415)
 27 119 880 p3.c
hello,I am parent of 25415 (wc:25415) (pid:25414)

給定可執行程式的名稱(如wc)及需要的引數(如p3.c)後,exec()會從可執行程式中載入程式碼和靜態資料,並用它覆寫自己的程式碼段(以及靜態資料),堆、棧及其他記憶體空間也會被重新初始化。然後作業系統就執行該程式,將引數通過argv傳遞給該程序。因此,它並沒有建立新程序,而是直接將當前執行的程式(以前的p3)替換為不同的執行程式(wc)。子程序執行exec()之後,幾乎就像p3.c從未執行過一樣。對exec()的成功呼叫永遠不會返回。

為什麼這樣設計程序API

分離fork()exec()的做法在構建UNIX shell時非常有用。shell是一個使用者程式,它首先顯示一個提示符,然後等待使用者輸入。你可以向它輸入一個命令(一個可執行程式的名稱及需要的引數),大多數情況下,shell可以在檔案系統中找到這個可執行程式,呼叫fork()建立新程序,並呼叫exec()的某個變體來執行這個可執行程式,呼叫wait()等待該命令完成。子程序執行結束後,shell從wait()返回並再次輸出一個提示符,等待使用者輸入下一條命令。

$ wc p3.c > newfile.txt

在上面的例子中,wc的輸出結果被重定向到檔案newfile.txt中。shell實現結果重定向的方式也很簡單,當完成子程序的建立後,shell在呼叫exec()之前先關閉了標準輸出,打開了檔案newfile.txt。這樣,即將執行的程式wc的輸出結果就被髮送到該檔案,而不是列印在螢幕上。

下面的程式展示了重定向的工作原理。具體來說,UNIX系統從0開始尋找可以使用的檔案描述符。在這個例子中,STDOUT_FILENO將成為第一個可用的檔案描述符,因此在open()被呼叫時,得到賦值。然後子程序向標準輸出檔案描述符的寫入,都會被透明地轉向新開啟的檔案而非螢幕。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main(int argc,char *argv[]) {
  int rc = fork();
  if (rc < 0) {  // fork failed; exit
    fprintf(stderr,"fork failed\n");
    exit(1);
  } else if (rc == 0) {  // child: redirect standard output to a file

    close(STDOUT_FILENO);
    open("./p4.output",O_CREAT | O_WRONLY | O_TRUNC,S_IRWXU);

    char *myargs[3];             // now exec "wc"...
    myargs[0] = strdup("wc");    // program: "wc" (word count)
    myargs[1] = strdup("p4.c");  // argument: file to count
    myargs[2] = NULL;            // marks end of array
    execvp(myargs[0],myargs);   // runs word count
  } else {  // parent goes down this path (original process)

    int wc = wait(NULL);
    assert(wc >= 0);
  }
  return 0;
}

上述程式的執行結果:

$ ./p4
$ cat p4.output
 31 120 877 p4.c

首先,當執行p4程式後,好像什麼也沒有發生。shell只是列印了命令提示符,等待使用者的下一個命令。但事實並非如此,p4確實呼叫了fork()來建立新的子程序,之後呼叫execvp()來執行wc。螢幕上沒有看到輸出,是由於結果被重定向到檔案p4.output。其次,當用cat命令列印輸出檔案時,能看到執行wc的所有預期輸出。

UNIX管道也是用類似的方式實現的,但用的是pipe()系統呼叫。在這種情況下,一個程序的輸出被連結到了一個核心管道上(佇列),另一個程序的輸入也被連線到了同一個管道上。因此,前一個程序的輸出無縫地作為後一個程序的輸入,許多命令可以用這種方式串聯在一起,共同完成某項任務。比如通過將grep、wc命令用管道連線可以完成從一個檔案中查詢某個詞,並統計其出現次數的功能:grep -o foo file | wc -l