命令列環境

當我們使用 shell 時,可以使用一些方法改善我們的作業流程,本節課我們就來討論這些方法。 我們已經使用 shell 一段時間了,但是到目前為止我們的關注點主要集中在使用不同的指令上面。現在,我們將會學習如何同時執行多個不同的行程(processes)並追蹤它們的狀態、如何停止或暫停某個行程,以及如何使其在背景執行。

我們還將學習一些方法能夠改善我們的 shell 及其他工具的作業流程,像是透過定義別名或修改配置文件。這些方法都可以幫我們節省大量的時間,僅需要執行一些簡單的指令,我們就可以在所有的主機上使用相同的配置。我們還會學習如何使用 SSH 操作遠端電腦。

任務控制

某些情況下我們需要中斷正在執行的任務,比如當一個指令需要執行很長時間才能完成時(假設我們在一個非常大的目錄結構中使用 find 進行搜索)。大多數情況下,我們可以使用 Ctrl-C 來停止指令的執行。但是它的工作原理是什麼呢?為什麼有的時候會無法結束行程?

結束行程

我們的 shell 會使用 UNIX 提供的信號(signal)機制達到行程間的相互溝通。當一個行程接收到信號時,它會停止執行、處理該信號並根據信號傳遞的訊息來改變其執行流程。就這一點而言,信號是一種軟體中斷(software interrupts)。

在上面的例子中,當我們輸入 Ctrl-C 時,shell 會發送一個SIGINT 信號到行程。

下面這個 Python 程式示範如何捕獲信號 SIGINT 並忽略它的基本操作, Ctrl-C 並不會讓程式停止。為了停止這個程式,我們需要使用 SIGQUIT 信號,通過輸入 Ctrl-\ 可以發送該信號。

#!/usr/bin/env python
import signal, time

def handler(signum, time):
    print("\nI got a SIGINT, but I am not stopping")

signal.signal(signal.SIGINT, handler)
i = 0
while True:
    time.sleep(.1)
    print("\r{}".format(i), end="")
    i += 1

如果我們向這個程式發送兩次 SIGINT ,然後再發送一次 SIGQUIT ,程式會有什麼反應?注意 ^ 是我們在終端輸入 Ctrl 時的表示形式:

$ python sigint.py
24^C
I got a SIGINT, but I am not stopping
26^C
I got a SIGINT, but I am not stopping
30^\[1]    39913 quit       python sigint.py

SIGINTSIGQUIT 都常常用來發出和終止程式相關的請求,但有個更加通用、優雅地退出信號的方法是 SIGTERM 。為了發出這個信號我們需要使用 kill 指令, 它的語法是: kill -TERM <PID>

行程暫停和背景執行

信號可以讓行程做其他的事情,而不僅僅是終止它們。例如,SIGSTOP 會讓行程暫停。在終端中,輸入 Ctrl-Z 會讓 shell 發送 SIGTSTP (Terminal Stop)信號。

我們可以使用 fgbg 指令恢復暫停的工作。它們分別表示在前台或背景繼續執行。

jobs 指令會列出當前終端作業中尚未完成的任務。我們可以用 pid 指定這些任務(也可以用 pgrep 找出 pid)。更加符合直覺的操作是我們可以使用百分比符號 + 任務編號( jobs 會打印任務編號)來選取該任務。如果要選擇最近的一個任務,可以使用 $! 這一特殊參數。

還有一件事情需要掌握,那就是指令中的 & 後綴可以讓指令在直接在背景執行,這使得我們可以直接在 shell 中繼續做其他操作,不過它此時還是會使用 shell 的標準輸出,這一點有時會比較惱人(這種情況可以使用 shell 重定向處理)。

讓已經在執行的行程轉到背景執行,我們可以輸入 Ctrl-Z ,然後緊接著再輸入 bg 。請注意,背景的行程仍然是我們的終端行程的子行程,一旦我們關閉終端(會發送另外一個信號 SIGHUP),這些背景的行程也會終止。為了防止這種情況發生,我們可以使用 nohup (一個用來忽略 SIGHUP 的封裝) 來執行程式。針對已經執行的程式,可以使用 disown 。除此之外,我們可以使用終端多工器來實現,下一章節我們會進行詳細地探討。

用一些簡單的指令來示範上述觀念:

$ sleep 1000
^Z
[1]  + 18653 suspended  sleep 1000

$ nohup sleep 2000 &
[2] 18745
appending output to nohup.out

$ jobs
[1]  + suspended  sleep 1000
[2]  - running    nohup sleep 2000

$ bg %1
[1]  - 18653 continued  sleep 1000

$ jobs
[1]  - running    sleep 1000
[2]  + running    nohup sleep 2000

$ kill -STOP %1
[1]  + 18653 suspended (signal)  sleep 1000

$ jobs
[1]  + suspended (signal)  sleep 1000
[2]  - running    nohup sleep 2000

$ kill -SIGHUP %1
[1]  + 18653 hangup     sleep 1000

$ jobs
[2]  + running    nohup sleep 2000

$ kill -SIGHUP %2

$ jobs
[2]  + running    nohup sleep 2000

$ kill %2
[2]  + 18745 terminated  nohup sleep 2000

$ jobs

SIGKILL 是一個特殊的信號,它不能被行程捕獲並且會馬上結束該行程。不過這樣做會有一些副作用,例如留下孤兒行程。

我們可以輸入 man signalkill -t 來,詳細可參考here

終端多工器 (Terminal Multiplexers)

當我們在使用命令列介面時,我們通常會希望同時執行多個任務。舉例來說,我們可以想要同時運行我們的編輯器,並在終端的另外一側執行程序。儘管再打開一個新的終端窗口也能達到目的,使用終端多工器則是一種更好的辦法。

tmux 這類的終端多工器可以允許我們根據面板和標籤分割出多個終端窗口,這樣我們便可以同時與多個 shell 進行互動。 不僅如此,終端多工使我們可以分離當前終端作業並在將來重新連接。 這讓我們操作遠端設備時的工作流程大大改善,避免了 nohup 和其他類似技巧的使用。

現在最流行的終端多工器是 tmuxtmux 是一個高度可定制的工具,我們可以使用相關快捷鍵建立多個分頁並在它們間切換。

tmux 的快捷鍵需要我們掌握,它們都是類似 <C-b> x 這樣的組合,即先按下Ctrl+b,鬆開後再按下 xtmux 中對象的繼承結構如下:

這裡 是一份 tmux 快速入門教學, 而 這一篇 文章則更加詳細,它包含了 screen 命令。我們也許想要掌握 screen 命令,因為在大多數 UNIX 系統中都預設安裝有該程序。

别名 (Aliases)

輸入一長串包含許多選項的命令會非常麻煩。 因此,大多數 shell 都支援設置 別名。 shell 的別名相當於一個長命令的縮寫,shell 會自動將其替換成原本的命令。 例如,bash 中的別名語法如下:

alias alias_name="command_to_alias arg1 arg2"

Note that there is no space around the equal sign =, because alias is a shell command that takes a single argument.

Aliases have many convenient features: 請注意 = 兩邊是沒有空格的,因為 alias 是一個 shell 命令,它只接受一個參數。 別名有許多很方便的特性:

# 創建常用指令的縮寫
alias ll="ls -lh"

# 能夠節省很多的指令輸入
alias gs="git status"
alias gc="git commit"
alias v="vim"

# 讓我們避免誤打命令
alias sl=ls

# 重定義現有命令的預設行為
alias mv="mv -i"           # -i prompts before overwrite
alias mkdir="mkdir -p"     # -p make parent dirs as needed
alias df="df -h"           # -h prints human readable format

# 別名可以組合使用
alias la="ls -A"
alias lla="la -l"

# 前置反斜線來忽略某個別名
\ls
# 或使用unalias來禁用別名
unalias la

# 印出該別名的定義
alias ll
# 將會印出 ll='ls -lh'

值得注意的是,在默認情況下 shell 並不會保存別名。 為了讓別名持續生效,我們需要將配置放進 shell 的啟動文件裡,像是 .bashrc.zshrc,下一節我們就會講到。

配置文件 (Dotfiles)

很多程式的配置都是通過純文本格式的被稱作點文件(dotfile)的配置文件來完成的(之所以稱為點文件,是因為它們的文件名以 . 開頭,例如 ~/.vimrc。也正因為此,它們默認是隱藏文件, ls 並不會顯示它們)。

shell 的配置也是通過這類文件完成的。在啟動時, shell 程式會讀取很多文件以載入其配置選項。根據 shell 本身的不同,我們從登入開始還是以互動的方式完成這一過程可能會有很大的不同。關於這一話題,這裡有非常好的資源。

對於 bash 來說,在大多數系統下,我們可以透過編輯 .bashrc.bash_profile 來進行配置。在文件中我們可以添加需要在啟動時執行的指令,例如上文我們講到過的別名,或者是我們的環境變數。 實際上,很多程序都要求我們在 shell 的配置文件中包含一行類似 export PATH="$PATH:/path/to/program/bin" 的命令,這樣才能確保這些程式能夠被 shell 找到。

還有一些其他工具可以透過點文件配置:

我們應該如何管理這些配置文件呢,它們應該在它們的文件夾下,並使用版本控制系統進行管理,然後通過腳本將其符號鏈接(symlinked)到需要的地方。這麼做有如下好處:

配置文件中需要放些什麼?我們可以通過線上文件和幫助手冊了解所使用工具的設置項。另一個方法是在網上搜尋有關特定程序的文章,作者們在文章中會分享他們的配置。還有一種方法就是直接瀏覽其他人的配置文件:我們可以在這裡找到無數的dotfiles庫 —— 其中最受歡迎的那些可以在這裡找到(建議不要直接複製別人的配置)。這裡也有一些非常有用的資源。 本課程的老師們也在 GitHub 上開源了他們的配置文件: Anish, Jon, Jose

可移植性

配置文件的一個常見的痛點是它可能並不能在多種設備上生效。例如,如果我們在不同設備上使用的操作系統或者 shell 是不同的,則配置文件是無法生效的。或者,有時我們僅希望特定的配置只在某些設備上生效。

有一些技巧可以輕鬆達成這些目的。如果配置文件 if 語句,則我們可以藉助它針對不同的設備編寫不同的配置。例如,我們的 shell 可以這樣做:

if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi

# 使用和 shell 相關的配置時先檢查當前 shell 類型
if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi

# 您也可以針對特定的設備進行配置
if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi

如果配置文件支持 include 功能,您也可以多加利用。例如: ~/.gitconfig 可以這樣編寫:

[include]
    path = ~/.gitconfig_local

然後我們可以在每個設備上建立配置文件 ~/.gitconfig_local ,包含與該設備相關的特定配置。甚至應該創建一個單獨的 repository 來管理這些與設備相關的配置。

如果希望在不同的程序之間共享某些配置,該方法也適用。例如,如果您想要在 bashzsh 中同時啟用一些別名,您可以把它們寫在 .aliases 裡,然後在這兩個 shell 裡套用:

# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
    source ~/.aliases
fi

遠端連線

對於工程師來說,在他們的日常工作中使用遠端伺服器已經非常普遍。如果需要使用遠端伺服器來部署後端軟體或一些計算能力強大的伺服器,您就會用到Secure Shell(SSH)。和其他工具一樣,SSH 也是可以高度可配置的,也值得我們花時間學習它。

通過以下指令,可以使用 ssh 連接到其他伺服器:

ssh foo@bar.mit.edu

這裡我們嘗試以用戶名 foo 登入伺服器 bar.mit.edu。伺服器可以通過 URL 指定(例如 bar.mit.edu),也可以使用 IP 指定(例如 foobar@192.168.1.42)。後面我們會介紹如何修改 ssh 配置文件使我們可以用類似 ssh bar 這樣的指令來登陸伺服器。

執行指令

ssh 的一個經常被忽視的特性是它可以直接遠端執行指令。 ssh foobar@server ls 可以直接在用foobar的指令下執行 ls 指令。想要配合管線來使用也可以, ssh foobar@server ls | grep PATTERN 會在本地查詢遠端 ls 的輸出而 ls | ssh foobar@server grep PATTERN 會在遠端對本地 ls 輸出的結果進行查詢。

SSH 密鑰

基於密鑰的驗證機制使用了密碼學中的公鑰,我們只需要向伺服器證明用戶端持有對應的私鑰,而不需要公開其私鑰。這樣就可以避免每次登入都輸入密碼的麻煩了。不過,私鑰(通常是 ~/.ssh/id_rsa 或者 ~/.ssh/id_ed25519) 等效於您的密碼,所以一定要好好保存它。

密鑰生成

使用 ssh-keygen 命令可以生成一對密鑰:

ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519

您可以為密鑰設置密碼,防止有人持有您的私鑰並使用它訪問您的伺服器。您可以使用 ssh-agentgpg-agent ,這樣就不需要每次都輸入該密碼了。

如果您曾經設定過使用 SSH 密鑰推送到 GitHub,那麼可能您已經完成了這裡介紹的這些步驟,並且已經有了一對可用的密鑰。要檢查您是否持有密碼並驗證它,您可以運行 ssh-keygen -y -f /path/to/key

基於密鑰的認證機制

ssh 會查詢 .ssh/authorized_keys 來確認哪些用戶允許登入。可以通過下面的命令將一個公鑰複製到這裡:

cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'

如果支援 ssh-copy-id 的話,可以使用下面這種更簡單的解決方案:

ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote

通過 SSH 複製文件

使用 ssh 複製文件有很多方法:

端口轉發 (Port Forwarding)

很多情況下我們都會遇到軟體需要監聽特定設備的端口。如果是在本機,可以使用 localhost:PORT127.0.0.1:PORT。但是如果需要監聽遠端伺服器的端口該如何操作呢?這種情況下遠端的端口並不能直接通過網路存取。

此時就需要進行端口轉發(port forwarding)。端口轉發有兩種,一種是本地端口轉發和遠端伺服器發(參見下圖,該圖片引用自這篇StackOverflow 文章)中的圖片。

本地端口轉發 Local Port Forwarding

遠端端口轉發 Remote Port Forwarding

常見的情景是使用本地端口轉發,即遠端設備上的服務監聽一個端口,而您希望在本地設備上的一個端口建立連接並轉發到遠端伺服器。例如,我們在遠端伺服器上運行 jupyter notebook 並監聽 8888 端口。然後,建立從本地端口 9999 的轉發,使用 ssh -L 9999:localhost:8888 foobar@remote_server 。這樣只需要訪問本地的 localhost:9999 即可。

SSH 配置

我們已經介紹了很多SSH可傳入的參數。為它們創建一個別名是個好主意,我們可以這樣做:

alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@remote_server

不過,更好的方法是使用 ~/.ssh/config

Host vm
    User foobar
    HostName 172.16.174.141
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888

# 在配置文件中也可以使用萬用字元
Host *.mit.edu
    User foobaz

這麼做的好處是,使用 ~/.ssh/config 文件來創建別名,類似 scprsyncmosh等指令都可以讀取這個配置並將設置轉換為對應的命令列選項。

注意,~/.ssh/config 文件也可以被當作配置文件(dotfile),而且一般情況下也是可以被引入到其他配置文件的。不過,如果您將其公開到網路上,那麼其他人都將會看到您的伺服器地址、用戶名、開放端口等等。這些資訊可能會幫助到那些企圖攻擊您系統的駭客所以請務必三思。

伺服器的配置通常放在 /etc/ssh/sshd_config。您可以在這裡配置免密碼認證、修改 ssh 端口、開啟 X11 轉發等等。也可以為個別用戶指定配置。

雜項

連接遠端伺服器的一個常見痛點是遇到由關機、休眠或網路環境變化導致的斷線。如果連接的延遲很高也很讓人討厭。 Mosh(即 mobile shell )對 ssh 進行了改進,它允許連接漫遊、間歇性連線及智慧本地回顯。

有時將一個遠端文件夾掛載到本地端會比較方便, sshfs 可以將遠端伺服器上的一個文件夾掛載到本地,然後您就可以使用本地的編輯器了。

Shell & 框架

在 shell 工具和腳本那節課中我們已經介紹了 bash shell,因為它是目前最通用的 shell,大多數的系統都將其作為默認 shell。但是,它並不是唯一的選項。

例如,zsh shell 是 bash 的父集合並提供了一些方便的功能:

框架也可以改進您的 shell。比較流行的通用框架包括 preztooh-my-zsh。還有一些更精簡的框架,它們往往專注於某一個特定功能,例如 zsh-syntax-highlightingzsh-history-substring-search。像 fish 這樣的 shell 包含了很多便利的功能,其中一些特性包括:

需要注意的是,使用這些框架可能會降低您 shell 的性能,尤其是如果這些框架的程式碼沒有最佳化或者過於冗長。您可以時常測試其性能或禁用某些不常用的功能來達到速度與功能的平衡。

終端模擬器 (Terminal Emulators)

和自定義 shell 一樣,花點時間選擇適合您的終端模擬器並進行設置是很有必要的。有許多終端模擬器可供您選擇(這裡有一些關於它們之間比較的資訊)。

您會花上很多時間在使用終端上,因此研究一下終端的設置是很有必要的,您可以從下面這些方面來設置您的終端:

課後練習

任務控制

  1. 我們可以使用類似 ps aux | grep 這樣的指令來獲取任務的 pid ,然後您可以根據 pid 來結束這些行程。但我們其實有更好的方法來做這件事。在終端中執行 sleep 10000 這個任務。然後用 Ctrl-Z 將其切換到後台並使用 bg 來繼續允許它。現在,使用 pgrep 來查找 pid 並使用 pkill 結束行程而不需要手動輸入pid。 (提示: 使用 -af 旗標)。
  1. 如果您希望某個行程結束後再開始另外一個行程, 應該如何實現呢?在這個練習中,我們使用 sleep 60 & 作為先執行的程序。一種方法是使用 wait 指令。嘗試啟動這個休眠指令,然後待其結束後再執行 ls 指令。

    但是,如果我們在不同的 bash 會話中進行操作,則上述方法就不起作用了。因為 wait 只能對子行程起作用。之前我們沒有提過的一個特性是,kill 指令成功退出時其狀態碼為 0 ,其他狀態則是非0。 kill -0 則不會發送信號,但是會在行程不存在時返回一個不為0的狀態碼。請撰寫一個 bash 函數 pidwait ,它接受一個 pid 作為輸入參數,然後一直等待直到該行程結束。您需要使用 sleep 來避免浪費 CPU 性能。

終端多工器

  1. 請完成這個 tmux 教學 參考這些步驟來學習如何自定義 tmux。

別名

  1. 創建一個 dc 別名,它的功能是當我們錯誤的將 cd 輸入為 dc 時也能正確執行。
  1. 執行 history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 來獲取您最常用的十條指令,嘗試為它們創建別名。注意:這個指令只在 Bash 中生效,如果您使用 ZSH,使用 history 1 替換 history

配置文件

讓我們幫助您進一步學習自動化配置文件:

  1. 為您的配置文件新建一個文件夾,並設置好版本控制
  2. 在其中添加至少一個配置文件,比如說您的 shell,在其中包含一些自定義設置(可以從設置 $PS1 開始)
  3. 建立一種在新設備進行快速安裝配置的方法(無需手動操作)。最簡單的方法是寫一個 shell 腳本對每個文件使用 ln -s,也可以使用專用工具
  4. 在新的虛擬機上測試該安裝腳本
  5. 將您現有的所有配置文件移動到dotfiles repository裡
  6. 將項目發佈到GitHub。

遠端連線

進行下面的練習需要您先安裝一個 Linux 虛擬機(如果已經安裝過則可以直接使用),如果您對虛擬機尚不熟悉,可以參考這篇教學來進行安裝。

  1. 前往 ~/.ssh/ 並查看是否已經存在 SSH 密鑰對。如果不存在,請使用ssh-keygen -o -a 100 -t ed25519 來創建一個。建議為密鑰設置密碼然後使用ssh-agent,更多信息可以參考這裡
  2. .ssh/config 加入下面內容:
Host vm
    User username_goes_here
    HostName ip_goes_here
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888
  1. 使用 ssh-copy-id vm 將您的 ssh 密鑰複製到伺服器。
  2. 使用 python -m http.server 8888 在您的虛擬機中啟動一個 Web 伺服器並通過本機的 http://localhost:9999 訪問虛擬機上的 Web 伺服器
  3. 使用 sudo vim /etc/ssh/sshd_config 編輯 SSH 伺服器配置,通過修改PasswordAuthentication 的值來禁用密碼驗證。通過修改 PermitRootLogin 的值來禁用 root 登陸。然後使用 sudo service sshd restart 重啟 ssh 伺服器,然後重新嘗試。
  4. (進階題) 在虛擬機中安裝 mosh 並啟動連接。然後斷開伺服器/虛擬機的網路卡。 mosh 可以恢復連接嗎?
  5. (進階題) 查看 ssh-N-f 選項的作用,找出在後台進行端口轉發的命令是什麼?

Edit this page.

Licensed under CC BY-NC-SA.