--- title: Input / Output (輸入輸出) - 從零開始的開源地下城 tags: Linux, Linux讀書會, Kernel, 從零開始的開源地下城, COMBO-tw description: 介紹與Linux Kernel相關基本知識 lang: zh-Hant GA: G-2QY5YFX2BV --- # Input / Output (輸入輸出) ###### tags: `Linux` ## 目錄 [TOC] ## Linux Kernel Input / Output ### Stdin/Stdout/Stderr 其實 C 函式庫中已經宣告好 3 個 `FILE *` 的指標,分別是 `stdin`、`stdout`、`stderr`。 * stdin : standard input 標準輸入串流 * stdout : standard output 標準輸出串流 * stderr : standard error 輸出串流且系統會留下紀錄檔 我們常用的函式 `printf`、`scanf` ,實際上是包裝過後的結果: * printf("...") 事實上是呼叫 fprintf(stdout, "....") * scanf("...") 事實上是呼叫 fsanf(stdin, "..." ) * fprintf(stderr, "....") 所印在螢幕上的東西不會被輸出轉向所影響 > 一般情況下,fprintf、printf 執行的結果是相同的。 ### 所有東西在 linux 中都是 file,/dev/* 當你到 `/dev` 中下 `ls` 會發現有許多熟悉的東西,舉凡 Console、SSH、Socket...等,每個你能想到的幾乎都能在這裡被找到。 而在 UNIX 當中有句話 "Everything is a file",描述了 Unix 及其衍生產品的定義特性之一,意思表示廣泛的輸入/輸出資源,如 File、目錄、硬體驅動、Modems、鍵盤、印表機,甚至一些 Process 間和網路通訊都是透過 File System 命名空間公開的簡單資料流。 這種方法的優點是可以在廣泛的資源上使用同一組工具、實用程式和 API。有多種檔案類型。打開檔案時,會創建 file descriptor。該路徑成為尋址系統和 file descriptor 是之間串流 I/O 的介面接口(interface)。但是 file descriptor 也是通過不同的方法為匿名 Pipe 和 Network Socket 等創建的。因此對這個特性更準確的描述是 `Everything is a file descriptor`。 ## Shell ![](https://hackmd.io/_uploads/ryr7Qs-L3.png) ### 何謂 Shell? * Shell 乃是 ***「介於使用者與作業系統之間的介面」*** ,其功能為 ***「讓使用者能夠藉由指令來操作作業系統」***。 * 替我們工作的是「硬體」,控制硬體的「Kernel」,我們則利用「shell」控制 Kernel 提供工具來使硬體正確工作。 * Bash 為 linux 預設的 shell,文字模式之指令下達方式就是 bash 的工具與介面。 * Shell 狹隘的定義是指文字模式的 BASH shell。廣義的 Shell 也可以是 KDE 之類的圖形介面控制軟體。 * 如果沒有特別說明, Shell 指的是比較狹義的文字模式。 ### Shell History ![](https://hackmd.io/_uploads/HyqXQjW8h.png) * Bourne shell * 以發明者 Steven Bourne 命名。 * 第一個重要的 shell,1979 年第一個流行的 Unix 版本 7 發行時,開始使用 Bourne shell。 * Bourne shell 的主檔名為 sh。(許多嵌入式 Linux 環境最基本支援的 Shell) * 雖然 Unix 上的 shell 有許多種,但許多 Unix 系統至今仍然使用 sh 做為重要的管理工具。 * C shell * 作者是柏克萊大學的 Bill Joy。 * 第一個廣為流行使用的 shell。 * 主要附在 BSD 版的 Unix 系統。 * 因其語法和 C 語言類似,因而得名,且使得程式設計師,學習 C shell 時更方便容易。 * Bash shell * Bash 是 GNU 計劃的重要工具軟體之一,也是 GNU 作業系統中標準的 shell。 * Bash 相容於 sh,許多早期開發的 Bourne shell 都可以繼續在 bash 中運作。 * Bash 在 1988 年誕生,最初作者 Brian Fox。 * Bash 是完全免費的,它是 Open Source 的一員,原始碼全部開放。 * 其他的 Shell,例如 tcsh, zch, ksh 及 pdksh...等 ### Shell 原理 所以 Shell 是怎樣跑一個程式的呢? * Shell(`pid 0`)接到指令,比方說 ls 好了,他就會先 fork() 出一個新 process(`pid 1`),然後新的 `pid 1` 使用 exec 指令將 forked 的 Shell 取代成 ls 並執行。此時 Shell(`pid 0`)會用 waitpid() 等 ls(`pid 1`)執行完印出輸出,才繼續執行 Shell。 Shell: ```shell $ # pid 0 ----------------------------------------------------- $ ls # pid 0 fork() 出 pid 1 A B C D # pid 1 執行 ls ----------------------------------------------------- $ # pid 0 用 waitpid() 等 pid 1 結束才繼續 ``` * 接下去,我們需要知道 `pipe()` 這個 System Call,他是一種讓程序之間可以溝通的方式之一,在實作 Shell 時,我們會需要用 `pipe()` 和 `dup2()` 來搞定 A | B。請大家先看「`pipe()` System call」、「C program to demonstrate `fork()` and `pipe()`」和「`dup()` and `dup2()` Linux system call」這三段,再接下去看下面的範例。 ### `pipe()` System call [資料來源](https://www.geeksforgeeks.org/pipe-system-call/) #### Pipeline 觀念介紹 ![](https://hackmd.io/_uploads/B1LEQibLh.png) `pipe` 又稱 `pipeline`,屬於系統調用的一種,用於兩個 Process 之間的溝通,通常是從一個 Process 的 stdout 送到另一個 Process 的 stdin。在 UNIX 系統中,Pipeline 對於 Process 之間的通訊是非常有用的。 * Pipeline 是一個單向傳遞的過程,就像寄 Email 一樣,A 寄給 B 時,會開啟一條單向的通道,此時我們可以視這條通道為 Pipeline。 * 當我們使用 pipeline 時,就像在記憶體中開啟了一個虛擬文件,A 可以寫內容進去,而 B 可以將內容取出。 * 而誰可以使用這個`虛擬文件`呢? 創建該文件的 Process 及其 Subprocess 皆擁有該文件的讀寫權限。 * 如果 Pipeline 內沒內容時,又有 Process 來讀取的話,該 Process 將會等待資料寫入。 * `pipe()` 系統調用將會在 Process 打開的文件列表 (file descriptor table) 中找到前兩個可用的位置,並將它們分配給 Pipeline 讀寫端使用。 * Pipeline 為 FIFO (First In First Out: 先進先出),其實做的資料結構為 Queue (佇列),讀寫的大小可以不必在此先定義,不過我們可以一次寫入 512 Bytes,但我們卻能一次讀取 1 Byte。 #### C 語言語法參考 ``` int pipe(int fds[2]); Parameters : fd[0] will be the fd(file descriptor) for the read end of pipe. fd[1] will be the fd for the write end of pipe. Returns : 0 on Success. -1 on error. ``` #### C語言範例 ```c= // C program to illustrate // pipe system call in C #include <stdio.h> #include <unistd.h> #define MSGSIZE 16 char* msg1 = "hello, world #1"; char* msg2 = "hello, world #2"; char* msg3 = "hello, world #3"; int main() { char inbuf[MSGSIZE]; int p[2], i; if (pipe(p) < 0) exit(1); /* continued */ /* write pipe */ write(p[1], msg1, MSGSIZE); write(p[1], msg2, MSGSIZE); write(p[1], msg3, MSGSIZE); for (i = 0; i < 3; i++) { /* read pipe */ read(p[0], inbuf, MSGSIZE); printf("% s\n", inbuf); } return 0; } ``` #### 父行程與子行程之間共享 pipe ![](https://hackmd.io/_uploads/BJeH7ibLn.png) * 如果我們在 `fork()` 之前有 `pipe()` 的話,將能透過此 Pipeline 進行溝通。 * [相關的關係請參考下方 "C program to demonstrate `fork()` and `pipe()`"](#C-program-to-demonstrate-fork-and-pipe) ### C program to demonstrate `fork()` and `pipe()` [資料來源](https://www.geeksforgeeks.org/c-program-demonstrate-fork-and-pipe/) 在此撰寫 Linux C 程式來創建兩個 Process,P1 和 P2。 * P1 寫入一個字串並將其傳遞給 P2。 * P2 不使用字串函數將接收到的字串與另一個字串連接起來,並將其發送回 P1 進行顯示。 * 舉例如下: ``` 另一個字串:forgeeks.org P1 輸入:www.geeks P1 輸出:www.geeksforgeeks.org P1 輸入:www.practice.geeks P1 輸出:practice.geeksforgeeks.org ``` #### `fork()` 使用說明 ``` #include <unistd.h> ... pid_t fork(void); ``` * 為了建立子行程,需要使用 `fork()` ,而 return 的意義如下: * < 0 : 無法建立子(新)行程,可對照錯誤代碼去找原因。 * = 0 : 當前是子行程。 * > 0 : 即子行程的 PID。當 > 0 時,表示目前正在執行父行程當中執行,而父行程知道子行程的 PID。 #### C 語言範例 ```c= // C program to demonstrate use of fork() and pipe() #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<string.h> #include<sys/wait.h> int main() { // We use two pipes // First pipe to send input string from parent // Second pipe to send concatenated string from child int fd1[2]; // Used to store two ends of first pipe int fd2[2]; // Used to store two ends of second pipe char fixed_str[] = "forgeeks.org"; char input_str[100]; pid_t p; if (pipe(fd1)==-1) { fprintf(stderr, "Pipe Failed" ); return 1; } if (pipe(fd2)==-1) { fprintf(stderr, "Pipe Failed" ); return 1; } printf("Please input a string:"); scanf("%s", input_str); p = fork(); if (p < 0) { fprintf(stderr, "fork Failed" ); return 1; } // Parent process else if (p > 0) { char concat_str[100]; close(fd1[0]); // Close reading end of first pipe // Write input string and close writing end of first // pipe. write(fd1[1], input_str, strlen(input_str)+1); close(fd1[1]); // Wait for child to send a string wait(NULL); close(fd2[1]); // Close writing end of second pipe // Read string from child, print it and close // reading end. read(fd2[0], concat_str, 100); printf("Concatenated string %s\n", concat_str); close(fd2[0]); } // child process else { close(fd1[1]); // Close writing end of first pipe // Read a string using first pipe char concat_str[100]; read(fd1[0], concat_str, 100); // Concatenate a fixed string with it int k = strlen(concat_str); int i; for (i=0; i<strlen(fixed_str); i++) concat_str[k++] = fixed_str[i]; concat_str[k] = '\0'; // string ends with '\0' // Close both reading ends close(fd1[0]); close(fd2[0]); // Write concatenated string and close writing end write(fd2[1], concat_str, strlen(concat_str)+1); close(fd2[1]); exit(0); } } ``` ### `dup()` and `dup2()` Linux system call [資料來源](https://www.geeksforgeeks.org/dup-dup2-linux-system-call/) `dup()` 及 `dup2()` 為系統呼叫,用於複製 file descriptor。 * 通常會從數字較小的地方開始找可以用的 file descriptor 編號來建立新的。 * 如果建立成功,表示新的與舊的可以交互使用。 * 它們將指向同一個 "已開啟" 的 file descriptor 因此 `offset` 及 `檔案的狀態旗標` 將會是一樣的。 #### 語法參考 ```c int dup(int oldfd); oldfd: old file descriptor whose copy is to be created. ``` #### C 語言範例 ```c= // CPP program to illustrate dup() #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { // open() returns a file descriptor file_desc to a // the file "dup.txt" here" int file_desc = open("./dup.txt", O_WRONLY | O_APPEND | O_CREAT); if(file_desc < 0) printf("Error opening the file\n"); // dup() will create the copy of file_desc as the copy_desc // then both can be used interchangeably. int copy_desc = dup(file_desc); // write() will write the given string into the file // referred by the file descriptors write(copy_desc,"This will be output to the file named dup.txt\n", 46); write(file_desc,"This will also be output to the file named dup.txt\n", 51); system("cat ./dup.txt"); system("rm -rf ./dup.txt"); return 0; } ``` ### 簡易的 Shell 範例 ```cpp= // shell.cpp #include <errno.h> #include <fcntl.h> #include <iostream> #include <signal.h> #include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(int argc, char **argv) { // 處理 SIGCHLD,可以避免 Child 疆屍程序 struct sigaction sigchld_action; sigchld_action.sa_handler = SIG_DFL; sigchld_action.sa_flags = SA_NOCLDWAIT; // 原本指令 ls | cat | cat | cat | cat | cat | cat | cat | cat // 假設 Shell 已經將指令 Parse 好 char **cmds[9]; char *p1_args[] = {"ls", NULL}; cmds[0] = p1_args; char *p2_args[] = {"cat", NULL}; // 只是 DEMO,所以重複利用 for (int i = 1; i < 9; i++) cmds[i] = p2_args; int pipes[16]; // 需要共 8 條 pipe for (int i = 0; i < 8; i++) pipe(pipes + i * 2); // 建立 i-th pipe pid_t pid; for (int i = 0; i < 9; i++) { pid = fork(); if (pid == 0) { // Child // 讀取端 if (i != 0) { // 用 dup2 將 pipe 讀取端取代成 stdin dup2(pipes[(i - 1) * 2], STDIN_FILENO); } // 用 dup2 將 pipe 寫入端取代成 stdout if (i != 8) { dup2(pipes[i * 2 + 1], STDOUT_FILENO); } // 關掉之前一次打開的 for (int j = 0; j < 16; j++) { close(pipes[j]); } execvp(*cmds[i], cmds[i]); // execvp 正確執行的話,程式不會繼續到這裡 fprintf(stderr, "Cannot run %s\n", *cmds[i]); } else { // Parent printf("- fork %d\n", pid); if (i != 0) { close(pipes[(i - 1) * 2]); // 前一個的寫 close(pipes[(i - 1) * 2 + 1]); // 當前的讀 } } } waitpid(pid, NULL, 0); // 等最後一個指令結束 std::cout << "===" << std::endl; std::cout << "All done." << std::endl; } ``` * 輸出應該會呈現這樣子 ```bash $ g++ shell.cpp && ./a.out - fork 8244 - fork 8245 - fork 8246 - fork 8247 - fork 8248 - fork 8249 - fork 8250 - fork 8251 - fork 8252 FILE_A FILE_B FILE_C === All done. ``` ### Bash 各功能簡易說明 * 查看 Bash 版本 (來自 Ubuntu 16.04) ```bash $ bash --version GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu) Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. ``` * [非官方鏡像的 Bash 4.3](https://github.com/bminor/bash/tree/bash-4.3) * [GUN 官方的 Bash 4.3](http://git.savannah.gnu.org/cgit/bash.git/tree/?h=bash-4.3) #### 語法特性 Bash 的命令語法是 Bourne shell 命令語法的超集。數量龐大的 Bourne shell 指令碼大多不經修改即可以在 bash 中執行,也因此 bash 成為許多 Linux Base 發行版預設使用的 Shell。 * 花括號擴充 > 能夠做到排列組合的功效,但不建議在可移植的環境當中使用,可能會在其他 Shell 中造成結果上的不同。 ```bash $ echo a{p,c,d,b}e ape ace ade abe $ echo {a,b,c}{d,e,f} ad ae af bd be bf cd ce cf ``` * 使用整數 > 可以直接做計算 ```bash VAR=55 # 將變數 55 賦值給 VAR ((VAR = VAR + 1)) # 變數 VAR 加 1。注意這裡沒有 '$' ((++VAR)) # 另一種方法给 VAR 加 1。使用 C 語言風格的前缀自增 ((VAR++)) # 另一種方法给 VAR 加 1。使用 C 語言風格的後缀自增 echo $((VAR * 22)) # VAR 乘以 22 並將结果送入命令 echo $[VAR * 22] # 同上,但為過時用法 ``` * 輸入輸出重新導向 > bash 擁有傳統 Bourne shell 缺乏的 I/O 重新導向語法。bash 可以同時重新導向標準輸出和標準錯誤。 ```bash command &> file command <<< "string to be read as standard input" ``` * 行程內的正規表示式 > bash 3.0 支援行程內的正規表示式 ```bash [[ string =~ regex ]] ``` * 跳脫字元 > $'string' 形式的字串會被特殊處理。字串會被展開成 string,並像 C 語言那樣將反斜槓及緊跟的字元進行替換。 ![](https://hackmd.io/_uploads/HySLmo-Lh.png) * 關聯陣列 > Bash 4.0 開始支援關聯陣列,通過類似AWK的方式,對於多維陣列提供了偽支援。 ```bash $ declare -A a # 宣告一個名為 a 的二維陣列 $ i=1; j=2 $ a[$i,$j]=5 # 將 Index 為 "$i,$j" 的位置賦值為 5 $ echo ${a[$i,$j]} ``` * 移植性 > 呼叫 Bash 時指定 `--posix` 或者在指令碼中聲明 `set -o posix` ,可以使得 Bash 幾乎遵循 POSIX 1003.2 標準。若要保證一個 Bash 指令碼的移植性,至少需要考慮到 Bourne shell,即 Bash 取代的 shell。 * Bash有一些傳統的 Bourne shell 所沒有的特性,包括以下這些: * 某些擴充的呼叫選項 * 命令替換(即$())(儘管這是 POSIX 1003.2 標準的一部分) * 花括號擴充 * 某些陣列操作、關聯陣列 * 擴充的雙層方括號判斷語句 * 某些字串生成操作 * 行程替換 * 正規表示式匹配符 * Bash特有的內建工具 * 協行程 ## 本章節練習與反思 * 為什麼我們需要這麼多種 Shell? * 為什麼我們要學習 Shell? * 請上網參考資料後試著建立自己的 Alias 指令。 * 請依照 Github 所提供的 Source Code 為基底,並[參考上述範例](#%E7%B0%A1%E6%98%93%E7%9A%84-Shell-%E7%AF%84%E4%BE%8B)寫一個自己的 Shell,msh。 * 畫面呈現類似這樣子 ``` ./msh msh> ls | wc -l msh> pwd msh> exit # 回到執行前的 Shell ``` ## 參考資料 * [JAN 29 2020 Bash shell 簡介](https://crmne0707.pixnet.net/blog/post/319685660-bash-shell-%E7%B0%A1%E4%BB%8B) * [Shell 簡介](https://dywang.csie.cyut.edu.tw/dywang/linuxSystem/node85.html) * [簡明 Shell 原理與實作](https://tigercosmos.xyz/post/2020/01/unix/simple-shell/) * [pipe() System call](https://www.geeksforgeeks.org/pipe-system-call/) * [C program to demonstrate fork() and pipe()](https://www.geeksforgeeks.org/c-program-demonstrate-fork-and-pipe/) * [dup() and dup2() Linux system call](https://www.geeksforgeeks.org/dup-dup2-linux-system-call/) * [[Linux C] fork 觀念由淺入深](https://wenyuangg.github.io/posts/linux/fork-use.html) * [stdin、stdout、stderr串流](https://welkinchen.pixnet.net/blog/post/41649481-stdin%E3%80%81stdout%E3%80%81stderr%E4%B8%B2%E6%B5%81) * [Everything is a file - Wiki](https://en.wikipedia.org/wiki/Everything_is_a_file) * [Bash - Wiki](https://zh.wikipedia.org/wiki/Bash)