iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0

前言

在今天我們將運用前面所學的關於檔案操作的 System call,以及 Process 相關的 System call,來實現 I/O 重導向的概念,並藉由 I/O 重導向的實現,來了解到系統分層的概念。最後將看到關於 pipe 的相關使用與實現。

I/O 重導向

我們之前常常使用 printf(), scanf(), putchar(), getchar(), puts() 等函數都是通過 stdin 預設的鍵盤進行輸入,並使用 stdout 預設的螢幕進行輸出,然而,在 UNIX 或 DOS 等作業系統中,我們可以通過重導向來改變輸入或是輸出來源。

echo hello > out

output:

這種示範就是輸出重導向 (input redirection),本質上是使 stdout 流表示為檔案,而非 Console (以檔案做為 output 即是使用這個概念)。所以我們沒有在 Console 中看到任何輸出,輸出位於檔案 out 中。

同理,我們可以將剛剛產生的 out 檔案作為輸入,使用 cat() 查看內容

cat < out

output:

hello

而 Shell 之所以可以這樣做,原因為 Shell 會進行 fork() 出一個子 Process,之後在子 Process 中,Shell 改變了檔案描述子,將檔案描述子由1變成了 out 所對應到的檔案描述子,接著執行我們的指令,由於子 Process 和親代 Process 擁有各自獨立的記憶體空間,因此子 Process 改變檔案描述子不會影響到親代 Process (親代 Process 的檔案描述子還是1 (stdout 標準輸出)。這是在 UNIX 中常見的重導向操作,我們只想改變子 Process 的輸出,而非親代 Process。

xv6 的 I/O 重導向實作

在Day-03,我們使用了fork()exec()wait(), close() 這一些有關於 Process 的 System,現在,我們可以結合這一些工具實現 I/O 重導向的操作。

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h"

int main(void)
{
    int pid = 0;
    pid = fork();
    if(pid == 0)
    {
        close(1);
        open("output.txt", O_WRONLY | O_CREATE);

        char *argv[] = {"echo", "this", "is", "redirected", "echo", 0};
        exec("echo", argv);
        printf("exec failed!\n");
        exit(1);
    }
    else
    {
        wait((int *)0);
    }
    exit(0);
    return 0;
}

在第9行的地方,我們執行了 fork() System call,接著會進入到子 Process 中。

在第12行的地方,我們執行了 close() System call,並將檔案描述子1傳入,表示關閉檔案描述子1所關聯的檔案,在 Shell 的預設情況檔案描述子1是關連到標準輸出上,而我們將其關閉,也就是解除檔案描述子1和標準輸出的關聯,使得我們能將輸出輸出到其他檔案中。

在第13行的地方,建立並且以讀寫模式開啟了 output.txt,而 open 會回傳檔案描述子,前面說到在預設情況下,UNIX 會將2以上的數字作為開啟檔案之檔案描述子,但是在這邊由於我們解除了檔案描述子1與標準輸出的關聯,因此 open() 會回傳1,使得在第13行執行完畢後,檔案描述子1和 output.txt 關聯在了一起。

接著執行16行,子 Process 變成了 echo,以第15行的 argv 作為參數執行,而 echo 會將輸出輸出到檔案描述子1關連到了檔案中,這裡為 output.txt,接著親代 Process 等待子 Process echo 執行 exit(),接著會傳到親代 Process 的 wait(),而這裡我們傳入 wait() 的引數為0,型別為 int *,表示我們不在意子 Process 的狀態。

echo 實際上並不知道自己是輸出到 Console 還是檔案上,只是知道輸出到了檔案描述子1關連到的檔案上

分離 fork()exec()

這裡我們可以看到將 fork()exec() 分開所帶來的好處,親代 Process 可以重新導向子 Process 的 I/O 而不會影響到親代 Process 的 I/O,而在親代 Process 執行了 fork() 後產生出子 Process 並且得到回傳,到子 Process 執行到 exec() 這一段時間,子 Process 正在執行的還是親代 Process 的程式碼,因此在程式碼第10行到16行這一段區間,我們還可以使用親代 Process 去對子 Process 作出一些影響。

如果把 fork()exec() 寫在一起,Shell 就需要在 forkexec() 執行之前修改 I/O 設置,且在 forkexec() 成功執行後取消修改。

pipe()

pipe() 為在 kernel 中一塊的緩衝區,而 pipe() 需要接收一個長度為2的整數陣列,內容為連接到輸入的檔案描述子以及連接到輸出的檔案描述子,將資料寫入到 pipe() 的一端,而另外的 Process 就可以從 pipe() 的另外一端讀取,實現 Process 之間的通訊。

// user/wc.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(void)
{
    int p[2];
    char *argv[2] = {"wc", 0};
    pipe(p);

    if(fork() == 0)
    {
        close(0);
        dup(p[0]);
        close(p[0]);
        close(p[1]);
        exec("/bin/wc", argv);
    }
    else
    {
        write(p[1], "hello world\n", 12);
        close(p[0]);
        close(p[1]);
    }
    return 0;
}

說明:
在最一開始的狀態,檔案描述子0是連接到標準輸入上,p[0] 為 pipe 的讀取端,p[1] 為 pipe 的寫入端

接著在第11行執行fork() System call,產生出子 Process,子 Process 的 PID 為0,進入到第13行。(fork() 後親代 Process 和子 Process 都有 pipe 的檔案描述子)

第13行解除檔案描述子0關連到的檔案,也就是解除檔案描述子0和標準輸入的關聯

第14行 dup() 功用為傳入檔案描述子 A,會回傳檔案描述子 B,且 A 和 B 皆會關連到檔案描述子 A 所關聯的檔案。傳入 p[0],會回傳一個檔案描述子關連到 p[0] 所關連到的檔案,也就是 pipe 的讀取端,由於我們解除了檔案描述子0和標準輸入的關聯,因此回傳的檔案描述子為0,0關連到 pipe 的讀取端。

第15行和16行解除 p[0]p[1] 所關聯的檔案。

這樣我們的程式就可以成功從 pipe 的一端讀入資料了。

接著第17行執行 exec(),變成 wc(),當 wc() 從檔案描述子0關連到的檔案讀取輸入時,實際上是從 pipe() 的讀取端獲得輸入。

接著親代Process會向pipe的寫入端寫入,並且解除 p[0]p[1] 所關聯的檔案。

而之所以在 exec() 之前要執行 close(),原因為如果在執行 read() 的情況下,read() 會從 pipe 讀取端等待讀入端讀入資料,直到有資料寫入或是指向寫入端的檔案描述子全部關閉,在指向寫入端的檔案描述子全部關閉的情況下,read() 會回傳0,就像是讀取到檔案的 EOF 一樣,如果沒有先執行 close(),可能會發生 read() 不斷等待的情況。

而我們可以看看在 /user/sh.c/ 中是如何進行 pipe 的。

case PIPE:
    pcmd = (struct pipecmd*)cmd;
    if(pipe(p) < 0)
      panic("pipe");
    if(fork1() == 0){
      close(1);
      dup(p[1]);
      close(p[0]);
      close(p[1]);
      runcmd(pcmd->left);
    }
    if(fork1() == 0){
      close(0);
      dup(p[0]);
      close(p[0]);
      close(p[1]);
      runcmd(pcmd->right);
    }
    close(p[0]);
    close(p[1]);
    wait(0);
    wait(0);
    break;

首先會在第3行的地方建立 pipe,接著在第5行的地方 fork() 產生出子 Process。

子 Process 會先將檔案描述子1連接到 pipe 的寫入端,接著執行,另外一個子 Process 會將檔案描述子0連接到 pipe 的讀取端,可以看到做法跟我們上面的作法有一些雷同。

在 UNIX 系統中,pipe 被視為一種特殊的檔案型態,因此可以通過 write()read() 等 System call 進行使用,這一點也可以從 xv6 的程式碼中看到相關實作

void
fileclose(struct file *f)
{
  struct file ff;

  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("fileclose");
  if(--f->ref > 0){
    release(&ftable.lock);
    return;
  }
  ff = *f;
  f->ref = 0;
  f->type = FD_NONE;
  release(&ftable.lock);

  if(ff.type == FD_PIPE){
    pipeclose(ff.pipe, ff.writable);
  } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
    begin_op();
    iput(ff.ip);
    end_op();
  }
}

kernel/file.cfileclose()可以看到我們傳入的一個檔案結構,並且會判斷這個檔案是屬於哪一個類型,在這邊可以看到總共判斷了三種檔案類型,分別為FD_PIPE, PD_INODE, FD_DEVICE,pipe 也是屬於一種檔案類型。

pipe 於 xv6 使用

我們常常在 Console 上使用到 pipe,將第一個指令的輸出作為第二個指令的輸入。舉例來說,我們可以使用 ls 檢視目前目錄底下的資料夾與檔案,對於一個擁有較多內容的目錄,我們可能需要捲動葉面來查看整個輸出,而使用more這個指令,我們便可以將大量的輸出變成好幾個頁,通過空白鍵去切換頁面來檢視輸出。lsmore都是獨立的一個 process,Shell 會通過fork(),接著exec()去執行。

而使用 pipe 的方式,我們可以讓ls()這個 process 的輸出,作為more()的輸入

ls | more

兩個Process溝通 (PingPong)

(來自於 6S081 的 Lab) 要求 :

Write a program that uses UNIX system calls to ``ping-pong’’ a byte between two processes over a pair of pipes, one for each direction. The parent sends by writing a byte to parent_fd[1] and the child receives it by reading from parent_fd[0]. After receiving a byte from parent, the child responds with its own byte by writing to child_fd[1], which the parent then reads. Your solution should be in the file user/pingpong.c

寫一隻名稱為"pingpong"的程式,pingpong 需要實現兩個 Process 之間通過一對 pipe 進行溝通

  • 親代 Process 對 parnet_fd[1] 進行寫入,並且子 Process 在 parent_fd[0] 進行讀取,並印出讀取的結果。
  • 子代 Process 結束讀取後,對 child_fd[1] 進行寫入,並在親代 Process 於 child_fd[0] 進行讀取,並印出讀取得結果。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(void)
{
    int parent_fd[2];
    int child_fd[2];
    char buffer[100];

    pipe(parent_fd);
    pipe(child_fd);
    int pid = fork();
    if(pid == 0)//child
    {
        read(parent_fd[0], buffer, 4);
        printf("%d: received ping\n", getpid());
        close(parent_fd[0]);
        write(child_fd[1], "pong", 4);
        close(child_fd[1]);
    }
    else//parnet
    {
        write(parent_fd[1], "ping", 4);
        close(parent_fd[1]);
        read(child_fd[0], buffer, 4);
        close(child_fd[0]);
        printf("%d: received pong\n", getpid());
    }
    exit(0);
}

reference

xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book
build a OS
圖片使用 draw.io 進行繪製


上一篇
Day-04 C 語言的檔案操作, xv6 File System call
下一篇
Day-06 RISC-V 簡介, Microkernel vs Monolithic kernel
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言