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