日拱一卒,配置開發環境不再愁,麻省理工手把手教你

語言: CN / TW / HK

大家好,日拱一卒,我是樑唐。

今天我們繼續聊聊麻省理工的missing smester,消失的學期,講述課堂上不會涉及,但又非常重要的知識和技能。

這一節課的主要內容是命令列的環境配置以及進階用法。主要包括任務管理、命令列多路執行器、別名、dotfile和遠端伺服器連線和使用等幾個部分。這些知識點非常非常有用,幾乎可以說是網際網路行業的任何技術崗位都能用得到。無論前端、後端、還是演算法。

相信我這麼說大家應該能體會到它的重要性。

這節課上課的又是西班牙老師,很遺憾,這節課在B站上沒有精校的中文翻譯版本,只有機翻的版本, 我個人感覺質量不是非常高,還是推薦有能力的直接看英文版字幕。可能會稍微吃力一點,但老師講的內容不錯,而且有視訊演示,所以不用特別擔心聽不懂的問題。

B站視訊連結

和之前一樣,這節課的note質量同樣非常高:note連結

本文是基於本節課note以及老師上課演示的內容,還有我個人的一些理解做的翻譯整理版本。日拱一卒,歡迎大家打卡一起學習。

前言

在這節課上我們將會介紹幾種方法,讓你在使用shell命令列的時候優化你的工作流。到目前為止,我們已經介紹了shell的不少內容,但我們對於同時執行多個命令的關注還比較少。我們將會一起來看看怎樣同時執行多個命令,並且追蹤它們,以及如何暫停、啟動和停止程序,還有如何讓一個程序在後臺執行。

我們同樣會學習shell的一些其他工具,比如定義一些別名,通過dotfile進行配置。這些都可以幫助你節約時間,比如通過一些配置可以讓你不再需要輸入長命令來完成任務。我們同樣會研究如何使用ssh命令來遠端控制機器。

任務控制

在一些情況下,你需要終端一個持續執行的程式。比如一個命令需要很長時間才能結束(比如在一個巨大的資料夾當中使用find搜尋)。大多數情況下你都可以使用Ctrl-C來結束,但這當中的原理是什麼呢?又為什麼有的時候Ctrl-C也不能奏效呢?

停止程序

你的shell使用一種叫做signal(訊號)的UNIX通訊機制和程序進行通訊。當一個程序接收到一個singal的時候,它會停止執行,處理這個訊號,並且基於這個訊號的資訊改變執行流。所以,訊號是一種軟體中斷。在我們的例子當中,當我們輸入Ctrl-C時,shell會發送一個SIGINT訊號給程序。

這裡有一個使用Python來捕獲SIGINT訊號並且忽視它的例子,因為捕獲了訊號,所以不會導致程式停止。想要停止程式需要使用SIGQUIT,輸入Ctrl-\即可。

```python

!/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在終端中會被展示成^

img

儘管SIGINTSIGQUIT都是常用的終止程式的終端請求,一個更常用的用來停止程式的訊號是SIGTERM。我們需要使用kill命令傳送這個訊號,語法是kill -TERM <PID>

暫停和後臺執行程序

訊號除了殺死程序之外還能做一些其他的事情。比如SIGSTOP可以讓一個程序暫停。在終端當中,輸入Ctrl-z可以傳送一個SIGTSTP訊號,SIGTSTP是terminal stop(終端停止)的簡寫,即終端版本的SIGSTOP

我們可以使用fgbg命令恢復一個暫停的任務,fg表示在前臺執行,bg表示在後臺執行。

jobs命令會列出當前終端session當中沒有結束的任務。你可以使用任務的pid來指代這些任務)也可以使用pgrep來找出pid)。你也可以使用百分號加上任務編號來指代任務,這樣更加符合直覺(jobs命令會打印出任務編號)。你也可以使用$!指代最近的一個任務。

另外一個需要注意的事情是shell命令列中最後加上&字尾,將會在後臺執行命令。這可以使得你可以繼續使用終端執行其他任務。不過後臺執行的任務仍然會使用shell的標準輸出,這點有的時候比較麻煩,可以使用重定向進行處理。

針對正在執行的程式,你可以先輸入Ctrl-z再使用bg命令將它轉入後臺執行。注意,轉入後臺執行的程序仍然是當前終端的子程序,這意味著當你關閉終端的時候(會發送另外一個訊號SIGHUP),這些程序都會結束。

為了防止這樣的情況發生,你可以使用nohup關鍵字(可以忽略SIGHUP訊號)來執行程式,如果程序已經在執行了,可以使用disown。或者你可以使用下一節中介紹的終端多路器。

下面是展示了剛才這些概念的簡單例子:

img

img

SIGKIL是一個特殊的訊號,因為它不能被程序捕獲,而是會直接結束程序。這樣做會有一些副作用,比如留下孤兒程序。

你可以查詢singal更多的用法,或者使用man signal或者使用kill -l來獲取更多關於訊號的資訊。

終端多路複用器

當你使用終端的時候,經常會需要同時執行多個程式。比如你想要同時編輯程式碼和執行程式,儘管開啟一個新的終端視窗也能實現,但使用終端多路複用器是一個更好的解決方案。

終端多路執行器比如tmux允許你在一個終端當中建立多個視窗, 在視窗中建立多個pane和tab,從而同時和多個終端session進行互動。不僅如此,終端多路執行器可以讓我們暫時離開當前終端session,並且在之後重新連線。當你在遠端機器上工作的時候,這會非常友好。因為可以避免使用nohup或者類似的操作。

目前最流行的終端多路複用器是tmuxtmux可以高度定製,通過組合鍵可以建立多個tab和pane以及快速在它們之間導航。

tmux希望你可以記住它的組合鍵,通常是 x的格式。代表按下Ctrl-c之後鬆開,再按下x。tmux當中的結構如下;

  • 會話 - 每個會話都是一個獨立的工作區,其中包含一個或多個視窗

  • tmux 開始一個新的會話

  • tmux new -s NAME 以指定名稱開始一個新的會話
  • tmux ls 列出當前所有會話
  • 在 tmux 中輸入 d ,將當前會話分離
  • tmux a 重新連線最後一個會話。您也可以通過 -t 來指定具體的會話

  • 視窗 - 相當於編輯器或是瀏覽器中的標籤頁,從視覺上將一個會話分割為多個部分

  • c 建立一個新的視窗,使用 關閉

  • N 跳轉到第 N 個視窗,注意每個視窗都是有編號的
  • p 切換到前一個視窗
  • n 切換到下一個視窗
  • , 重新命名當前視窗
  • w 列出當前所有視窗

  • 面板 - 像 vim 中的分屏一樣,面板使我們可以在一個螢幕裡顯示多個 shell

  • " 水平分割

  • % 垂直分割
  • <方向> 切換到指定方向的面板,<方向> 指的是鍵盤上的方向鍵
  • z 切換當前面板的縮放
  • [ 開始往回捲動螢幕。您可以按下空格鍵來開始選擇,回車鍵複製選中的部分
  • <空格> 在不同的面板排布間切換

想要了解更多tmux的用法,可以訪問網站:http://www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/和http://linuxcommand.org/lc3_adv_termmux.php。第二篇更加詳細,而且還包含了screenscreen也是一個終端多路執行器,並且在大多數UNIX系統當中都預設安裝了screen

別名

有的時候輸入比較長的命令比較麻煩,尤其是涉及多許多flag和選項的時候。出於簡化的目的,大多數shell都支援別名。shell中的別名是一個命令的縮寫形式,shell會自動替我們做好替換。比如bash中的別名語法如下:

img

注意,在等號左右沒有空格,因為alias是一個shell命令,它只接收一個引數。

別名有許多很方便的特性:

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

獲取別名的定義

alias ll

會列印 ll='ls -lh'

```

注意別名預設不是在shell中永久儲存的,為了讓別名永久生效,你可以將配置寫入shell的啟動配置當中。比如.bashrc.zshrc,下一節我們將會講到這個部分。

配置檔案(Dotfiles)

許多程式使用純文字的檔案來進行配置,這些檔案被稱為dotfiles(點檔案),因為它們的檔名以.開頭。比如~/.vimrc,因此這些檔案在資料夾當中都是隱藏的。

shell也是使用dotfile進行配置的程式,在啟動的時候,shell會讀取很多檔案來載入配置。根據shell的不同,你是否登入或者是否以互動的形式開始,這個過程會有很大的區別並且非常複雜。關於這個話題,這裡有一個很好的資源:http://blog.flowblok.id.au/2013-02/shell-startup-scripts.html

對於bash來說,編輯你的.bashrc或者.bash_profile在大多數系統當中能夠生效。在這兩個檔案當中,你可以引入一些你想要在啟動的時候執行的命令,比如我們剛才介紹的別名或者是配置一些PATH環境變數。實際上,很多程式都會要求你在shell配置檔案當中加入一行類似export PATH="$PATH:/path/to/program/bin"配置。加入了之後,才能保證這些程式能夠被shell找到。

還有一些其他的工具也可以使用dotfile進行配置:

  • bash - ~/.bashrc, ~/.bash_profile
  • git - ~/.gitconfig
  • vim - ~/.vimrc 和 ~/.vim 目錄
  • ssh - ~/.ssh/config
  • tmux - ~/.tmux.conf

我們將要怎麼管理我們的dotfile呢?它們應該在它們獨自的資料夾下,被版本控制管理,通過指令碼將它syblink到需要的地方。這樣做有這些好處:

  • 安裝簡單: 如果您登入了一臺新的裝置,在這臺裝置上應用您的配置只需要幾分鐘的時間;
  • 可以執行: 您的工具在任何地方都以相同的配置工作
  • 同步: 在一處更新配置檔案,可以同步到其他所有地方
  • 變更追蹤: 您可能要在整個程式設計師生涯中持續維護這些配置檔案,而對於長期專案而言,版本歷史是非常重要的

dotfile當中應該放些什麼?你可以閱讀一些線上文件,或者是man page。也可以在網際網路上搜索一些相關的部落格,作者將會告訴你它們偏好的配置。當然也可以看看其他人的dotfile,你可以在GitHub上找到成千上萬份dotfile:http://github.com/search?o=desc&q=dotfiles&s=stars&type=Repositories。這是其中最受歡迎的:http://github.com/mathiasbynens/dotfiles,這是另外一個很好的資源:http://dotfiles.github.io/。

我們希望你不是僅僅複製貼上,而是能花點時間閱讀一下配置檔案當中的細節, 理解這些配置存在的意義以及這麼配置的原因。

可移植性

配置檔案的一個痛點是它不能在不同的機器上生效,比如使用不同的作業系統或者是不同的裝置,那麼配置檔案可能不能生效。有的時候你可能也會希望配置檔案只在某些機器上生效。

有一些技巧可以輕鬆達到這個目的,如果機器適配配置檔案,可以使用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功能,你也可以使用include,比如~/.gitignore可以這樣編寫:

shell [include] path = ~/.gitconfig_local

對於每臺機器來說,~/.gitconfig_local可以包含獨有的一些配置。你甚至可以建立一個專門的倉庫來追蹤管理這些特定的配置。

在你想要不同的程式共享一些配置的時候,這個思路也一樣有用。比如,你想要讓bash和zsh共享同樣的別名,你可以將這些別名寫在.aliases當中,然後在這兩個shell的配置當中加上:

```shell

Test if ~/.aliases exists and source it

if [ -f ~/.aliases ]; then source ~/.aliases fi ```

遠端機器

對於程式設計師來說,日常工作當中經常會用到遠端機器。如果你需要使用遠端伺服器來部署後端程式或者是你需要一個高效能運算的伺服器,你需要使用Secure Shell(SSH)來進行連線。和其他工具一樣,SSH也是可以高度定製的,也值得我們花時間學習。

通過如下命令來登入伺服器:

img

這裡的foo是使用者名稱,bar.mit.edu是伺服器地址。伺服器地址可以是域名也可以是ip。之後我們將會看到進行ssh配置之後,我們可以僅僅使用ssh bar來進行登入。

執行命令

ssh一個經常被忽略的功能是直接執行命令。ssh [email protected] ls將會在foobar機器中home目錄下執行ls命令。管道命令同樣有效,所以ssh [email protected] ls | grep PATTERN將會本地grep遠端命令ls獲取的結果。ls | ssh [email protected] grep PATTERN將會在遠端對本地得到的結果進行grep。

SSH Keys

基於key驗證的機制使用了密碼學中的公鑰來向伺服器證明使用者持有對應的私鑰,而不需要公開私鑰。使用這種方法可以避免每次登入都輸入密碼。不過,私鑰相當於你的密碼,你需要保管好它。通常存放在~/.ssh/id_rsa或者~/.ssh/id_ed25519

Key 生成

你可以使用ssh-keygen命令生成私鑰和公鑰。

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

你可以給你的私鑰設定密碼,這樣就不用擔心別人持有你的私鑰訪問伺服器了。可以使用ssh-agent或者gpg-agent,這樣可以避免每次都輸入密碼。

如果你曾經配置過SSH祕鑰來push程式碼到GitHub,那麼你可能已經生成過了。要檢查你是否持有密碼並且驗證它,你可以執行ssh-keygen -y -f /path/to/key

基於key的驗證

ssh將會查詢.ssh/authorized_keys來決定允許哪些使用者訪問。你可以使用命令將你的公鑰拷貝到伺服器上:

shell cat .ssh/id_ed25519.pub | ssh [email protected] 'cat >> ~/.ssh/authorized_keys'

如果支援ssh-copy-id的話,下面這個方法更加簡單:

shell ssh-copy-id -i .ssh/id_ed25519 [email protected]

通過 SSH 複製檔案

使用 ssh 複製檔案有很多方法:

  • ssh+tee, 最簡單的方法是執行 ssh 命令,然後通過這樣的方法利用標準輸入實現 cat localfile | ssh remote_server tee serverfile。回憶一下,tee 命令會將標準輸出寫入到一個檔案;
  • scp :當需要拷貝大量的檔案或目錄時,使用scp 命令則更加方便,因為它可以方便的遍歷相關路徑。語法如下:scp path/to/local_file remote_host:path/to/remote_file;
  • rsync 對 scp 進行了改進,它可以檢測本地和遠端的檔案以防止重複拷貝。它還可以提供一些諸如符號連線、許可權管理等精心打磨的功能。甚至還可以基於 --partial標記實現斷點續傳。rsync 的語法和scp類似;

埠轉發

在許多場景當中,你將會執行一些監聽某些埠的程式。當你在本地執行的時候,你可以使用localhost:PORT或者127.0.0.1:PORT。但當你在伺服器上執行時你該如何操作呢?伺服器上的埠通常不會通過網路暴露給你。

此時就需要使用埠轉發,埠轉發有兩種,一種是本地埠轉發,一種是遠端埠轉發。參考下圖引用子Stack Overflow的圖片。

本地埠轉發

img

遠端埠轉發

img

最常用的是本地埠轉發,即遠端機器上的服務監聽了一個埠,你希望將本地機器的一個埠和遠端的埠連線起來。舉個例子,如果我們在遠端伺服器上的8888埠運行了一個jupyter notebook。然後我們建立本地9999埠的轉發,使用ssh -L 9999:localhost:8888 [email protected]_server。這樣一來,我們只需要訪問本地的localhost:9999埠即可。

SSH 配置

我們已經介紹了許多引數,一個很好的做法是為它們建立別名,比如這樣:

shell alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 [email protected]_server

不過更好的做法是配置~/.ssh/config

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

Configs can also take wildcards

Host *.mit.edu User foobaz ```

配置~/.ssh/config一個額外的好處是這些別名其他的程式,比如scp、rsync、mosh都能夠使用。

注意~/.ssh/config也是一個dotfile,一般情況下也可以被當做dotfile一起匯入。如果你公開到網際網路上,那麼其他人也能看到你的一些潛在資訊,比如伺服器、使用者名稱、埠號等等,這可能會幫助到那些想要入侵你的黑客,請務必小心。

伺服器端的配置通常在/etc/ssh/sshd_config當中,你可以在其中配置諸如取消密碼驗證、修改ssh埠、開啟X11轉發等等。你可以針對每一個使用者進行單獨設定。

雜項

一個遠端連線伺服器的痛點是,當網路發生變化、電腦關機/睡眠時會導致斷開連線。並且如果連線的延遲很高也很讓人絕望。Mosh(mobile shell)對ssh進行了改進,允許連線漫遊、間歇連線等等功能。

有時將遠端的資料夾掛載到本地比較方便,sshfs可以將遠端伺服器中的一個資料夾掛載到本地,這樣你就可以使用本地編輯器進行訪問了。

shell和框架

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

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

  • 智慧替換, **
  • 行內替換/萬用字元擴充套件
  • 拼寫糾錯
  • 更好的 tab 補全和選擇
  • 路徑展開 (cd /u/lo/b 會被展開為 /usr/local/bin)

框架也可以提升你的shell體驗,比較流行的通用框架包括preztooh-my-zsh。還有一些更精簡的框架,它們往往專注於某一個特定功能,例如zsh 語法高亮zsh 歷史子串查詢。 像 fish 這樣的 shell 包含了很多使用者友好的功能,其中一些特性包括:

  • 向右對齊
  • 命令語法高亮
  • 歷史子串查詢
  • 基於手冊頁面的選項補全
  • 更智慧的自動補全
  • 提示符主題

需要注意的是,使用這些框架可能會降低您 shell 的效能,尤其是如果這些框架的程式碼沒有優化或者程式碼過多。您隨時可以測試其效能或禁用某些不常用的功能來實現速度與功能的平衡。

終端模擬器

和自定義shell一樣,花費一點時間選擇和配置一個終端模擬器也是值得的。市面上的終端模擬器有很多,可以參考這個網站:http://anarc.at/blog/2018-04-12-terminal-emulators-1/

因為你會在終端上花費大量的時間, 因此好好配置是非常有必要的。通常有這些方面需要設定:

  • 字型選擇
  • 彩色主題
  • 快捷鍵
  • 標籤頁/面板支援
  • 回退配置
  • 效能(像 Alacritty 或者 kitty 這種比較新的終端,它們支援GPU加速)

練習

Job control

  1. 我們剛才已經看到,我們可以使用ps aux | grep命令來獲得我們任務的pid來kill它們。但還有更好的做法。在終端開啟一個sleep 10000的任務,使用Ctrl-Z讓它進入後臺,使用bg讓它繼續執行。現在使用pgrep命令來找到它的pid,使用pkill來殺掉它,而不再需要輸入pid(提示:使用-af標記)

答案

首先,建立sleep程序,並且讓它進入後臺執行

shell sleep 10000 Ctrl-Z bg

接著使用pgrep和pkill

img

a和f選項的含義:

img

a表示匹配程序的祖先程序,f表示匹配所有引數列表,預設只匹配程序名稱

  1. 如果你想要在一個程序結束之後啟動另外一個程序,應該怎麼操作呢?在這個練習噹噹中,我們將會首先啟動一個sleep 60 &的程序作為先導程序。一種方法是使用wait命令,試著先啟動sleep命令,然後等到結束再執行一個ls命令。

然而如果我們換一個bash的會話這種方法就行不通了,因為wait只會在子程序當中能夠執行。我們在note當 中沒有討論到的一點是kill命令在成功時會返回0,失敗會返回非0。kill -0將不會發送訊號,但會在程序不存 在的時候返回非0的狀態。編寫一個叫做pidwait的bash函式,它接收一個pid,並且等待直到程序結束。你 可以使用sleep來避免CPU資源的浪費

答案

使用wait的方式,注意wait需要接收一個pid,所以需要使用管道命令。並且wait和ls之間不用管道,因為我們需要讓wait等待,如果管道的話會同時執行。

shell sleep 60 & pgrep sleep | wait; ls

終端多路複用

  1. 請完成這個tmux教程:http://www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/

別名

  1. 建立別名dc,它的功能是當我們將cd輸錯的時候也能生效
  2. 執行history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10命令獲取你最常用的10個命令,嘗試為它們建立別名。注意這個只在bash當中生效,如果你使用的是zsh,使用history 1代替history

配置檔案

讓我們幫助您進一步學習配置檔案:

  1. 為您的配置檔案新建一個資料夾,並設定好版本控制
  2. 在其中新增至少一個配置檔案,比如說您的 shell,在其中包含一些自定義設定(可以從設定 $PS1 開始)。
  3. 建立一種在新裝置進行快速安裝配置的方法(無需手動操作)。最簡單的方法是寫一個 shell 指令碼對每個檔案使用 ln -s,也可以使用專用工具:http://dotfiles.github.io/utilities/
  4. 在新的虛擬機器上測試該安裝指令碼。
  5. 將您現有的所有配置檔案移動到專案倉庫裡。
  6. 將專案釋出到GitHub。

遠端裝置

進行下面的練習需要您先安裝一個 Linux 虛擬機器(如果已經安裝過則可以直接使用),如果您對虛擬機器尚不熟悉,可以參考這篇教程 來進行安裝,http://hibbard.eu/install-ubuntu-virtual-box/

  1. 前往 ~/.ssh/ 並檢視是否已經存在 SSH 金鑰對。如果不存在,請使用ssh-keygen -o -a 100 -t ed25519來建立一個。建議為金鑰設定密碼然後使用ssh-agent,更多資訊可以參考 這裡, http://www.ssh.com/ssh/agent
  2. 在.ssh/config加入下面內容:

shell 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 選項的作用,找出在後臺進行埠轉發的命令是什麼?