Try   HackMD

Input / Output (輸入輸出)

tags: Linux

目錄

Linux Kernel Input / Output

Stdin/Stdout/Stderr

其實 C 函式庫中已經宣告好 3 個 FILE * 的指標,分別是 stdinstdoutstderr

  • stdin : standard input 標準輸入串流
  • stdout : standard output 標準輸出串流
  • stderr : standard error 輸出串流且系統會留下紀錄檔

我們常用的函式 printfscanf ,實際上是包裝過後的結果:

  • 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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

何謂 Shell?

  • Shell 乃是 「介於使用者與作業系統之間的介面」 ,其功能為 「讓使用者能夠藉由指令來操作作業系統」
  • 替我們工作的是「硬體」,控制硬體的「Kernel」,我們則利用「shell」控制 Kernel 提供工具來使硬體正確工作。
  • Bash 為 linux 預設的 shell,文字模式之指令下達方式就是 bash 的工具與介面。
  • Shell 狹隘的定義是指文字模式的 BASH shell。廣義的 Shell 也可以是 KDE 之類的圖形介面控制軟體。
  • 如果沒有特別說明, Shell 指的是比較狹義的文字模式。

Shell History

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

  • 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:

$            # 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

資料來源

Pipeline 觀念介紹

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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 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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

C program to 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 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

資料來源
dup()dup2() 為系統呼叫,用於複製 file descriptor。

  • 通常會從數字較小的地方開始找可以用的 file descriptor 編號來建立新的。
  • 如果建立成功,表示新的與舊的可以交互使用。
  • 它們將指向同一個 "已開啟" 的 file descriptor 因此 offset檔案的狀態旗標 將會是一樣的。

語法參考

int dup(int oldfd);
oldfd: old file descriptor whose copy is to be created.

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 範例

// 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; }
  • 輸出應該會呈現這樣子
$ 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 --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 的命令語法是 Bourne shell 命令語法的超集。數量龐大的 Bourne shell 指令碼大多不經修改即可以在 bash 中執行,也因此 bash 成為許多 Linux Base 發行版預設使用的 Shell。

  • 花括號擴充

能夠做到排列組合的功效,但不建議在可移植的環境當中使用,可能會在其他 Shell 中造成結果上的不同。

$ 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
  • 使用整數

可以直接做計算

 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 可以同時重新導向標準輸出和標準錯誤。

command &> file
 command <<< "string to be read as standard input"
  • 行程內的正規表示式

bash 3.0 支援行程內的正規表示式

[[ string =~ regex ]]
  • 跳脫字元

$'string' 形式的字串會被特殊處理。字串會被展開成 string,並像 C 語言那樣將反斜槓及緊跟的字元進行替換。

  • 關聯陣列

Bash 4.0 開始支援關聯陣列,通過類似AWK的方式,對於多維陣列提供了偽支援。

$ 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 為基底,並參考上述範例寫一個自己的 Shell,msh。
    • 畫面呈現類似這樣子
      ​​​​​​​​./msh
      ​​​​​​​​msh> ls | wc -l
      ​​​​​​​​msh> pwd
      ​​​​​​​​msh> exit
      ​​​​​​​​# 回到執行前的 Shell
      

參考資料