日拱一卒,量大管饱,MIT手把手教你配环境

2022-09-21 11:11:11 浏览数 (1)

作者 | 梁唐

出品 | 公众号:Coder梁(ID:Coder_LT)

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

今天我们继续聊聊麻省理工的missing smester,消失的学期,讲述课堂上不会涉及,但又非常重要的知识和技能。

这一节课的主要内容是命令行的环境配置以及进阶用法。主要包括任务管理、命令行多路执行器、别名、dotfile和远程服务器连接和使用等几个部分。这些知识点非常非常有用,几乎可以说是互联网行业的任何技术岗位都能用得到。无论前端、后端、还是算法。

相信我这么说大家应该能体会到它的重要性。

这节课上课的又是西班牙老师,很遗憾,这节课在B站上没有精校的中文翻译版本,只有机翻的版本, 我个人感觉质量不是非常高,还是推荐有能力的直接看英文版字幕。可能会稍微吃力一点,但老师讲的内容不错,而且有视频演示,所以不用特别担心听不懂的问题。

B站视频链接:https://www.bilibili.com/video/BV1x7411H7wa?p=5

和之前一样,这节课的note质量同样非常高。大家可以点击「阅读原文」跳转英文原文。

本文是基于本节课note以及老师上课演示的内容,还有我个人的一些理解做的翻译整理版本。日拱一卒,欢迎大家打卡一起学习。

前言

在这节课上我们将会介绍几种方法,让你在使用shell命令行的时候优化你的工作流。到目前为止,我们已经介绍了shell的不少内容,但我们对于同时执行多个命令的关注还比较少。我们将会一起来看看怎样同时运行多个命令,并且追踪它们,以及如何暂停、启动和停止进程,还有如何让一个进程在后台执行。

我们同样会学习shell的一些其他工具,比如定义一些别名,通过dotfile进行配置。这些都可以帮助你节约时间,比如通过一些配置可以让你不再需要输入长命令来完成任务。我们同样会研究如何使用ssh命令来远程控制机器。

任务控制

在一些情况下,你需要终端一个持续运行的程序。比如一个命令需要很长时间才能结束(比如在一个巨大的文件夹当中使用find搜索)。大多数情况下你都可以使用Ctrl-C来结束,但这当中的原理是什么呢?又为什么有的时候Ctrl-C也不能奏效呢?

停止进程

你的shell使用一种叫做signal(信号)的UNIX通信机制和进程进行通信。当一个进程接收到一个singal的时候,它会停止运行,处理这个信号,并且基于这个信号的信息改变运行流。所以,信号是一种软件中断。在我们的例子当中,当我们输入Ctrl-C时,shell会发送一个SIGINT信号给进程。

这里有一个使用Python来捕获SIGINT信号并且忽视它的例子,因为捕获了信号,所以不会导致程序停止。想要停止程序需要使用SIGQUIT,输入Ctrl-即可。

代码语言:javascript复制
#!/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

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的用法,可以访问网站:https://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中的别名语法如下:

注意,在等号左右没有空格,因为alias是一个shell命令,它只接收一个参数。

别名有许多很方便的特性:

代码语言:javascript复制
# 创建常用命令的缩写
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的不同,你是否登录或者是否以交互的形式开始,这个过程会有很大的区别并且非常复杂。关于这个话题,这里有一个很好的资源:https://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:https://github.com/search?o=desc&q=dotfiles&s=stars&type=Repositories。这是其中最受欢迎的:https://github.com/mathiasbynens/dotfiles,这是另外一个很好的资源:https://dotfiles.github.io/。

我们希望你不是仅仅复制粘贴,而是能花点时间阅读一下配置文件当中的细节, 理解这些配置存在的意义以及这么配置的原因。

可移植性

配置文件的一个痛点是它不能在不同的机器上生效,比如使用不同的操作系统或者是不同的设备,那么配置文件可能不能生效。有的时候你可能也会希望配置文件只在某些机器上生效。

有一些技巧可以轻松达到这个目的,如果机器适配配置文件,可以使用if判断语句来对使用配置的机器进行配置。比如:

代码语言:javascript复制
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可以这样编写:

代码语言:javascript复制
[include]
    path = ~/.gitconfig_local

对于每台机器来说,~/.gitconfig_local可以包含独有的一些配置。你甚至可以创建一个专门的仓库来追踪管理这些特定的配置。

在你想要不同的程序共享一些配置的时候,这个思路也一样有用。比如,你想要让bash和zsh共享同样的别名,你可以将这些别名写在.aliases当中,然后在这两个shell的配置当中加上:

代码语言:javascript复制
# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
    source ~/.aliases
fi

远端机器

对于程序员来说,日常工作当中经常会用到远端机器。如果你需要使用远端服务器来部署后端程序或者是你需要一个高性能计算的服务器,你需要使用Secure Shell(SSH)来进行连接。和其他工具一样,SSH也是可以高度定制的,也值得我们花时间学习。

通过如下命令来登录服务器:

这里的foo是用户名,bar.mit.edu是服务器地址。服务器地址可以是域名也可以是ip。之后我们将会看到进行ssh配置之后,我们可以仅仅使用ssh bar来进行登录。

执行命令

ssh一个经常被忽略的功能是直接执行命令。ssh foobar@server ls将会在foobar机器中home目录下执行ls命令。管道命令同样有效,所以ssh foobar@server ls | grep PATTERN将会本地 grep远程命令ls获取的结果。ls | ssh foobar@server grep PATTERN将会在远端对本地得到的结果进行grep。

SSH Keys

基于key验证的机制使用了密码学中的公钥来向服务器证明用户持有对应的私钥,而不需要公开私钥。使用这种方法可以避免每次登录都输入密码。不过,私钥相当于你的密码,你需要保管好它。通常存放在~/.ssh/id_rsa或者~/.ssh/id_ed25519

Key 生成

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

代码语言:javascript复制
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来决定允许哪些用户访问。你可以使用命令将你的公钥拷贝到服务器上:

代码语言:javascript复制
cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'

如果支持ssh-copy-id的话,下面这个方法更加简单:

代码语言:javascript复制
ssh-copy-id -i .ssh/id_ed25519 foobar@remote

通过 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的图片。

本地端口转发
远端端口转发

最常用的是本地端口转发,即远端机器上的服务监听了一个端口,你希望将本地机器的一个端口和远程的端口连接起来。举个例子,如果我们在远程服务器上的8888端口运行了一个jupyter notebook。然后我们建立本地9999端口的转发,使用ssh -L 9999:localhost:8888 foobar@remote_server。这样一来,我们只需要访问本地的localhost:9999端口即可。

SSH 配置

我们已经介绍了许多参数,一个很好的做法是为它们创建别名,比如这样:

代码语言:javascript复制
alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@remote_server

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

代码语言:javascript复制
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体验,比较流行的通用框架包括prezto 或 oh-my-zsh。还有一些更精简的框架,它们往往专注于某一个特定功能,例如zsh 语法高亮 或 zsh 历史子串查询。像 fish 这样的 shell 包含了很多用户友好的功能,其中一些特性包括:

  • 向右对齐
  • 命令语法高亮
  • 历史子串查询
  • 基于手册页面的选项补全
  • 更智能的自动补全
  • 提示符主题

需要注意的是,使用这些框架可能会降低您 shell 的性能,尤其是如果这些框架的代码没有优化或者代码过多。您随时可以测试其性能或禁用某些不常用的功能来实现速度与功能的平衡。

终端模拟器

和自定义shell一样,花费一点时间选择和配置一个终端模拟器也是值得的。市面上的终端模拟器有很多,可以参考这个网站:https://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进程,并且让它进入后台运行

代码语言:javascript复制
sleep 10000
Ctrl-Z
bg

接着使用pgrep和pkill

a和f选项的含义:

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等待,如果管道的话会同时执行。

代码语言:javascript复制
sleep 60 & pgrep sleep | wait; ls

终端多路复用

  1. 请完成这个tmux教程:https://www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/

别名

  1. 创建别名dc,它的功能是当我们将cd输错的时候也能生效
  2. 运行history | awk '{1="";print substr(

配置文件

让我们帮助您进一步学习配置文件:

  1. 为您的配置文件新建一个文件夹,并设置好版本控制
  2. 在其中添加至少一个配置文件,比如说您的 shell,在其中包含一些自定义设置(可以从设置 $PS1 开始)。
  3. 建立一种在新设备进行快速安装配置的方法(无需手动操作)。最简单的方法是写一个 shell 脚本对每个文件使用 ln -s,也可以使用专用工具:https://dotfiles.github.io/utilities/
  4. 在新的虚拟机上测试该安装脚本。
  5. 将您现有的所有配置文件移动到项目仓库里。
  6. 将项目发布到GitHub。

远程设备

进行下面的练习需要您先安装一个 Linux 虚拟机(如果已经安装过则可以直接使用),如果您对虚拟机尚不熟悉,可以参考这篇教程 来进行安装,https://hibbard.eu/install-ubuntu-virtual-box/

  1. 前往 ~/.ssh/ 并查看是否已经存在 SSH 密钥对。如果不存在,请使用ssh-keygen -o -a 100 -t ed25519来创建一个。建议为密钥设置密码然后使用ssh-agent,更多信息可以参考 这里, https://www.ssh.com/ssh/agent
  2. 在.ssh/config加入下面内容:
代码语言:javascript复制
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 选项的作用,找出在后台进行端口转发的命令是什么?

喜欢本文的话不要忘记三连~

0 人点赞