第1.1節 進程和內存

1.1節 進程和內存

這一節涉及的系統調用有:

  • fork
  • exit
  • wait

一個xv6進程由用戶空間內存(指令、數據、棧)和內核專有的進程狀態組成。

xv6分時共享進程,即xv6透明地在一組等待執行的進程間切換可用的CPU。

當一個進程不執行時,xv6保存該進程的CPU寄存器,當下次運行該進程時,恢復先前保存的CPU寄存器值。

內核給每個進程關聯一個進程標識符或者PID。

理解系統調用fork

一個進程可使用fork系統調用來創建新的進程。

fork創建一個新的進程,即子進程:子進程跟父進程有着同樣的內存內容。

fork在父進程和子進程中都會返回。

  • 對父進程來說,fork的返回值是子進程的PID;
  • 對子進程來說,fork的返回值是0;

示例代碼1

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

parent: child 1234 is done

系統調用exit

  • 會導致調用進程停止執行,釋放諸如內存和打開文件等資源。
  • 參數是整數類型的狀態參數,約定0表示成功,1表示失敗;

系統調用wait

  • 返回值是當前進程的已退出的子進程的PID,並拷貝子進程的退出狀態給傳遞給wait的地址;
  • 如果調用者的子進程沒有一個退出,則wait就等待着其中一個退出;
  • 如果調用者沒有子進程,則wait立即返回-1;
  • 如果父進程不關心子進程的退出狀態,則父進程可傳遞一個0地址給wait

注意:

  • 雖然最初子進程和父進程有着相同的內存內容,但是父進程和子進程是使用的是不同的內存和寄存器,即修改一個進程中的變量不會影響另一個進程中的變量的值。比如,在父進程中,wait函數的返回值保存在變量pid中,這並沒有修改子進程中的pid的值,子進程中的pid的值仍是0。

xv6系統使用的ELF格式。

系統調用exec

  • 有兩個參數,一個是可執行文件的名字,一個是字符串數組參數;
  • 使用從存儲在文件系統裏的文件中加載的新的內存映像來替換調用進程的內存。
  • 前述的文件必須有一個特定的格式,描述了文件的哪個部分持有指令,哪個部分持有數據,從哪個指令開始執行等。
  • 當exec執行成功後,它並不返回給調用程序,而開始執行在ELF頭裏聲明的的入口處的指令。

代碼示例2

char *argv[3];

argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");

這段代碼使用程序/bin/echo的運行實例來替換調用程序,參數列表是echo hello

大部分程序都忽略參數數組的第一個元素,因爲約定第一個元素是程序的名字。

xv6的shell程序使用上面的系統調用來代表用戶運行程序。

shell程序的主體結構很簡單:

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));
    wait(0);
  }
  exit(0);
}

解釋:
主循環
首先,父進程使用getcmd從用戶讀取一行輸入;
然後,父進程調用fork創建了shell進程的拷貝,即子進程;
最後,父進程調用wait,子進程運行命令。

比如,用戶在shell上鍵入了"echo hello",則runcmd被調用使的參數是"echo hello"。

runcmd會運行實際的命令。對於"echo hello",runcmd會調用exec

如果exec成功了,則子進程將執行來自echo的指令而不是runcmd

在某個點處,echo會調用exit,會導致父進程從wait中返回。

爲什麼不將forkexec整個成一個調用?
shell使用forkexec的這種隔離來實現I/O重定向。

爲了避免創建一個拷貝進程帶來的浪費,然後立即用exec來替換子進程,內核通過使用諸如copy-on-write等的虛擬內存技術來優化fork的實現。

xv6都是隱式地分配大部分用戶空間內存:

  • fork分配了父進程內存的子進程拷貝所需的內存;
  • exec分配了足夠的內存來持有可執行文件;

如果一個進程在運行時需要更多的內存,則該進程可調用sbrk(n)來使其數據內存增長n個字節。

注意:
sbrk的返回值是新的內存的地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章