基于北大前沿计算实践课和missing semester的linux等编程工具笔记
简介
Linux是一群开源的、基于Linux内核的类Unix操作系统集合。
- 操作系统(operating system):管理计算机硬件和软件资源的程序,为用户程序提供硬件抽象和接口。
- 操作系统内核(operating system kernel):操作系统最核心的部分,管理系统的进程、内存、设备驱动程序、文件和网络系统,一直在内存中,不包括图形界面、Shell等功能
- Shell:内核的封装,为用户提供更高级的抽象,比如
echo
、ls
、cd
等命令,以及进程间通信功能(管道) - Unix内核:最早形成规模,被广泛使用的操作系统,由肯•汤普森(Ken Thompson)和丹尼斯•里奇(Dennis Ritchie)发明,使用C编写,现在常用的基于UNIX内核的操作系统有Solaris、FreeBSD
- Linux内核:由李纳斯•托瓦兹(Linus Torvalds)在赫尔辛基大学读书时出于个人爱好而编写的,当时他觉得教学用的迷你版UNIX操作系统Minix太难用了,于是决定自己开发一个操作系统。第1版本于1991年9月发布,当时仅有10000行代码。李纳斯•托瓦兹没有保留Linux源代码的版权,公开了代码,并邀请他人一起完善Linux。据估计,现在只有2%的Linux核心代码是由李纳斯•托瓦兹自己编写的,虽然他仍然拥有Linux内核(操作系统的核心部分),并且保留了选择新代码和需要合并的新方法的最终裁定权(Benevolent dictator for life, BDFL)。
shell的配置
很多程序的配置都是通过纯文本格式的被称作_点文件_的配置文件来完成的(之所以称为点文件,是因为它们的文件名以 .
开头,例如 ~/.vimrc
。也正因为此,它们默认是隐藏文件,ls
并不会显示它们)。
shell 的配置也是通过这类文件完成的。在启动时,您的 shell 程序会读取很多文件以加载其配置项。根据 shell 本身的不同,您从登录开始还是以交互的方式完成这一过程可能会有很大的不同。关于这一话题,这里 有非常好的资源
对于 bash
来说,在大多数系统下,您可以通过编辑 .bashrc
或 .bash_profile
来进行配置。在文件中您可以添加需要在启动时执行的命令,例如上文我们讲到过的别名,或者是您的环境变量。
实际上,很多程序都要求您在 shell 的配置文件中包含一行类似 export PATH="$PATH:/path/to/program/bin"
的命令,这样才能确保这些程序能够被 shell 找到。
还有一些其他的工具也可以通过_点文件_进行配置:
bash
-~/.bashrc
,~/.bash_profile
git
-~/.gitconfig
vim
-~/.vimrc
和~/.vim
目录ssh
-~/.ssh/config
tmux
-~/.tmux.conf
我们应该如何管理这些配置文件呢,它们应该在它们的文件夹下,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。这么做有如下好处:
- 安装简单: 如果您登录了一台新的设备,在这台设备上应用您的配置只需要几分钟的时间;
- 可以执行: 您的工具在任何地方都以相同的配置工作
- 同步: 在一处更新配置文件,可以同步到其他所有地方
- 变更追踪: 您可能要在整个程序员生涯中持续维护这些配置文件,而对于长期项目而言,版本历史是非常重要的
- 配置文件的一个常见的痛点是它可能并不能在多种设备上生效。例如,如果您在不同设备上使用的操作系统或者 shell 是不同的,则配置文件是无法生效的。或者,有时您仅希望特定的配置只在某些设备上生效。
有一些技巧可以轻松达成这些目的。如果配置文件 if 语句,则您可以借助它针对不同的设备编写不同的配置。例如,您的 shell 可以这样做:
1 | if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi |
如果配置文件支持 include 功能,您也可以多加利用。例如:~/.gitconfig
可以这样编写:
1 | [include] |
然后我们可以在日常使用的设备上创建配置文件 ~/.gitconfig_local
来包含与该设备相关的特定配置。您甚至应该创建一个单独的代码仓库来管理这些与设备相关的配置。
如果您希望在不同的程序之间共享某些配置,该方法也适用。例如,如果您想要在 bash
和 zsh
中同时启用一些别名,您可以把它们写在 .aliases
里,然后在这两个 shell 里应用:
1 | # Test if ~/.aliases exists and source it |
包管理
在Ubuntu下,我们也可以在网站上下载软件包,后缀为.deb
,然后调用dpkg
进行安装:
1 | sudo dpkg -i xxx.deb |
.deb
文件后缀是Debian系统的 软件包格式,Ubuntu基于Debian开发因此使用相同的软件包格式,里面包含了程序的二进制文件、配置文件、man/info帮助页面等信息。用户 不同的任务依赖大量的软件支撑,不同的软件往往有着复杂的依赖关系,一个软件往往也有很多版本,为了管理这么多的软件,Ubuntu提供了统一的软件管理 机制,也就是dpkg
和ap![[download.gif]]t
。 dpkg
的用法如上所示,dpkg -l
可以列出所有的以安装软件,dpkg -r
卸载软件,更多命令可以使用dpkg --help
或者man dpkg
。dpkg安装完成之后,默认文件存放位置如下:
- 二进制文件:
/usr/bin
- 库文件:
/usr/lib
- 配置文件:
/etc
- 使用手册和帮助文档:
/usr/share/doc
- man帮助页面:
/usr/share/man
然而dpkg并不是万能的,当某个软件的依赖项没有安装时dpkg就会报错,需要用户手动安装依赖项。apt很好地解决了这一问题。首先开发者会将 编译后的二进制文件和软件信息存放在Ubuntu的源服务器上,当需要安装软件时,apt会自动从服务器上获取软件依赖信息,然后从服务器上下载依赖并安 装,然后再安装需要的软件。服务器的信息记录在本地的/etc/apt/sources.list
中:
shell脚本
赋值
在bash中为变量赋值的语法是foo=bar
,访问变量中存储的数值,其语法为 $foo
。 需要注意的是,foo = bar
(使用空格隔开)是不能正确工作的,因为解释器会调用程序foo
并将 =
和 bar
作为参数。 总的来说,在shell脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。
Bash中的字符串通过'
和 "
分隔符来定义,但是它们的含义并不相同。以'
定义的字符串为原义字符串,其中的变量不会被转义,而 "
定义的字符串会将变量值进行替换
参数
$0
- 脚本名$1
到$9
- 脚本的参数。$1
是第一个参数,依此类推。$@
- 所有参数$#
- 参数个数$?
- 前一个命令的返回值$$
- 当前脚本的进程识别码!!
- 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用sudo !!
再尝试一次。$_
- 上一条命令的最后一个参数。如果你正在使用的是交互式 shell,你可以通过按下Esc
之后键入 . 来获取这个值。
返回值
当您通过 $( CMD )
这样的方式来执行CMD
这个命令时,它的输出结果会替换掉 $( CMD )
。例如,如果执行 for file in $(ls)
,shell首先将调用ls
,然后遍历得到的这些返回值。还有一个冷门的类似特性是 进程替换(process substitution), <( CMD )
会执行 CMD
并将结果输出到一个临时文件中,并将 <( CMD )
替换成临时文件名。这在我们希望返回值通过文件而不是STDIN传递时很有用。例如, diff <(ls foo) <(ls bar)
会显示文件夹 foo
和 bar
中文件的区别
查找
查找文件
1 | # 查找所有名称为src的文件夹 |
1 | # 删除全部扩展名为.tmp 的文件 |
尽管 find
用途广泛,它的语法却比较难以记忆。例如,为了查找满足模式 PATTERN
的文件,您需要执行 find -name '*PATTERN*'
(如果您希望模式匹配时是不区分大小写,可以使用-iname
选项)
您当然可以使用 alias 设置别名来简化上述操作,但 shell 的哲学之一便是寻找(更好用的)替代方案。 记住,shell 最好的特性就是您只是在调用程序,因此您只要找到合适的替代程序即可(甚至自己编写)。
例如,fd
就是一个更简单、更快速、更友好的程序,它可以用来作为find
的替代品。它有很多不错的默认设置,例如输出着色、默认支持正则匹配、支持unicode并且我认为它的语法更符合直觉。以模式PATTERN
搜索的语法是 fd PATTERN
。
大多数人都认为 find
和 fd
已经很好用了,但是有的人可能想知道,我们是不是可以有更高效的方法,例如不要每次都搜索文件而是通过编译索引或建立数据库的方式来实现更加快速地搜索。
这就要靠 locate
了。 locate
使用一个由 updatedb
负责更新的数据库,在大多数系统中 updatedb
都会通过 cron
每日更新。这便需要我们在速度和时效性之间作出权衡。而且,find
和类似的工具可以通过别的属性比如文件大小、修改时间或是权限来查找文件,locate
则只能通过文件名。 这里有一个更详细的对比。
查找代码
grep
有很多选项,这也使它成为一个非常全能的工具。其中我经常使用的有 -C
:获取查找结果的上下文(Context);-v
将对结果进行反选(Invert),也就是输出不匹配的结果。举例来说, grep -C 5
会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R
会递归地进入子目录并搜索所有的文本文件。 它的替代品,包括 ack, ag 和 rg。它们都特别好用,但是功能也都差不多,我比较常用的是 ripgrep (rg
) ,因为它速度快,而且用法非常符合直觉。例子如下:****
1 | # 查找所有使用了 requests 库的文件 |
使用建议
建立别名
输入一长串包含许多选项的命令会非常麻烦。因此,大多数 shell 都支持设置别名。shell 的别名相当于一个长命令的缩写,shell 会自动将其替换成原本的命令。例如,bash 中的别名语法如下:
1 | alias alias_name="command_to_alias arg1 arg2" |
注意, =
两边是没有空格的,因为 alias
是一个 shell 命令,它只接受一个参数。
别名有许多很方便的特性:
1 | # 创建常用命令的缩写 |
值得注意的是,在默认情况下 shell 并不会保存别名。为了让别名持续生效,您需要将配置放进 shell 的启动文件里,像是.bashrc
或 .zshrc
,
tips
右击边框开启设置菜单 history
命令允许您以程序员的方式来访问shell中输入的历史命令。这个命令会在标准输出中打印shell中的里面命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep
进行模式搜索。 history | grep find
会打印包含find子串的命令。
对于大多数的shell来说,您可以使用 Ctrl+R
对命令历史记录进行回溯搜索。敲 Ctrl+R
后您可以输入子串来进行匹配,查找历史命令行。
反复按下就会在所有搜索结果中循环。在 zsh 中,使用方向键上或下也可以完成这项工作。
Ctrl+R
可以配合 fzf 使用。fzf
是一个通用对模糊查找工具,它可以和很多命令一起使用。这里我们可以对历史命令进行模糊查找并将结果以赏心悦目的格式输出。
另外一个和历史命令相关的技巧我喜欢称之为基于历史的自动补全。 这一特性最初是由 fish shell 创建的,它可以根据您最近使用过的开头相同的命令,动态地对当前对shell命令进行补全。这一功能在 zsh 中也可以使用,它可以极大的提高用户体验。
你可以修改 shell history 的行为,例如,如果在命令的开头加上一个空格,它就不会被加进shell记录中。当你输入包含密码或是其他敏感信息的命令时会用到这一特性。 为此你需要在.bashrc
中添加HISTCONTROL=ignorespace
或者向.zshrc
添加 setopt HIST_IGNORE_SPACE
。 如果你不小心忘了在前面加空格,可以通过编辑。bash_history
或 .zhistory
来手动地从历史记录中移除那一项。 fasd
和 autojump 这两个工具来查找最常用或最近使用的文件和目录。
Fasd 基于 frecency 对文件和文件排序,也就是说它会同时针对频率(frequency)和时效(recency)进行排序。默认情况下,fasd
使用命令 z
帮助我们快速切换到最常访问的目录。例如, 如果您经常访问/home/user/files/cool_project
目录,那么可以直接使用 z cool
跳转到该目录。对于 autojump,则使用j cool
代替即可。
还有一些更复杂的工具可以用来概览目录结构,例如 tree
, broot
或更加完整的文件管理器,例如 nnn
或 ranger
。
命令行环境
默认环境变量
因为Shell中定义的环境变量之后影响子进程,为了避免我们每次都需要在使用前定义环境变量,我们可以将这一设置写进~/.bashrc
里,这个配置文件会在每次启动bash时执行。~/.bashrc
中已经包含了很多配置,我们要做的就是在最后添加
1 | export HELLO=hello |
然后执行source ~/.bashrc
,这样就可以在每次打开Shell的时候定义这一环境变量。利用这个机制,假如我们想把自定义的命令加入PATH
中时就可以在~/.bashrc
中添加
1 | export PATH=/path/to/bin:$PATH |
这样命令就会添加到Shell的搜索路径中。需要注意的一点是,如果使用的是zsh,需要修改的就不是~/.bashrc
,而是~/.zshrc
端口转发
很多情况下我们都会遇到软件需要监听特定设备的端口。如果是在您的本机,可以使用 localhost:PORT
或 127.0.0.1:PORT
。但是如果需要监听远程服务器的端口该如何操作呢?这种情况下远端的端口并不会直接通过网络暴露给您。
此时就需要进行 端口转发。端口转发有两种,一种是本地端口转发和远程端口转发(参见下图,该图片引用自这篇StackOverflow 文章)中的图片。
本地端口转发
远程端口转发
常见的情景是使用本地端口转发,即远端设备上的服务监听一个端口,而您希望在本地设备上的一个端口建立连接并转发到远程端口上。例如,我们在远端服务器上运行 Jupyter notebook 并监听 8888
端口。 然后,建立从本地端口 9999
的转发,使用 ssh -L 9999:localhost:8888 foobar@remote_server
。这样只需要访问本地的 localhost:9999
即可。
信号机制
您的 shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。就这一点而言,信号是一种_软件中断_。
在上面的例子中,当我们输入 Ctrl-C
时,shell 会发送一个SIGINT
信号到进程。
下面这个 Python 程序向您展示了捕获信号SIGINT
并忽略它的基本操作,它并不会让程序停止。为了停止这个程序,我们需要使用SIGQUIT
信号,通过输入Ctrl-\
可以发送该信号。 尽管 SIGINT
和 SIGQUIT
都常常用来发出和终止程序相关的请求。SIGTERM
则是一个更加通用的、也更加优雅地退出信号。为了发出这个信号我们需要使用 kill
命令, 它的语法是: kill -TERM <PID>
。 信号可以让进程做其他的事情,而不仅仅是终止它们。例如,SIGSTOP
会让进程暂停。在终端中,键入 Ctrl-Z
会让 shell 发送 SIGTSTP
信号,SIGTSTP
是 Terminal Stop 的缩写(即terminal
版本的SIGSTOP)。
我们可以使用 fg
或 bg
命令恢复暂停的工作。它们分别表示在前台继续或在后台继续。
jobs
命令会列出当前终端会话中尚未完成的全部任务。您可以使用 pid 引用这些任务(也可以用 pgrep
找出 pid)。更加符合直觉的操作是您可以使用百分号 + 任务编号(jobs
会打印任务编号)来选取该任务。如果要选择最近的一个任务,可以使用 $!
这一特殊参数。
还有一件事情需要掌握,那就是命令中的 &
后缀可以让命令在直接在后台运行,这使得您可以直接在 shell 中继续做其他操作,不过它此时还是会使用 shell 的标准输出,这一点有时会比较恼人(这种情况可以使用 shell 重定向处理)。
让已经在运行的进程转到后台运行,您可以键入Ctrl-Z
,然后紧接着再输入bg
。注意,后台的进程仍然是您的终端进程的子进程,一旦您关闭终端(会发送另外一个信号SIGHUP
),这些后台的进程也会终止。为了防止这种情况发生,您可以使用 nohup
(一个用来忽略 SIGHUP
的封装) 来运行程序。针对已经运行的程序,可以使用disown
。除此之外,您可以使用终端多路复用器来实现。
1 | $ sleep 1000 |
SIGKILL
是一个特殊的信号,它不能被进程捕获并且它会马上结束该进程。不过这样做会有一些副作用,例如留下孤儿进程。
tmux多路终端复用
当您在使用命令行时,您通常会希望同时执行多个任务。举例来说,您可以想要同时运行您的编辑器,并在终端的另外一侧执行程序。尽管再打开一个新的终端窗口也能达到目的,使用终端多路复用器则是一种更好的办法。
像 tmux
这类的终端多路复用器可以允许我们基于面板和标签分割出多个终端窗口,这样您便可以同时与多个 shell 会话进行交互。
不仅如此,终端多路复用使我们可以分离当前终端会话并在将来重新连接。
这让您操作远端设备时的工作流大大改善,避免了 nohup
和其他类似技巧的使用。
现在最流行的终端多路器是 tmux
。tmux
是一个高度可定制的工具,您可以使用相关快捷键创建多个标签页并在它们间导航。
tmux
的快捷键需要我们掌握,它们都是类似 <C-b> x
这样的组合,即需要先按下Ctrl+b
,松开后再按下 x
。tmux
中对象的继承结构如下:
- 会话 - 每个会话都是一个独立的工作区,其中包含一个或多个窗口
tmux
开始一个新的会话tmux new -s NAME
以指定名称开始一个新的会话tmux ls
列出当前所有会话- 在
tmux
中输入<C-b> d
,将当前会话分离 tmux a
重新连接最后一个会话。您也可以通过-t
来指定具体的会话
- 窗口 - 相当于编辑器或是浏览器中的标签页,从视觉上将一个会话分割为多个部分
<C-b> c
创建一个新的窗口,使用<C-d>
关闭<C-b> N
跳转到第 N 个窗口,注意每个窗口都是有编号的<C-b> p
切换到前一个窗口<C-b> n
切换到下一个窗口<C-b> ,
重命名当前窗口<C-b> w
列出当前所有窗口
- 面板 - 像 vim 中的分屏一样,面板使我们可以在一个屏幕里显示多个 shell
<C-b> "
水平分割<C-b> %
垂直分割<C-b> <方向>
切换到指定方向的面板,<方向> 指的是键盘上的方向键<C-b> z
切换当前面板的缩放<C-b> [
开始往回卷动屏幕。您可以按下空格键来开始选择,回车键复制选中的部分<C-b> <空格>
在不同的面板排布间切换
ssh
通过如下命令,您可以使用 ssh
连接到其他服务器:
1 | 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
命令可以生成一对密钥:
1 | ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519 |
您可以为密钥设置密码,防止有人持有您的私钥并使用它访问您的服务器。您可以使用 ssh-agent
或 gpg-agent
,这样就不需要每次都输入该密码了。
如果您曾经配置过使用 SSH 密钥推送到 GitHub,那么可能您已经完成了这里 介绍的这些步骤,并且已经有了一个可用的密钥对。要检查您是否持有密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key
.
基于密钥的认证机制
使用 ssh-keygen
命令可以生成一对密钥:
1 | ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519 |
您可以为密钥设置密码,防止有人持有您的私钥并使用它访问您的服务器。您可以使用 ssh-agent
或 gpg-agent
,这样就不需要每次都输入该密码了。
如果您曾经配置过使用 SSH 密钥推送到 GitHub,那么可能您已经完成了这里 介绍的这些步骤,并且已经有了一个可用的密钥对。要检查您是否持有密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key
.
G# 基于密钥的认证机制
ssh
会查询 .ssh/authorized_keys
来确认那些用户可以被允许登录。您可以通过下面的命令将一个公钥拷贝到这里:
1 | cat .ssh/id_ed25519 | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys' |
如果支持 ssh-copy-id
的话,可以使用下面这种更简单的解决方案:
1 | ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote |
文本匹配
- 当你想要利用通配符进行匹配时,你可以分别使用
?
和*
来匹配一个或任意个字符。例如,对于文件foo
,foo1
,foo2
,foo10
和bar
,rm foo?
这条命令会删除foo1
和foo2
,而rm foo*
则会删除除了bar
之外的所有文件。 - 花括号
{}
- 当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。 - [[Pasted image 20230128155221.png]]
正则表达式
正则表达式通常以(尽管并不总是) /
开始和结束。大多数的 ASCII 字符都表示它们本来的含义,但是有一些字符确实具有表示匹配行为的“特殊”含义。不同字符所表示的含义,根据正则表达式的实现方式不同,也会有所变化,这一点确实令人沮丧。常见的模式有:
.
除换行符之外的”任意单个字符”*
匹配前面字符零次或多次+
匹配前面字符一次或多次[abc]
匹配a
,b
和c
中的任意一个(RX1|RX2)
任何能够匹配RX1
或RX2
的结果^
行首$
行尾
awk
擅长文本处理的编程语言 {print $2}
的作用是什么? awk
程序接受一个模式串(可选),以及一个代码块,指定当模式匹配时应该做何种操作。默认当模式串即匹配所有行(上面命令中当用法)。 在代码块中,$0
表示整行的内容,$1
到 $n
为一行中的 n 个区域,区域的分割基于 awk
的域分隔符(默认是空格,可以通过-F
来修改)。在这个例子中,我们的代码意思是:对于每一行文本,打印其第二个部分,也就是用户名 让我们统计一下所有以c
开头,以 e
结尾,并且仅尝试过一次登录的用户。
1 | | awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l |
让我们好好分析一下。首先,注意这次我们为 awk
指定了一个匹配模式串(也就是{...}
前面的那部分内容)。该匹配要求文本的第一部分需要等于1(这部分刚好是uniq -c
得到的计数值),然后其第二部分必须满足给定的一个正则表达式。代码块中的内容则表示打印用户名。然后我们使用 wc -l
统计输出结果的行数。
不过,既然 awk
是一种编程语言,那么则可以这样:
1 | BEGIN { rows = 0 } |
BEGIN
也是一种模式,它会匹配输入的开头( END
则匹配结尾)。然后,对每一行第一个部分进行累加,最后将结果输出。
守护进程
即便守护进程(daemon)这个词看上去有些陌生,你应该已经大约明白它的概念。大部分计算机都有一系列在后台保持运行,不需要用户手动运行或者交互的进程。这些进程就是守护进程。以守护进程运行的程序名一般以 d
结尾,比如 SSH 服务端 sshd
,用来监听传入的 SSH 连接请求并对用户进行鉴权。
Linux 中的 systemd
(the system daemon)是最常用的配置和运行守护进程的方法。运行 systemctl status
命令可以看到正在运行的所有守护进程。这里面有很多可能你没有见过,但是掌管了系统的核心部分的进程:管理网络、DNS解析、显示系统的图形界面等等。用户使用 systemctl
命令和 systemd
交互来enable
(启用)、disable
(禁用)、start
(启动)、stop
(停止)、restart
(重启)、或者status
(检查)配置好的守护进程及系统服务。
systemd
提供了一个很方便的界面用于配置和启用新的守护进程或系统服务。下面的配置文件使用了守护进程来运行一个简单的 Python 程序。文件的内容非常直接所以我们不对它详细阐述。systemd
配置文件的详细指南可参见 freedesktop.org。
1 | # /etc/systemd/system/myapp.service |
备份
任何没有备份的数据都可能在一个瞬间永远消失。复制数据很简单,但是可靠地备份数据很难。下面列举了一些关于备份的基础知识,以及一些常见做法容易掉进的陷阱。
首先,复制存储在同一个磁盘上的数据不是备份,因为这个磁盘是一个单点故障(single point of failure)。这个磁盘一旦出现问题,所有的数据都可能丢失。放在家里的外置磁盘因为火灾、抢劫等原因可能会和源数据一起丢失,所以是一个弱备份。推荐的做法是将数据备份到不同的地点存储。
同步方案也不是备份。即使方便如 Dropbox 或者 Google Drive,当数据在本地被抹除或者损坏,同步方案可能会把这些“更改”同步到云端。同理,像 RAID 这样的磁盘镜像方案也不是备份。它不能防止文件被意外删除、损坏、或者被勒索软件加密。
有效备份方案的几个核心特性是:版本控制,删除重复数据,以及安全性。对备份的数据实施版本控制保证了用户可以从任何记录过的历史版本中恢复数据。在备份中检测并删除重复数据,使其仅备份增量变化可以减少存储开销。在安全性方面,作为用户,你应该考虑别人需要有什么信息或者工具才可以访问或者完全删除你的数据及备份。最后一点,不要盲目信任备份方案。用户应该经常检查备份是否可以用来恢复数据。
备份不限制于备份在本地计算机上的文件。云端应用的重大发展使得我们很多的数据只存储在云端。当我们无法登录这些应用,在云端存储的网络邮件,社交网络上的照片,流媒体音乐播放列表,以及在线文档等等都会随之丢失。用户应该有这些数据的离线备份,而且已经有项目可以帮助下载并存储它们。
常见命令行标志参数及模式
命令行工具的用法千差万别,阅读 man
页面可以帮助你理解每种工具的用法。即便如此,下面我们将介绍一下命令行工具一些常见的共同功能。
大部分工具支持
--help
或者类似的标志参数(flag)来显示它们的简略用法。会造成不可撤回操作的工具一般会提供“空运行”(dry run)标志参数,这样用户可以确认工具真实运行时会进行的操作。这些工具通常也会有“交互式”(interactive)标志参数,在执行每个不可撤回的操作前提示用户确认。
--version
或者-V
标志参数可以让工具显示它的版本信息(对于提交软件问题报告非常重要)。基本所有的工具支持使用
--verbose
或者-v
标志参数来输出详细的运行信息。多次使用这个标志参数,比如-vvv
,可以让工具输出更详细的信息(经常用于调试)。同样,很多工具支持--quiet
标志参数来抑制除错误提示之外的其他输出。大多数工具中,使用
-
代替输入或者输出文件名意味着工具将从标准输入(standard input)获取所需内容,或者向标准输出(standard output)输出结果。会造成破坏性结果的工具一般默认进行非递归的操作,但是支持使用“递归”(recursive)标志函数(通常是
-r
)。有的时候你可能需要向工具传入一个 看上去 像标志参数的普通参数,比如:
使用
rm
删除一个叫-r
的文件;在通过一个程序运行另一个程序的时候(
ssh machine foo
),向内层的程序(foo
)传递一个标志参数。这时候你可以使用特殊参数
--
让某个程序 停止处理--
后面出现的标志参数以及选项(以-
开头的内容):rm -- -r
会让rm
将-r
当作文件名;ssh machine --for-ssh -- foo --for-foo
的--
会让ssh
知道--for-foo
不是ssh
的标志参数。
代码调试
事件分析
在我们使用strace
调试代码的时候,您可能会希望忽略一些特殊的代码并希望在分析时将其当作黑盒处理。perf
命令将 CPU 的区别进行了抽象,它不会报告时间和内存的消耗,而是报告与您的程序相关的系统事件。
例如,perf
可以报告不佳的缓存局部性(poor cache locality)、大量的页错误(page faults)或活锁(livelocks)。下面是关于常见命令的简介:
perf list
- 列出可以被 pref 追踪的事件;perf stat COMMAND ARG1 ARG2
- 收集与某个进程或指令相关的事件;perf record COMMAND ARG1 ARG2
- 记录命令执行的采样信息并将统计数据储存在perf.data
中;perf report
- 格式化并打印perf.data
中的数据。
资源监控
有时候,分析程序性能的第一步是搞清楚它所消耗的资源。程序变慢通常是因为它所需要的资源不够了。例如,没有足够的内存或者网络连接变慢的时候。
有很多很多的工具可以被用来显示不同的系统资源,例如 CPU 占用、内存使用、网络、磁盘使用等。
- 通用监控 - 最流行的工具要数
htop
,了,它是top
的改进版。htop
可以显示当前运行进程的多种统计信息。htop
有很多选项和快捷键,常见的有:<F6>
进程排序、t
显示树状结构和h
打开或折叠线程。 还可以留意一下glances
,它的实现类似但是用户界面更好。如果需要合并测量全部的进程,dstat
是也是一个非常好用的工具,它可以实时地计算不同子系统资源的度量数据,例如 I/O、网络、 CPU 利用率、上下文切换等等; - I/O 操作 -
iotop
可以显示实时 I/O 占用信息而且可以非常方便地检查某个进程是否正在执行大量的磁盘读写操作; - 磁盘使用 -
df
可以显示每个分区的信息,而du
则可以显示当前目录下每个文件的磁盘使用情况( disk usage)。-h
选项可以使命令以对人类(human)更加友好的格式显示数据;ncdu
是一个交互性更好的du
,它可以让您在不同目录下导航、删除文件和文件夹; - 内存使用 -
free
可以显示系统当前空闲的内存。内存也可以使用htop
这样的工具来显示; - 打开文件 -
lsof
可以列出被进程打开的文件信息。 当我们需要查看某个文件是被哪个进程打开的时候,这个命令非常有用; - 网络连接和配置 -
ss
能帮助我们监控网络包的收发情况以及网络接口的显示信息。ss
常见的一个使用场景是找到端口被进程占用的信息。如果要显示路由、网络设备和接口信息,您可以使用ip
命令。注意,netstat
和ifconfig
这两个命令已经被前面那些工具所代替了。 - 网络使用 -
nethogs
和iftop
是非常好的用于对网络占用进行监控的交互式命令行工具。
如果您希望测试一下这些工具,您可以使用 stress
命令来为系统人为地增加负载。
QA
source script.sh
和 ./script.sh
有什么区别?
这两种情况 script.sh
都会在bash会话中被读取和执行,不同点在于哪个会话执行这个命令。 对于 source
命令来说,命令是在当前的bash会话中执行的,因此当 source
执行完毕,对当前环境的任何更改(例如更改目录或是定义函数)都会留存在当前会话中。 单独运行 ./script.sh
时,当前的bash会话将启动新的bash会话(实例),并在新实例中运行命令 script.sh
。 因此,如果 script.sh
更改目录,新的bash会话(实例)会更改目录,但是一旦退出并将控制权返回给父bash会话,父会话仍然留在先前的位置(不会有目录的更改)。 同样,如果 script.sh
定义了要在终端中访问的函数,需要用 source
命令在当前bash会话中定义这个函数。否则,如果你运行 ./script.sh
,只有新的bash会话(进程)才能执行定义的函数,而当前的shell不能。
各种软件包和工具存储在哪里?引用过程是怎样的? /bin
或 /lib
是什么?
根据你在命令行中运行的程序,这些包和工具会全部在 PATH
环境变量所列出的目录中查找到, 你可以使用 which
命令(或是 type
命令)来检查你的shell在哪里发现了特定的程序。 一般来说,特定种类的文件存储有一定的规范,文件系统,层次结构标准(Filesystem, Hierarchy Standard)可以查到我们讨论内容的详细列表。
/bin
- 基本命令二进制文件/sbin
- 基本的系统二进制文件,通常是root运行的/dev
- 设备文件,通常是硬件设备接口文件/etc
- 主机特定的系统配置文件/home
- 系统用户的主目录/lib
- 系统软件通用库/opt
- 可选的应用软件/sys
- 包含系统的信息和配置(第一堂课介绍的)/tmp
- 临时文件(/var/tmp
) 通常重启时删除/usr/
- 只读的用户数据/usr/bin
- 非必须的命令二进制文件/usr/sbin
- 非必须的系统二进制文件,通常是由root运行的/usr/local/bin
- 用户编译程序的二进制文件
/var
-变量文件 像日志或缓存
我应该用 apt-get install
还是 pip install
去下载软件包呢?
这个问题没有普遍的答案。这与使用系统程序包管理器还是特定语言的程序包管理器来安装软件这一更笼统的问题相关。需要考虑的几件事:
常见的软件包都可以通过这两种方法获得,但是小众的软件包或较新的软件包可能不在系统程序包管理器中。在这种情况下,使用特定语言的程序包管理器是更好的选择。
同样,特定语言的程序包管理器相比系统程序包管理器有更多的最新版本的程序包。
当使用系统软件包管理器时,将在系统范围内安装库。如果出于开发目的需要不同版本的库,则系统软件包管理器可能不能满足你的需要。对于这种情况,大多数编程语言都提供了隔离或虚拟环境,因此你可以用特定语言的程序包管理器安装不同版本的库而不会发生冲突。对于 Python,可以使用 virtualenv,对于 Ruby,使用 RVM 。
根据操作系统和硬件架构,其中一些软件包可能会附带二进制文件或者软件包需要被编译。例如,在树莓派(Raspberry Pi)之类的ARM架构计算机中,在软件附带二进制文件和软件包需要被编译的情况下,使用系统包管理器比特定语言包管理器更好。这在很大程度上取决于你的特定设置。 你应该仅使用一种解决方案,而不同时使用两种方法,因为这可能会导致难以解决的冲突。我们的建议是尽可能使用特定语言的程序包管理器,并使用隔离的环境(例如 Python 的 virtualenv)以避免影响全局环境
Docker和虚拟机有什么区别?
Docker 基于容器这个更为概括的概念。关于容器和虚拟机之间最大的不同是,虚拟机会执行整个的 OS 栈,包括内核(即使这个内核和主机内核相同)。与虚拟机不同,容器避免运行其他内核实例,而是与主机分享内核。在Linux环境中,有LXC机制来实现,并且这能使一系列分离的主机像是在使用自己的硬件启动程序,而实际上是共享主机的硬件和内核。因此容器的开销小于完整的虚拟机。
另一方面,容器的隔离性较弱而且只有在主机运行相同的内核时才能正常工作。例如,如果你在macOS 上运行 Docker,Docker 需要启动 Linux虚拟机去获取初始的 Linux内核,这样的开销仍然很大。最后,Docker 是容器的特定实现,它是为软件部署而定制的。基于这些,它有一些奇怪之处:例如,默认情况下,Docker 容器在重启之间不会有以任何形式的存储。
还有更多的 Vim 小窍门吗?
更多的窍门:
- 插件 - 花时间去探索插件。有很多不错的插件修复了vim的缺陷或者增加了能够与现有vim工作流结合的新功能。关于这部分内容,资源是VimAwesome 和其他程序员的dotfiles。
- 标记 - 在vim里你可以使用
m<X>
为字母X
做标记,之后你可以通过'<X>
回到标记位置。这可以让你快速定位到文件内或文件间的特定位置。 - 导航 -
Ctrl+O
和Ctrl+I
命令可以使你在最近访问位置前后移动。 - 撤销树 - vim 有不错的更改跟踪机制,不同于其他的编辑器,vim存储变更树,因此即使你撤销后做了一些修改,你仍然可以通过撤销树的导航回到初始状态。一些插件比如 gundo.vim 和 undotree 通过图形化来展示撤销树。
- 时间撤销 -
:earlier
和:later
命令使得你可以用时间而非某一时刻的更改来定位文件。 - 持续撤销 - 是一个默认未被开启的vim的内置功能,它在vim启动之间保存撤销历史,需要配置在
.vimrc
目录下的undofile
和undodir
,vim会保存每个文件的修改历史。 - 热键(Leader Key) - 热键是一个用于用户自定义配置命令的特殊按键。这种模式通常是按下后释放这个按键(通常是空格键)并与其他的按键组合去实现一个特殊的命令。插件也会用这些按键增加它们的功能,例如,插件UndoTree使用
<Leader> U
去打开撤销树。 - 高级文本对象 - 文本对象比如搜索也可以用vim命令构成。例如,
d/<pattern>
会删除下一处匹配 pattern 的字符串,cgn
可以用于更改上次搜索的关键字。