2022年8月23日 星期二

Linux IO 輸入與輸出重新導向

I/O 的重新導向是 Linux 系統中很重要的一個特性,它可以讓我們任意串接各種程式的輸入與輸出、將資料導入檔案或從檔案中導出資料,結合多種 Linux 指令,組成任意的「指令管線」(command pipeline)。

輸入與輸出的重新導向

一般的 Linux 指令在執行時,會有三個輸入與輸出的資料流,分別為:

  1. 標準輸入(standard input,代碼為 0):程式執行所需要的輸入資料。
  2. 標準輸出(standard output,代碼為 1):程式正常執行所產生的輸出資料。
  3. 標準錯誤輸出(standard error output,代碼為 2):程式出錯時通知使用者用的訊息,或是呈現程式狀態用的訊息。

Linux 程式執行時的狀況就像這樣:

而重新導向的作用就是改變這些資料的流向,讓使用者可以非常彈性的組合各種程式,以下我們以範例來說明重新導向的用法。

標準輸出

最典型的程式會將程式執行的結果輸出在螢幕上(也就是說標準輸出預設就是螢幕),而我們可以使用 > 這個重新導向的運算子,將程式的標準輸出導向檔案,這樣輸出的訊息內容就會被儲存在檔案中,其用法為:

N > FILE

其中 N 是要設定重新導向的檔案代碼,若省略的話預設值為標準輸出的檔案代碼(即 1),而 FILE 就是要儲存輸出資料的檔案名稱。

以下是一個簡單的例子:

ls > output.txt

這樣就會把 ls 指令的輸出儲存至 output.txt 檔案中,而執行這行指令時,螢幕上就不會有其他的輸出了。

在執行上面這行指令時,如果 output.txt 這個檔案不存在的話,就會自動建立這個檔案,並把資料寫入其中。但若是這個檔案已經存在了,系統會把它的內容先清空,再將 ls 的輸出儲存進去,所以如果原本 output.txt 檔案中存在有舊的資料,就會全部被清掉。

如果想要以附加的方式把程式的輸出放在原本的檔案內容之後,可以使用 >>,其用法也是非常類似:

N >> FILE

以下為實際範例:

date >> output.txt
這樣一來,他就會在 output.txt 檔案中,於原本的 ls 輸出之後,再加上一行日期資訊。

標準錯誤輸出

若程式發生錯誤時,錯誤訊息預設也是會輸出在螢幕上(標準錯誤輸出預設為螢幕),例如:

ls non_exist > output.txt

這裡我們使用 ls 查看一個不存在的檔案,讓它產生錯誤,執行之後會在螢幕上看到一行錯誤訊息:

ls: non_exist: No such file or directory

這一行就是來自於 ls 標準錯誤輸出的訊息,而 output.txt 這個輸出檔案也會被建立,不過它的內容是空的(因為程式沒有產生任何正常的輸出)。

如果我們想要把程式的錯誤訊息導入檔案,可以使用 > 運算子,把標準錯誤輸出(2)導至指定的檔案:

ls non_exist > output.txt 2 > error.txt

這行指令就會將 ls 的標準錯誤輸出導入 error.txt 檔案,而正常的輸出則一樣導入 output.txt。

這是以附加方式寫入 error.txt 的例子:

ls non_exist > output.txt 2 >> error.txt

如果想要把正常的輸出以及錯誤的輸出都一起導入同一個 output.txt,可以加上 2 > &1

ls non_exist > output.txt 2 > &1

2 > &1 就是把標準錯誤輸出(2)導入標準輸出(1)的意思,然後再靠著 > 把所有的資料全部導入 output.txt,這樣所有的輸出訊息就會一起存入 outpupt.txt 中了。

這類重新導向的語法可以自己變化,例如我們也可以把資料都導向標準錯誤輸出,然後再導向檔案,結果是一樣的:

ls non_exist 2 > output.txt 1 > &2

另外還有一種寫法也可以將標準輸出與標準錯誤輸出都導向至同一個檔案:

ls non_exist &> output.txt

或是這樣也可以:

ls non_exist >& output.txt

標準輸入

一般需要輸入資料的 Linux 程式如果執行時沒有給他資料的話,預設就會從鍵盤讀取資料(也就是標準輸入預設是鍵盤),例如:

cat

直接執行 cat 時,他就會等待使用者從鍵盤輸入資料,並將接收到的資料輸出在螢幕上。

我們可以使用 < 運算子,將指定的檔案設定為程式的標準輸入,這樣他就會從檔案中讀取資料,用法如下:

cat < input.txt

這樣 cat 就可以取得 input.txt 檔案中的資料,並且顯示在螢幕上。

管線

前面介紹過的重新導向都是用於程式與檔案之間的資料流,而如果要把兩個程式的輸入與輸出串接起來,就可以使用管線(pipe)。

管線的寫法是 |,以下是一個例子:

ls | nl

nl 這個指令會把每一行資料加上行號,這一指令會將 ls 的輸出導給 nl,讓每個檔案名稱加上行號後,輸出在螢幕上。

管線與重新導向可以混用:

ls | nl > output 2 > &1

串接多個程式的狀況也是很常見的:

ls | grep keyword | nl | head -n 5

這個例子是在 ls 的輸出中,以 grep 篩選出有 keyword 的檔名,交給 nl 加上行號,最後交給 head 輸出前 5 行資料。

Linux 特殊檔案

在 Linux 系統上有一些特殊的檔案,善用這些特殊檔案可以讓我們在撰寫 shell 指令稿時更方便。

/dev/null

/dev/null 是 Linux 系統的空裝置,所有寫入這個檔案的資料都會被直接丟棄,而如果從這個檔案讀取資料,則會像讀取空檔案一樣,立即得到一個 EOF。

最常見的用法就是將沒有用的程式輸出導引至 /dev/null,這樣就不會讓沒用的輸出干擾螢幕畫面:

ls > /dev/null

上面這個例子是將 ls 的輸出導向至 /dev/null,這樣螢幕上就不會出現任何訊息。

以下是一個實務上比較常用的例子,假設我們想要檢查 Linux 系統上特定的程式有沒有在執行,我們可以在 ps 指令的輸出中,尋找特定的關鍵字,執行完之後,以 $? 查看最後一個 grep 執行的結果:

KEYWORD=chromium
ps aux | grep $KEYWORD | grep -v grep > /dev/null
echo $?

這我們要找的是 Chromium 瀏覽器的行程,看看系統上是不是有 Chromium 瀏覽器在執行,若有找到的話,$? 的值就會是 0,若沒有找到的話 $? 就會是 1。

在這個例子中,我們只需要 grep 最後的傳回值,而不需要看它輸出,所以我們將其標準輸出直接導向至 /dev/null 丟棄。

另外一個典型的例子就是把程式的任何輸出都丟棄,也就是把標準輸出與標準錯誤輸出都導向至 /dev/null,這個用法在執行自己撰寫的指令稿時很有用:

/path/to/script.sh > /dev/null 2 > &1

如果我們將自己的指令稿放在 crontab 中執行時,通常都會以這個方式丟棄所有的輸出,因為如果輸出太多訊息,會造成系統管理者有收不完的回報信件。

/dev/zero

/dev/zero 是一個特殊檔案,當我們從這個檔案讀取資料時,會讀出無限個 NULL 字元(0x00)資料,這個檔案可以用於清除硬碟資料:

# 清除硬碟(不可執行)
dd if=/dev/zero of=/dev/sda1

或是建立內容都是 0x00 的檔案:

dd if=/dev/zero of=file.dat count=1024 bs=1024

請參閱:dd - 備份與回覆資料的小工具

/dev/random 與 /dev/urandom

/dev/random 與 /dev/urandom 是 UNIX/Linux 系統的隨機資料來源,從這兩個檔案讀取出來的資料是隨機產生的,這兩個檔案功能相同,差別在於資料產生速度與品質。

/dev/random 的資料產生速度比較慢,但是品質比較好(比較接近隨機),如果從中讀取大量資料,超過系統所能產生的隨機量時,就會出現阻塞(block)的狀況。

/dev/urandom 在產生隨機資料時,若系統所能產生的隨機量不足,就會根據其他的方式生成隨機的資料,不會產生阻塞的狀況,缺點就是資料沒有那麼隨機。

以下是從 /dev/urandom 讀取資料,交給 cksum 產生隨機亂數的例子:

head -200 /dev/urandom | cksum

/dev/full

/dev/full 是一個常滿裝置,不管寫入什麼資料,都會因為空間不足而無法寫入,這個檔案通常被用來測試磁碟無剩餘空間時,程式所產生的行為。

ls > /dev/full

ls: write error: No space left on device