计算机基础
发布于 2026-04-06更新于 2026-11-1428902 字178 分钟 · 阅读时间
shell 是一个为用户提供操作系统服务的界面,包括:
命令行界面(CLI, Command Line Interface)Shell
Bash PowerShell 等软件图形用户界面(GUI, Graphical User Interface)Shell
Windows桌面环境 GNOME KDE 等软件从技术上来说,Shell 本身是一种命令解释器,它接受用户输入的命令,解析这些命令,并调用系统内核中的相应功能来完成任务。不同的 Shell 只是用不同的方式(命令行界面或图形界面)来提供与操作系统交互的体验。
以下讲的 shell 大多是 C 命令行界面
如输入echo 'hello world',事实上是通过 shell 调用了一个叫 echo 的程序,然后通过空格区分参数,向它传递了一个叫hello world的参数,然后 echo 软件在 shell 界面中显示出其输出。当然也可以使用双引号或者转义字符\来表示空格
echo $PATH $HOME 输出这两个环境变量的值
which ls 通过 which 软件查找$PATH 变量中所包含的目录中是否有软件ls
这些软件在命令行页面中又称命令
但像是 cd 等是 shell 内置命令,无法通过 which 查找到
.是当前目录,..是父目录
cd ~ 切换回主目录(/home/elric/),cd - 切换回上一个目录
ls -l的各种权限
[文件类型]*1[拥有者权限]*3[所属组权限]*3[其他用户权限]*3-rwxr-xr---:普通文件。 r:读权限。d:目录。 w:写权限。l:符号链接(symlink)。 x:执行权限c:字符设备文件(如终端设备)。 -:没有权限。b:块设备文件(如硬盘)。 s:setuid/setgid 权限。s:套接字(socket)。 t:粘滞位(sticky bit)p:命名管道(FIFO)rm -r 可以递归删除目录
Ctrl+L 清除终端并回到顶部
一些命令的英文:
cd:change directory 改变目录
pwd:present working directory (打印)当前工作目录
ls:list 列表 (列出当前目录的文件)
mv:move 移动 (或重命名文件)
cp:copy 复制 (文件或目录)
rm:remove 删除 (文件)
rmdir:remove directory 删除(空)目录
mkdir:make directory 创建目录
man:manual 手册
touch: touch 触碰(没有文件就先创建在触碰;含有文件就触碰以更新触碰时间戳)
chomd:change mode 改变模式(改变各种用户对文件的权限)
env:environment 环境(显示所有环境变量或让命令在特定环境下运行)
比如让程序与文件交互,让文件在各种程序中传输。
流(Streams)是指数据在命令或进程之间传递的通道,在 Shell 中有三个标准的流:
标准输入(stdin):编号 0,通常从键盘或文件中读取输入。
默认连接到键盘输入,也可从重定向为文件或其他数据流作为输入
# 从`input_file`中读取内容到某命令[某命令] < input_file标准输出(stdout):编号 1,用于输出正常的信息,通常显示在终端屏幕上。
默认连接到当前终端,也可重定向到文件或发送给另一个命令
[某命令] > output_file标准错误(stderr):编号 2,用于输出错误信息,通常也显示在终端屏幕上。
这是专门蔬菜错误消息的流,默认也连接到当前终端,也可重定向到文件或合并到 stdout 中等
[某命令] 2> error_file重定向与管道(Pipes) 用于处理流
重定向符号:
<:输入重定向,将文件内容作为 stdin 输入。
ls > file_list.txt>:输出重定向,将 stdout 重定向到文件中。如果文件存在,会覆盖该文件。
echo "new line" >> file_list.txt>>:追加输出重定向,将 stdout 追加到文件末尾,而不是覆盖。
wc -l < file_list.txt2>:错误输出重定向,将 stderr 重定向到文件。
ls non_existing_file 2> error.log2>&1:将 stderr 重定向到 stdout,即将标准错误输出和标准输出合并。
command > all_output.log 2>&1管道(Pipes):
管道(|)允许你将一个命令的标准输出(stdout)直接作为另一个命令的标准输入(stdin),使多个命令串联起来处理数据
[命令1] | [命令2]ls | grep "file" # 将 ls 的输出传递给 grep,筛选包含 "file" 的文件。find /path -name "*.txt" > output.txt 2> error.log# stdout(找到的结果)被重定向到 output.txt 文件# stderr(错误信息)被重定向到 error.log 文件tee 命令常与管道符连用(T 为 T 形管道(分流))
tee 用于将输入的数据同时输出到终端和文件
Bashls -l | tee -a output.txtls -l | tee output.txt | grep "pattern"
一些英文单词:
standard input stream 标准输入流
output stream 输出流
error stream 错误流
cat:catch 抓取(打印某文件的数据到 stdout)
通过 sudo 命令来提权
sudo su 将当前终端重定向一个以 root 用户打开的终端,使用 exit 退出
sys 中放的是内核
一些英文单词:
shell 具体的笔记在下面
赋值时不要带空格,不然他会把第一个变量当作程序命令来执行
elric@PC:/$ foo=barelric@PC:/$ echo $foobarelric@PC:/$ foo = bar找不到命令 “foo”,您的意思是: “xxx” 命令来自 xxx 软件包 xxx (2.0.3)elric@PC:/$可以使用$()来获取上一个命令的输出
elric@PC:/mnt/c$ foo=$(pwd) # “$()” 用于将小括号的内容替换到一个字符串里面elric@PC:/mnt/c$ echo $foo/mnt/celric@PC:/mnt/c$ fooo=$(echo "hello")elric@PC:/mnt/c$ echo $fooohello小括号可以用于替换
diff <(ls) <(ls ..) # 这里的“<()” 用于将小括号里的内容替换到一个临时文件里使用双引号来设置字符串会将变量变为字面量
使用单引号来设置字符串不会将变量改变
elric@PC:/$ echo "Value is $foo"Value is barelric@PC:/$ echo 'Value is $foo'Value is $foo elric@PC:/mnt/c$ echo "we are in $(pwd)"we are in /mnt/celric@PC:/mnt/c$ echo "we are in $foo"we are in /mnt/cshell 脚本(.sh)也可以编写函数
mch(){ mkdir -p "$1" cd "$1"} # 运行:mch 文件夹1# 执行:新建文件夹1,然后进入它通过 source 命令立刻执行某个文件中的命令
bash 中“$0、1......?、$_"等都是有已经被占用的变量
mrdik "文件夹1" ”文件夹2" # 新建了文件夹1与文件夹2cd $_ # $_用于获取上一个命令最后一个参数 # $0获取的是上一个命令名,如'cd'# $1获取第一个参数...一直到$9,之后的要用${10}、${11}等等# $@是获取所有变量,如使用在for中# $# 参数数量# $$ 进程PID(Process ID) # ⬆上面的是用于脚本终端位置参数,而不是变量,如`echo a b c`就无法使用$1 # $?获取的是上一个命令的错误代码# $_获取的是上一个命令的最后一个参数 # 它是一个变量,只是每次都会获取最后一个参数而已# !!获取的是上一个命令本身,如 “sudo !!” = “sudo cd $_”echo "hello"echo $? # 正常结束,返回和C语言一样的00 apt update # 权限不够,返回错误echo $?100逻辑运算符:&& || ! 和 C 语言一样,也是一个一个判断
elric@PC:/$ false || echo "hello" # 当第一个不成立才判断第二个helloelric@PC:/$ true || echo "hello"elric@PC:/$ true && echo "hello" # 两个都判断hello用于执行自动化任务,与 shell 交互的程序都是脚本,像是 Bash 脚本,Python 脚本
# CLI脚本#!/bin/bashecho "Hello, World!"# GUI脚本使用 AutoHotkey 来自动点击某个按钮 Click, 100, 200 ; 模拟鼠标点击坐标 (100, 200)例子 1
#!/bin/bash # 假设程序名字为"new_foobar.sh" # #!用于告诉系统这是一个脚本,要用特殊的解释器来运行,要用“/bin”的“bash”来解释与执行脚本中的命令, # 像是Python火Perl可以改为“#!usr/bin/python3”或“#!/usr/bin/perl”之类的 echo "程序运行时间为 “ $(date) ”" # 获取了date的命令的输出 echo "程序 $0 有 $# 个参数,进程PID为 $$" for file in "$@": do # 循环进行“file=$(1、2、3)”, do以开始循环 grep foobar "$file" > /dev/null 2> /dev/null # 检查文件中是否有“foobar”,然后忽略所有输出, # 也可以改为“grep -q foobar "$file" 2>/dev/null”来取消标准输出, # 但没上面那个直观(-q只能取消标准输出) if [[ "$?" -ne 0 ]]; then # 只使用一个[的“[ 条件 ](必须有空格分隔)”是传统模式,不支持&&、||、==等更现代的逻辑运算符, # 两个[[是bash提供的扩展,可以只能处理字符串与空格呀等等, # then为这个语句的一部分(then:然后,那么,接下来) echo "文件 $file 没有找到任何 “foobar” ,因此已添加一个" echo "# foobar" >> "$file" else echo "文件 $file 存在 “foobar”" fi # 是if的逆序表示 end if 结束if判断 done # done:已完成,表示结束循环,与do一起作为循环的大括号# 如下使用# 注意要先用chmod给它运行权限./new_foobar.sh 文件1.sh 文件2.py 文件3.cc new_foobar.sh通配符(wildcard)或文件名匹配模式
通配符用于匹配文件名和路径,是 shell 中常用的模式匹配工具。常见的通配符包括:
*:匹配零个或多个任意字符
?:匹配单个任意字符。
[ ]:匹配指定范围内的任意一个字符。
{ , }:替换不同项,如main.{exe,cpp}=main.exe main.cpp
{x..y}:按照 ASCII 字符集生成 x 到 y 的所有字符
通配符的功能相对简单,只能用于文件名匹配,而非复杂的文本模式匹配。
像是
ls等支持
elric@PC:/mnt/d/elric/Code/Test file$ ls *.cpp'C++ calc.cpp' 'C++ main.cpp' 'C++ test2.cpp' 'C++ test.cpp' elric@PC:/mnt/d/elric/Code/Test file$ ls ?++????????? # 或 ls *++*'C++ calc.cpp' 'C++ calc.exe' 'C++ main.cpp' 'C++ main.exe' 'C++ test.cpp' 'C++ test.exe' elric@PC:/mnt/d/elric/Code/Test file$ echo C++\ {,main,test}.{exe,cpp}C++ .exe C++ .cpp C++ main.exe C++ main.cpp C++ test.exe C++ test.cpp elric@PC:/mnt/d/elric/Code/Test file$ touch foo/{a..j}# 在文件夹foo中生成10个文件正则表达式(Regular Expression)
正则表达式是用于文本模式匹配的一种强大工具,通常用于搜索、替换或验证复杂的字符串模式。它比通配符更灵活和复杂,支持更多的模式符号和规则。
^a.*b$:匹配以 a 开头,以 b 结尾,中间可以有任意字符的字符串。
[0-9]{3}:匹配连续的三位数字。
\w+@\w+\.\w+:匹配电子邮件地址格式。
在命令行中,一些工具支持正则表达式,例如
grep、sed、awk等
用于告诉 shell 脚本的解释器是哪个,其中也可以写参数,如#! /usr/bin/env python 用 env 命令的功能来“获取 python 的位置并使用 python 解释”
find:相比与 ls 更强大的搜索工具,可以递归的搜索,且有更多的参数
locate:用于建立搜索索引,快速搜索的工具
cron:一个用于定期运行各种任务、脚本的工具
Ctrl+R:反向查找/搜索历史命令,按下→可以将命令上屏并退出,按下Enter可以退出
fzf:模糊搜索,安装后默认会绑定Ctrl+R
tree:树状递归的打印当前目录的结构
tldr:更简洁的 man 命令
zsh-autosuggestions 插件:Zsh Shell的插件。自动匹配命令的前缀,按下右方向键上屏,Oh-My-Zsh、Fish Shell、VSCode等都有这个功能
图形化的编辑器还是 VSCode ,永远的神
NOTE
待补充
数据整理包括但不限于清理、分类(排序)、组织(合并、去重)、转换(格式)等,以方便处理
这里的数据整理指:使用或编写各种命令行工具,包括但不限于自带的
curl、sort、grep等,还是使用python编写的脚本,以方便解放双手、减少重复性工作在这里叫”数据整理“比如:之前我在手机上一个个点击来保存微博美图。然后我改成通过浏览器脚本自动大量下载那些微博,之后又用命令行语言自动将图片转移到特殊的文件夹。解放了双手,只用等待完成即可。
NOTE
待补充
sed
sort 与 uniq
uniq:unique 唯一的,独特的
grep
curl
awk?要学吗
R语言
等
您的 shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。就这一点而言,信号是一种 软件中断。
在上面的例子中,当我们输入 Ctrl-C 时,shell 会发送一个 SIGINT 信号到进程。
下面这个 Python 程序向您展示了捕获信号 SIGINT 并忽略它的基本操作,它并不会让程序停止。为了停止这个程序,我们需要使用 SIGQUIT 信号,通过输入 Ctrl-\ 可以发送该信号。
尽管 SIGINT 和 SIGQUIT 都常常用来发出和终止程序相关的请求。SIGTERM 则是一个更加通用的、也更加优雅地退出信号。为了发出这个信号我们需要使kill 命令, 它的语法是: kill -TERM <PID>。
jobs 命令会列出当前终端会话中尚未完成的全部任务
您可以使用 pid 引用这些任务(也可以用 pgrep 找出 pid)。更加符合直觉的操作是您可以使用百分号 + 任务编号(jobs 会打印任务编号)来选取该任务。如果要选择最近的一个任务,可以使用 $! 这一特殊参数。
还有一件事情需要掌握,那就是命令中的 & 后缀可以让命令在直接在后台运行,这使得您可以直接在 shell 中继续做其他操作,不过它此时还是会使用 shell 的标准输出,这一点有时会比较恼人(这种情况可以使用 shell 重定向处理)。
让已经在运行的进程转到后台运行,您可以键入 Ctrl-Z ,然后紧接着再输入 bg。注意,后台的进程仍然是您的终端进程的子进程,一旦您关闭终端(会发送另外一个信号 SIGHUP),这些后台的进程也会终止。为了防止这种情况发生,您可以使用 nohup(一个用来忽略 SIGHUP 的封装)来运行程序。针对已经运行的程序,可以使用 disown 。除此之外,您可以使用终端多路复用器来实现,下一章节我们会进行详细地探讨。
SIGKILL 是一个特殊的信号,它不能被进程捕获并且它会马上结束该进程。不过这样做会有一些副作用,例如留下孤儿进程。
您可以在 这里 或输入 man signal 或使用 kill -l 来获取更多关于信号的信息。
当我们输入 Ctrl-C 时,shell 会发送一个 SIGINT 信号到进程。
和siquit、sigtrm、sigstop(ctrl+z)有什么区别
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> <空格> 在不同的面板排布间切换
扩展阅读: 这里 是一份 tmux 快速入门教程, 而这一篇 文章则更加详细,它包含了 screen 命令。您也许想要掌握 screen 命令,因为在大多数 UNIX 系统中都默认安装有该程序。
输入一长串包含许多选项的命令会非常麻烦。因此,大多数 shell 都支持设置别名。shell 的别名相当于一个长命令的缩写,shell 会自动将其替换成原本的命令。例如,bash 中的别名语法如下:
alias alias_name="command_to_alias arg1 arg2"注意, = 两边是没有空格的,因为 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 overwritealias mkdir="mkdir -p" # -p make parent dirs as neededalias 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,下一节我们就会讲到。
很多程序的配置都是通过纯文本格式的被称作 点文件 的配置文件来完成的(之所以称为点文件,是因为它们的文件名以 . 开头,例如 ~/.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
我们应该如何管理这些配置文件呢,它们应该在它们的文件夹下,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。这么做有如下好处:
安装简单: 如果您登录了一台新的设备,在这台设备上应用您的配置只需要几分钟的时间;
可移植性: 您的工具在任何地方都以相同的配置工作
同步: 在一处更新配置文件,可以同步到其他所有地方
变更追踪: 您可能要在整个程序员生涯中持续维护这些配置文件,而对于长期项目而言,版本历史是非常重要的
配置文件中需要放些什么?您可以通过在线文档和 帮助手册 了解所使用工具的设置项。另一个方法是在网上搜索有关特定程序的文章,作者们在文章中会分享他们的配置。还有一种方法就是直接浏览其他人的配置文件:您可以在这里找到无数的 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 来包含与该设备相关的特定配置。您甚至应该创建一个单独的代码仓库来管理这些与设备相关的配置。
如果您希望在不同的程序之间共享某些配置,该方法也适用。例如,如果您想要在 bash 和 zsh 中同时启用一些别名,您可以把它们写在 .aliases 里,然后在这两个 shell 里应用:
# Test if ~/.aliases exists and source itif [ -f ~/.aliases ]; then source ~/.aliasesfisymlink [选项] 目标 源 (目标是旧位置还是源是旧位置 )ln -s 目标 源对于程序员来说,在他们的日常工作中使用远程服务器已经非常普遍了。如果您需要使用远程服务器来部署后端软件或您需要一些计算能力强大的服务器,您就会用到安全 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/id_rsa 或者 ~/.ssh/id_ed25519) 等效于您的密码,所以一定要好好保存它。
使用 ssh-keygen 命令可以生成一对密钥:
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 会查询 .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+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。但是如果需要监听远程服务器的端口该如何操作呢?这种情况下远端的端口并不会直接通过网络暴露给您。
此时就需要进行 端口转发。端口转发有两种,一种是本地端口转发和远程端口转发(参见下图,该图片引用自这篇 StackOverflow 文章)中的图片。
本地端口转发
把远程服务转发到本地访问
ssh -L 本地端口:目标地址:目标端口 用户@跳板机ssh -L 3307:127.0.0.1:3306 root@server.com浏览器 ↓localhost:3307 (本地) ↓ SSHserver:3306 (远程)远程端口转发
把本地服务暴露到远程访问
ssh -R 远程端口:目标地址:目标端口 用户@服务器远程访问者 ↓server:8080 (远程) ↓ SSHlocalhost:3000 (本地)常见的情景是使用本地端口转发,即远端设备上的服务监听一个端口,而您希望在本地设备上的一个端口建立连接并转发到远程端口上。例如,我们在远端服务器上运行 Jupyter notebook 并监听 8888 端口。 然后,建立从本地端口 9999 的转发,使用 ssh -L 9999:localhost:8888 foobar@remote_server 。这样只需要访问本地的 localhost:9999 即可。
我们已经介绍了很多参数。为它们创建一个别名是个好想法,我们可以这样做:
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 文件来创建别名,类似 scp、rsync 和 mosh 的这些命令都可以读取这个配置并将设置转换为对应的命令行选项。
注意,~/.ssh/config 文件也可以被当作配置文件,而且一般情况下也是可以被导入其他配置文件的。不过,如果您将其公开到互联网上,那么其他人都将会看到您的服务器地址、用户名、开放端口等等。这些信息可能会帮助到那些企图攻击您系统的黑客,所以请务必三思。
服务器侧的配置通常放在 /etc/ssh/sshd_config。您可以在这里配置免密认证、修改 ssh 端口、开启 X11 转发等等。 您也可以为每个用户单独指定配置。
连接远程服务器的一个常见痛点是遇到由关机、休眠或网络环境变化导致的掉线。如果连接的延迟很高也很让人讨厌。Mosh(即 mobile shell )对 ssh 进行了改进,它允许连接漫游、间歇连接及智能本地回显。
有时将一个远端文件夹挂载到本地会比较方便, sshfs 可以将远端服务器上的一个文件夹挂载到本地,然后您就可以使用本地的编辑器了。
ssh 登录
ssh 密钥
在 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 一样,花点时间选择适合您的 终端模拟器 并进行设置是很有必要的。有许多终端模拟器可供您选择(这里有一些关于它们之间 比较 的信息)
您会花上很多时间在使用终端上,因此研究一下终端的设置是很有必要的,您可以从下面这些方面来配置您的终端:
git 是跟踪源代码或其他文件/文件夹的更改记录的工具
git config --global core.autocrlf input 是一个 Git 配置命令,用于控制 Git 如何处理不同平台之间的行尾符转换问题,尤其是在 Linux/macOS 和 Windows 系统之间协作时很有用。
git config --global:为全局配置 Git(即应用于当前用户的所有 Git 仓库)。
core.autocrlf:这是一个 Git 配置选项,用于控制行尾符(CRLF 或 LF)自动转换的行为。
input:该选项值的意思是,当你将文件提交到 Git 仓库时,如果文件的行尾符是 CRLF(Windows 风格),则 Git 会将其转换为 LF(Unix 风格),但在检出时不会自动转换。这种设置适合在 Linux 和 macOS 上工作,因为这两个系统通常使用 LF 作为行尾符。
core.autocrlf false:不进行任何行尾符转换,适合全团队都在 Unix 风格(Linux/macOS)系统上工作的项目。
core.autocrlf true:提交时将 LF 转为 CRLF,检出时将 CRLF 转为 LF。适合在 Windows 系统上工作的团队。
core.autocrlf input:提交时将 CRLF 转换为 LF,检出时不做转换。适合在 Linux/macOS 系统上工作,但也需要兼容 Windows 用户的情况。
。我们会从数据模型开始,最后再学习它的接口。一旦您搞懂了 Git 的数据模型,再学习其接口并理解这些接口是如何操作数据模型的就非常容易了。
以下是 Git 数据结构的一些关键特点:
提交(Commit):每个提交对象包含一个快照(文件树的状态)、作者信息、提交信息、时间戳以及一个或多个父提交的指针。首次提交没有父提交,而合并提交可能有两个或更多个父提交。
分支(Branch):在 Git 中,分支只是指向特定提交的可移动指针。当您创建新提交时,所处分支的指针会向前移动到新的提交。
标签(Tag):标签类似于分支,但是它们固定在一个特定的提交上,不会随着新提交的产生而改变。
树(Tree):树对象表示项目中的目录结构,它们包含了文件名、模式(权限)和其他子树(即子目录)的信息。
Blob:Blob 对象用于存储文件的实际内容。每个 Blob 都是一个独立的对象,其内容是不可变的。
引用(Reference):引用是一种指向其他对象的指针,如分支、标签等。它们通常位于
.git/refs目录下,分为heads(分支)、tags(标签)和remotes(远程跟踪分支)。
Git 是一个分布式版本控制系统,它使用以下数据结构来存储和管理代码和版本信息:
1. 哈希(SHA-1)
Git 的核心是通过 SHA-1 哈希来标识所有对象(文件、提交、树等)。SHA-1 是一个 40 字符长的十六进制字符串,它通过内容计算生成,确保相同内容的对象具有相同的哈希值。
2. 对象(Object)模型
Git 中有四种主要的对象类型:
Blob(数据块)
表示文件的内容。
不存储文件名和权限,只存储文件内容本身。
Tree(树)
表示目录。
存储文件名、权限和指向对应 Blob 或子树的引用。
树结构允许 Git 高效地存储文件系统的层次关系。
Commit(提交)
表示一个快照。
指向一个 Tree 对象,表示项目当前的完整状态。
包含作者信息、提交时间、提交消息,以及父提交的引用(用于构建提交历史)。
Tag(标签)
指向一个提交,用于给特定版本打标签。
可以是轻量标签(直接指向提交)或带有额外描述的注解标签。
3. 树(Tree)
Git 使用树数据结构存储文件目录结构。每个目录是一个树对象,每个文件是一个 Blob 对象。树对象引用 Blob 对象或其他树对象,形成递归层次结构。
4. 有向无环图(DAG)
提交历史在 Git 中表现为一个 有向无环图(DAG),其中每个提交节点指向其父提交。分支只是指向提交对象的引用:
单个分支是一个链式结构。
合并分支后形成分叉再合并的 DAG。
5. 索引(Index / Staging Area)
Git 的暂存区(Index)使用一个简单的哈希表,记录文件的状态。它将工作目录中的文件映射到对应的 Blob 对象,用于准备下一次提交。
6. 引用(Refs)
Git 使用引用来指向特定的提交或分支:
分支(Branch) 是指向某个提交的可移动指针。
HEAD 是一个特殊的引用,指向当前检出的分支或具体的提交。
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在 Git 的术语里,文件被称作 Blob 对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
<root> (tree)|+- foo (tree)| || + bar.txt (blob, contents = "hello world")|+- baz.txt (blob, contents = "git is wonderful")这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个 blob 对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过出于种种原因,Git 并没有采用这样的模型。
在 Git 中,历史记录是一个由快照组成的有向无环图。有向无环图,听上去似乎是什么高大上的数学名词。不过不要怕,您只需要知道这代表 Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。
在 Git 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
o <-- o <-- o <-- o ^ \ --- o <-- o上面是一个 ASCII 码构成的简图,其中的 o 表示一次提交(快照)。
箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样(最新的合并提交用粗体标记):
o <-- o <-- o <-- o <---- o ^ / \ v --- o <-- oGit 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
// 文件就是一组数据type blob = array<byte> // 一个包含文件和目录的目录type tree = map<string, tree | blob> // 每个提交都包含一个父辈,元数据和顶层树type commit = struct { parents: array<commit> author: string message: string snapshot: tree}这是一种简洁的历史模型。
Git 中的对象可以是 blob、树或提交:
type object = blob | tree | commitGit 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
objects = map<string, object> def store(object): id = sha1(object) objects[id] = object def load(id): return objects[id]Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
例如,上面 例子中的树(可以通过 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d 来进行可视化),看上去是这样的:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo树本身会包含一些指向其他内容的指针,例如 baz.txt (blob) 和 foo (树)。如果我们用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85,即通过哈希值查看 baz.txt 的内容,会得到以下信息:
git is wonderful现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master 引用通常会指向主分支的最新一次提交。
references = map<string, string> def update_reference(name, id): references[name] = id def read_reference(name): return references[name] def load_reference(name_or_id): if name_or_id in references: return load(references[name_or_id]) else: return load(name_or_id)这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。
有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
最后,我们可以粗略地给出 Git 仓库的定义了:对象 和 引用。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git 命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e 时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e)
Git 中还包括一个和数据模型完全不相关的概念,但它确是创建提交的接口的一部分。
就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但 Git 不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。
Git 处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括那些改动。
为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读 Pro Git 中文版 或可以观看本讲座的视频来学习。
git help <command>: 获取 git 命令的帮助信息
git init: 创建一个新的 git 仓库,其数据会存放在一个名为 .git 的目录下
git status: 显示当前的仓库状态
git add <filename>: 添加文件到暂存区
git commit: 创建一个新的提交
git log: 显示历史日志
git log --all --graph --decorate: 可视化历史记录(有向无环图)
git diff <filename>: 显示与暂存区文件的差异
git diff <revision> <filename>: 显示某个文件两个版本之间的差异
git checkout <revision>: 更新 HEAD 和目前的分支
git branch: 显示分支
git branch <name>: 创建分支
git checkout -b <name>: 创建分支并切换到该分支
git branch <name>; git checkout <name>git merge <revision>: 合并到当前分支
git mergetool: 使用工具来处理合并冲突
git rebase: 将一系列补丁变基(rebase)为新的基线
git remote: 列出远端
git remote add <name> <url>: 添加一个远端
git push <remote> <local branch>:<remote branch>: 将对象传送至远端并更新远端引用
git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系
git fetch: 从远端获取对象/索引
git pull: 相当于 git fetch; git merge
git clone: 从远端下载仓库
git commit --amend: 编辑提交的内容或信息
git reset HEAD <file>: 恢复暂存的文件
git checkout -- <file>: 丢弃修改
git restore: git2.32 版本后取代 git reset 进行许多撤销操作
git config: Git 是一个 高度可定制的 工具
git clone --depth=1: 浅克隆(shallow clone),不包括完整的版本历史信息
git add -p: 交互式暂存
git rebase -i: 交互式变基
git blame: 查看最后修改某行的人
git stash: 暂时移除工作目录下的修改内容
git bisect: 通过二分查找搜索历史记录
.gitignore: 指定 故意不追踪的文件
图形用户界面: Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh, bash)。Oh My Zsh 这样的框架中一般已经集成了这一功能
编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 Git 的常用插件
GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作 拉取请求(pull request) 的方法来向其他项目贡献代码
Pro Git ,强烈推荐!学习前五章的内容可以教会您流畅使用 Git 的绝大多数技巧,因为您已经理解了 Git 的数据模型。后面的章节提供了很多有趣的高级主题。(Pro Git 中文版);
Oh Shit, Git!?! ,简短的介绍了如何从 Git 错误中恢复;
Git for Computer Scientists ,简短的介绍了 Git 的数据模型,与本文相比包含较少量的伪代码以及大量的精美图片;
Git from the Bottom Up 详细的介绍了 Git 的实现细节,而不仅仅局限于数据模型。好奇的同学可以看看;
Learn Git Branching 通过基于浏览器的游戏来学习 Git ;
调试有printf打印调试,gdb断点调试,日志调试
分析是性能分析,包括时间复杂度与空间复杂度
全按照您的写法运行,这是编程界的一条金科玉律。
让您的写法符合您的想法是非常困难的。在这节课中,我们会传授给您一些非常有用技术,帮您处理代码中的 bug 和程序性能问题。
“最有效的 debug 工具就是细致的分析,配合恰当位置的打印语句” — Brian Kernighan, Unix 新手入门。
调试代码的第一种方法往往是在您发现问题的地方添加一些打印语句,然后不断重复此过程直到您获取了足够的信息并找到问题的根本原因。
另外一个方法是使用日志,而不是临时添加打印语句。日志较普通的打印语句有如下的一些优势:
您可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR 等),这使您可以根据需要过滤日志;
对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。
这里 是一个包含日志的例程序:
$ python logger.py# Raw output as with just prints$ python logger.py log# Log formatted output$ python logger.py log ERROR# Print only ERROR levels and above$ python logger.py color# Color formatted output有很多技巧可以使日志的可读性变得更好,我最喜欢的一个是技巧是对其进行着色。到目前为止,您应该已经知道,以彩色文本显示终端信息时可读性更好。但是应该如何设置呢?
ls 和 grep 这样的程序会使用 ANSI escape codes,它是一系列的特殊字符,可以使您的 shell 改变输出结果的颜色。例如,执行 echo -e "\e[38;2;255;0;0mThis is red\e[0m" 会打印红色的字符串:This is red 。只要您的终端支持 真彩色。如果您的终端不支持真彩色(例如 MacOS 的 Terminal.app),您可以使用支持更加广泛的 16 色,例如:”\e[31; 1mThis is red\e[0m “。
下面这个脚本向您展示了如何在终端中打印多种颜色(只要您的终端支持真彩色)
#!/usr/bin/env bashfor R in $(seq 0 20 255); do for G in $(seq 0 20 255); do for B in $(seq 0 20 255); do printf "\e[38;2;${R};${G};${B}m█\e[0m"; done donedone如果您正在构建大型软件系统,您很可能会使用到一些依赖,有些依赖会作为程序单独运行。如 Web 服务器、数据库或消息代理都是此类常见的第三方依赖。
和这些系统交互的时候,阅读它们的日志是非常必要的,因为仅靠客户端侧的错误信息可能并不足以定位问题。
幸运的是,大多数的程序都会将日志保存在您的系统中的某个地方。对于 UNIX 系统来说,程序的日志通常存放在 /var/log。例如, NGINX web 服务器就将其日志存放于 /var/log/nginx。
目前,系统开始使用 system log,您所有的日志都会保存在这里。大多数(但不是全部的)Linux 系统都会使用 systemd,这是一个系统守护进程,它会控制您系统中的很多东西,例如哪些服务应该启动并运行。systemd 会将日志以某种特殊格式存放于 /var/log/journal,您可以使用 journalctl 命令显示这些消息。
类似地,在 macOS 系统中是 /var/log/system.log,但是有更多的工具会使用系统日志,它的内容可以使用 log show 显示。
对于大多数的 UNIX 系统,您也可以使用 dmesg 命令来读取内核的日志。
如果您希望将日志加入到系统日志中,您可以使用 logger 这个 shell 程序。下面这个例子显示了如何使用 logger 并且如何找到能够将其存入系统日志的条目。
不仅如此,大多数的编程语言都支持向系统日志中写日志。
logger "Hello Logs"# On macOSlog show --last 1m | grep Hello# On Linuxjournalctl --since "1m ago" | grep Hello正如我们在数据整理那节课上看到的那样,日志的内容可以非常的多,我们需要对其进行处理和过滤才能得到我们想要的信息。
如果您发现您需要对 journalctl 和 log show 的结果进行大量的过滤,那么此时可以考虑使用它们自带的选项对其结果先过滤一遍再输出。还有一些像 lnav 这样的工具,它为日志文件提供了更好的展现和浏览方式。
当通过打印已经不能满足您的调试需求时,您应该使用调试器。
调试器是一种可以允许我们和正在执行的程序进行交互的程序,它可以做到:
当到达某一行时将程序暂停;
一次一条指令地逐步执行程序;
程序崩溃后查看变量的值;
满足特定条件时暂停程序;
其他高级功能。
很多编程语言都有自己的调试器。Python 的调试器是 pdb.
下面对 pdb 支持的命令进行简单的介绍:
l(ist) - 显示当前行附近的 11 行或继续执行之前的显示;
s(tep) - 执行当前行,并在第一个可能的地方停止;
n(ext) - 继续执行直到当前函数的下一条语句或者 return 语句;
b(reak) - 设置断点(基于传入的参数);
p(rint) - 在当前上下文对表达式求值并打印结果。还有一个命令是 pp ,它使用 pprint 打印;
r(eturn) - 继续执行直到当前函数返回;
q(uit) - 退出调试器。
让我们使用 pdb 来修复下面的 Python 代码(参考讲座视频)
def bubble_sort(arr): n = len(arr) for i in range(n): for j in range(n): if arr[j] > arr[j+1]: arr[j] = arr[j+1] arr[j+1] = arr[j] return arr print(bubble_sort([4, 2, 1, 8, 7, 6]))注意,因为 Python 是一种解释型语言,所以我们可以通过 pdb shell 执行命令。 ipdb 是一种增强型的 pdb ,它使用 IPython 作为 REPL 并开启了 tab 补全、语法高亮、更好的回溯和更好的内省,同时还保留了 pdb 模块相同的接口。
对于更底层的编程语言,您可能需要了解一下 gdb ( 以及它的改进版 pwndbg) 和 lldb。
它们都对类 C 语言的调试进行了优化,它允许您探索任意进程及其机器状态:寄存器、堆栈、程序计数器等。
即使您需要调试的程序是一个二进制的黑盒程序,仍然有一些工具可以帮助到您。当您的程序需要执行一些只有操作系统内核才能完成的操作时,它需要使用 系统调用。有一些命令可以帮助您追踪您的程序执行的系统调用。在 Linux 中可以使用 strace ,在 macOS 和 BSD 中可以使用 dtrace。dtrace 用起来可能有些别扭,因为它使用的是它自有的 D 语言,但是我们可以使用一个叫做 dtruss 的封装使其具有和 strace (更多信息参考 这里)类似的接口
下面的例子展现来如何使用 strace 或 dtruss 来显示 ls 执行时,对 stat 系统调用进行追踪对结果。若需要深入了解 strace,这篇文章 值得一读。
# On Linuxsudo strace -e lstat ls -l > /dev/null4# On macOSsudo dtruss -t lstat64_extended ls -l > /dev/null有些情况下,我们需要查看网络数据包才能定位问题。像 tcpdump 和 Wireshark 这样的网络数据包分析工具可以帮助您获取网络数据包的内容并基于不同的条件进行过滤。
对于 web 开发, Chrome/Firefox 的开发者工具非常方便,功能也很强大:
源码 -查看任意站点的 HTML/CSS/JS 源码;
实时地修改 HTML, CSS, JS 代码 - 修改网站的内容、样式和行为用于测试(从这一点您也能看出来,网页截图是不可靠的);
Javascript shell - 在 JS REPL 中执行命令;
网络 - 分析请求的时间线;
存储 - 查看 Cookies 和本地应用存储。
有些问题是您不需要执行代码就能发现的。例如,仔细观察一段代码,您就能发现某个循环变量覆盖了某个已经存在的变量或函数名;或是有个变量在被读取之前并没有被定义。 这种情况下 静态分析 工具就可以帮我们找到问题。静态分析会将程序的源码作为输入然后基于编码规则对其进行分析并对代码的正确性进行推理。
下面这段 Python 代码中存在几个问题。 首先,我们的循环变量 foo 覆盖了之前定义的函数 foo。最后一行,我们还把 bar 错写成了 baz,因此当程序完成 sleep (一分钟)后,执行到这一行的时候便会崩溃。
import time def foo(): return 42 for foo in range(5): print(foo)bar = 1bar *= 0.2time.sleep(60)print(baz)静态分析工具可以发现此类的问题。当我们使用 pyflakes 分析代码的时候,我们会得到与这两处 bug 相关的错误信息。mypy 则是另外一个工具,它可以对代码进行类型检查。这里,mypy 会经过我们 bar 起初是一个 int ,然后变成了 float。这些问题都可以在不运行代码的情况下被发现。
$ pyflakes foobar.pyfoobar.py:6: redefinition of unused 'foo' from line 3foobar.py:11: undefined name 'baz' $ mypy foobar.pyfoobar.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")foobar.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int")foobar.py:11: error: Name 'baz' is not definedFound 3 errors in 1 file (checked 1 source file)在 shell 工具那一节课的时候,我们介绍了 shellcheck,这是一个类似的工具,但它是应用于 shell 脚本的。
大多数的编辑器和 IDE 都支持在编辑界面显示这些工具的分析结果、高亮有警告和错误的位置。 这个过程通常称为 code linting 。风格检查或安全检查的结果同样也可以进行相应的显示。
在 vim 中,有 ale 或 syntastic 可以帮助您做同样的事情。 在 Python 中, pylint 和 pep8 是两种用于进行风格检查的工具,而 bandit 工具则用于检查安全相关的问题。
对于其他语言的开发者来说,静态分析工具可以参考这个列表:Awesome Static Analysis (您也许会对 Writing 一节感兴趣) 。对于 linters 则可以参考这个列表: Awesome Linters。
对于风格检查和代码格式化,还有以下一些工具可以作为补充:用于 Python 的 black、用于 Go 语言的 gofmt、用于 Rust 的 rustfmt 或是用于 JavaScript, HTML 和 CSS 的 prettier 。这些工具可以自动格式化您的代码,这样代码风格就可以与常见的风格保持一致。 尽管您可能并不想对代码进行风格控制,标准的代码风格有助于方便别人阅读您的代码,也可以方便您阅读它的代码。
即使您的代码能够像您期望的一样运行,但是如果它消耗了您全部的 CPU 和内存,那么它显然也不是个好程序。算法课上我们通常会介绍大 O 标记法,但却没教给我们如何找到程序中的热点。 鉴于 过早的优化是万恶之源,您需要学习性能分析和监控工具,它们会帮助您找到程序中最耗时、最耗资源的部分,这样您就可以有针对性的进行性能优化。
和调试代码类似,大多数情况下我们只需要打印两处代码之间的时间即可发现问题。下面这个例子中,我们使用了 Python 的 time 模块。
import time, randomn = random.randint(1, 10) * 100 # 获取当前时间 start = time.time() # 执行一些操作print("Sleeping for {} ms".format(n))time.sleep(n/1000) # 比较当前时间和起始时间print(time.time() - start) # Output# Sleeping for 500 ms# 0.5713930130004883不过,执行时间(wall clock time)也可能会误导您,因为您的电脑可能也在同时运行其他进程,也可能在此期间发生了等待。 对于工具来说,需要区分真实时间、用户时间和系统时间。通常来说,用户时间 + 系统时间代表了您的进程所消耗的实际 CPU (更详细的解释可以参照 这篇文章)。
真实时间 Real - 从程序开始到结束流失掉的真实时间,包括其他进程的执行时间以及阻塞消耗的时间(例如等待 I/O 或网络);
用户时间 User - CPU 执行用户代码所花费的时间;
系统时间 Sys - CPU 执行系统内核代码所花费的时间。
例如,试着执行一个用于发起 HTTP 请求的命令并在其前面添加 time 前缀。网络不好的情况下您可能会看到下面的输出结果。请求花费了 2s 多才完成,但是进程仅花费了 15ms 的 CPU 用户时间和 12ms 的 CPU 内核时间。
$ time curl https://missing.csail.mit.edu &> /dev/nullreal 0m2.561suser 0m0.015ssys 0m0.012s大多数情况下,当人们提及性能分析工具的时候,通常指的是 CPU 性能分析工具。 CPU 性能分析工具有两种: 追踪分析器(tracing)及采样分析器(sampling)。 追踪分析器 会记录程序的每一次函数调用,而采样分析器则只会周期性的监测(通常为每毫秒)您的程序并记录程序堆栈。它们使用这些记录来生成统计信息,显示程序在哪些事情上花费了最多的时间。如果您希望了解更多相关信息,可以参考 这篇 介绍性的文章。
大多数的编程语言都有一些基于命令行的分析器,我们可以使用它们来分析代码。它们通常可以集成在 IDE 中,但是本节课我们会专注于这些命令行工具本身。
在 Python 中,我们使用 cProfile 模块来分析每次函数调用所消耗的时间。 在下面的例子中,我们实现了一个基础的 grep 命令:
#!/usr/bin/env python import sys, re def grep(pattern, file): with open(file, 'r') as f: print(file) for i, line in enumerate(f.readlines()): pattern = re.compile(pattern) match = pattern.search(line) if match is not None: print("{}: {}".format(i, line), end="") if __name__ == '__main__': times = int(sys.argv[1]) pattern = sys.argv[2] for i in range(times): for file in sys.argv[3:]: grep(pattern, file)我们可以使用下面的命令来对这段代码进行分析。通过它的输出我们可以知道,IO 消耗了大量的时间,编译正则表达式也比较耗费时间。因为正则表达式只需要编译一次,我们可以将其移动到 for 循环外面来改进性能。
$ python -m cProfile -s tottime grep.py 1000 '^(import|\s*def)[^,]*$' *.py [omitted program output] ncalls tottime percall cumtime percall filename:lineno(function) 8000 0.266 0.000 0.292 0.000 {built-in method io.open} 8000 0.153 0.000 0.894 0.000 grep.py:5(grep) 17000 0.101 0.000 0.101 0.000 {built-in method builtins.print} 8000 0.100 0.000 0.129 0.000 {method 'readlines' of '_io._IOBase' objects} 93000 0.097 0.000 0.111 0.000 re.py:286(_compile) 93000 0.069 0.000 0.069 0.000 {method 'search' of '_sre.SRE_Pattern' objects} 93000 0.030 0.000 0.141 0.000 re.py:231(compile) 17000 0.019 0.000 0.029 0.000 codecs.py:318(decode) 1 0.017 0.017 0.911 0.911 grep.py:3(<module>) [omitted lines]关于 Python 的 cProfile 分析器(以及其他一些类似的分析器),需要注意的是它显示的是每次函数调用的时间。看上去可能快到反直觉,尤其是如果您在代码里面使用了第三方的函数库,因为内部函数调用也会被看作函数调用。
更加符合直觉的显示分析信息的方式是包括每行代码的执行时间,这也是 行分析器 的工作。例如,下面这段 Python 代码会向本课程的网站发起一个请求,然后解析响应返回的页面中的全部 URL:
#!/usr/bin/env pythonimport requestsfrom bs4 import BeautifulSoup # 这个装饰器会告诉行分析器 # 我们想要分析这个函数@profiledef get_urls(): response = requests.get('https://missing.csail.mit.edu') s = BeautifulSoup(response.content, 'lxml') urls = [] for url in s.find_all('a'): urls.append(url['href']) if __name__ == '__main__': get_urls()如果我们使用 Python 的 cProfile 分析器,我们会得到超过 2500 行的输出结果,即使对其进行排序,我仍然搞不懂时间到底都花在哪了。如果我们使用 line_profiler,它会基于行来显示时间:
$ kernprof -l -v a.pyWrote profile results to urls.py.lprofTimer unit: 1e-06 s Total time: 0.636188 sFile: a.pyFunction: get_urls at line 5 Line # Hits Time Per Hit % Time Line Contents============================================================== 5 @profile 6 def get_urls(): 7 1 613909.0 613909.0 96.5 response = requests.get('https://missing.csail.mit.edu') 8 1 21559.0 21559.0 3.4 s = BeautifulSoup(response.content, 'lxml') 9 1 2.0 2.0 0.0 urls = []10 25 685.0 27.4 0.1 for url in s.find_all('a'):11 24 33.0 1.4 0.0 urls.append(url['href'])像 C 或者 C++ 这样的语言,内存泄漏会导致您的程序在使用完内存后不去释放它。为了应对内存类的 Bug,我们可以使用类似 Valgrind 这样的工具来检查内存泄漏问题。
对于 Python 这类具有垃圾回收机制的语言,内存分析器也是很有用的,因为对于某个对象来说,只要有指针还指向它,那它就不会被回收。
下面这个例子及其输出,展示了 memory-profiler 是如何工作的(注意装饰器和 line-profiler 类似)。
@profiledef my_func(): a = [1] * (10 ** 6) b = [2] * (2 * 10 ** 7) del b return a if __name__ == '__main__': my_func()$ python -m memory_profiler example.pyLine # Mem usage Increment Line Contents============================================== 3 @profile 4 5.97 MB 0.00 MB def my_func(): 5 13.61 MB 7.64 MB a = [1] * (10 ** 6) 6 166.20 MB 152.59 MB b = [2] * (2 * 10 ** 7) 7 13.61 MB -152.59 MB del b 8 13.61 MB 0.00 MB return a在我们使用 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 分析数据的形式是 火焰图,火焰图会在 Y 轴显示函数调用关系,并在 X 轴显示其耗时的比例。火焰图同时还是可交互的,您可以深入程序的某一具体部分,并查看其栈追踪。

调用图和控制流图可以显示子程序之间的关系,它将函数作为节点并把函数调用作为边。将它们和分析器的信息(例如调用次数、耗时等)放在一起使用时,调用图会变得非常有用,它可以帮助我们分析程序的流程。 在 Python 中您可以使用 pycallgraph 来生成这些图片。

有时候,分析程序性能的第一步是搞清楚它所消耗的资源。程序变慢通常是因为它所需要的资源不够了。例如,没有足够的内存或者网络连接变慢的时候。
有很多很多的工具可以被用来显示不同的系统资源,例如 CPU 占用、内存使用、网络、磁盘使用等。
通用监控 - 最流行的工具要数 htop, 了,它是 top 的改进版。htop 可以显示当前运行进程的多种统计信息。htop 有很多选项和快捷键,常见的有:<F6> 进程排序、 t 显示树状结构和 h 打开或折叠线程。 还可以留意一下 glances ,它的实现类似但是用户界面更好。如果需要合并测量全部的进程, dstat 是也是一个非常好用的工具,它可以实时地计算不同子系统资源的度量数据,例如 I/O、网络、 CPU 利用率、上下文切换等等;
I/O 操作 - iotop 可以显示实时 I/O 占用信息而且可以非常方便地检查某个进程是否正在执行大量的磁盘读写操作;
磁盘使用 - df 可以显示每个分区的信息,而 du 则可以显示当前目录下每个文件的磁盘使用情况( d isk u sage)。-h 选项可以使命令以对人类(h uman)更加友好的格式显示数据;ncdu 是一个交互性更好的 du ,它可以让您在不同目录下导航、删除文件和文件夹;
内存使用 - free 可以显示系统当前空闲的内存。内存也可以使用 htop 这样的工具来显示;
打开文件 - lsof 可以列出被进程打开的文件信息。 当我们需要查看某个文件是被哪个进程打开的时候,这个命令非常有用;
网络连接和配置 - ss 能帮助我们监控网络包的收发情况以及网络接口的显示信息。ss 常见的一个使用场景是找到端口被进程占用的信息。如果要显示路由、网络设备和接口信息,您可以使用 ip 命令。注意,netstat 和 ifconfig 这两个命令已经被前面那些工具所代替了。
如果您希望测试一下这些工具,您可以使用 stress 命令来为系统人为地增加负载。
有时候,您只需要对黑盒程序进行基准测试,并依此对软件选择进行评估。 类似 hyperfine 这样的命令行可以帮您快速进行基准测试。例如,我们在 shell 工具和脚本那一节课中我们推荐使用 fd 来代替 find。我们这里可以用 hyperfine 来比较一下它们。
例如,下面的例子中,我们可以看到 fd 比 find 要快 20 倍。
$ hyperfine --warmup 3 'fd -e jpg' 'find . -iname "*.jpg"'Benchmark #1: fd -e jpg Time (mean ± σ): 51.4 ms ± 2.9 ms [User: 121.0 ms, System: 160.5 ms] Range (min … max): 44.2 ms … 60.1 ms 56 runs Benchmark #2: find . -iname "*.jpg" Time (mean ± σ): 1.126 s ± 0.101 s [User: 141.1 ms, System: 956.1 ms] Range (min … max): 0.975 s … 1.287 s 10 runs Summary 'fd -e jpg' ran 21.89 ± 2.33 times faster than 'find . -iname "*.jpg"'和 debug 一样,浏览器也包含了很多不错的性能分析工具,可以用来分析页面加载,让我们可以搞清楚时间都消耗在什么地方(加载、渲染、脚本等等)。 更多关于 Firefox 和 Chrome 的信息可以点击链接。
如何用make构建一个工程/系统,并处理依赖管理,版本号如何取
如果您使用 LaTeX 来编写论文,您需要执行哪些命令才能编译出您想要的论文呢?执行基准测试、绘制图表然后将其插入论文的命令又有哪些?或者,如何编译本课程提供的代码并执行测试呢?
对于大多数系统来说,不论其是否包含代码,都会包含一个 “构建过程”。有时,您需要执行一系列操作。通常,这一过程包含了很多步骤,很多分支。执行一些命令来生成图表,然后执行另外的一些命令生成结果,然后再执行其他的命令来生成最终的论文。有很多事情需要我们完成,您并不是第一个因此感到苦恼的人,幸运的是,有很多工具可以帮助我们完成这些操作。
这些工具通常被称为 “构建系统”,而且这些工具还不少。如何选择工具完全取决于您当前手头上要完成的任务以及项目的规模。从本质上讲,这些工具都是非常类似的。您需要定义 依赖、目标 和 规则。您必须告诉构建系统您具体的构建目标,系统的任务则是找到构建这些目标所需要的依赖,并根据规则构建所需的中间产物,直到最终目标被构建出来。理想的情况下,如果目标的依赖没有发生改动,并且我们可以从之前的构建中复用这些依赖,那么与其相关的构建规则并不会被执行。
make 是最常用的构建系统之一,您会发现它通常被安装到了几乎所有基于 UNIX 的系统中。make 并不完美,但是对于中小型项目来说,它已经足够好了。当您执行 make 时,它会去参考当前目录下名为 Makefile 的文件。所有构建目标、相关依赖和规则都需要在该文件中定义,它看上去是这样的:
paper.pdf: paper.tex plot-data.png pdflatex paper.tex plot-%.png: %.dat plot.py ./plot.py -i $*.dat -o $@这个文件中的指令,即如何使用右侧文件构建左侧文件的规则。或者,换句话说,冒号左侧的是构建目标,冒号右侧的是构建它所需的依赖。缩进的部分是从依赖构建目标时需要用到的一段命令。在 make 中,第一条指令还指明了构建的目的,如果您使用不带参数的 make,这便是我们最终的构建结果。或者,您可以使用这样的命令来构建其他目标:make plot-data.png。
规则中的 % 是一种模式,它会匹配其左右两侧相同的字符串。例如,如果目标是 plot-foo.png, make 会去寻找 foo.dat 和 plot.py 作为依赖。现在,让我们看看如果在一个空的源码目录中执行 make 会发生什么?
$ makemake: *** No rule to make target 'paper.tex', needed by 'paper.pdf'. Stop.make 会告诉我们,为了构建出 paper.pdf,它需要 paper.tex,但是并没有一条规则能够告诉它如何构建该文件。让我们构建它吧!
$ touch paper.tex$ makemake: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'. Stop.哟,有意思,我们是 有 构建 plot-data.png 的规则的,但是这是一条模式规则。因为源文件 data.dat 并不存在,因此 make 就会告诉您它不能构建 plot-data.png,让我们创建这些文件:
$ cat paper.tex\documentclass{article}\usepackage{graphicx}\begin{document}\includegraphics[scale=0.65]{plot-data.png}\end{document}$ cat plot.py#!/usr/bin/env pythonimport matplotlibimport matplotlib.pyplot as pltimport numpy as npimport argparse parser = argparse.ArgumentParser()parser.add_argument('-i', type=argparse.FileType('r'))parser.add_argument('-o')args = parser.parse_args() data = np.loadtxt(args.i)plt.plot(data[:, 0], data[:, 1])plt.savefig(args.o)$ cat data.dat1 12 23 34 45 8当我们执行 make 时会发生什么?
$ make./plot.py -i data.dat -o plot-data.pngpdflatex paper.tex... lots of output ...看!PDF !
如果再次执行 make 会怎样?
$ makemake: 'paper.pdf' is up to date.什么事情都没做!为什么?好吧,因为它什么都不需要做。make 检查出所有之前构建的目标仍然与其列出的依赖项保持最新状态。让我们试试修改 paper.tex 后再重新执行 make:
$ vim paper.tex$ makepdflatex paper.tex...注意 make 并 没有 重新构建 plot.py,因为没必要;plot-data.png 的所有依赖都没有发生改变。
就您的项目来说,它的依赖可能本身也是其他的项目。您也许会依赖某些程序(例如 python)、系统包(例如 openssl)或相关编程语言的库(例如 matplotlib)。 现在,大多数的依赖可以通过某些 软件仓库 来获取,这些仓库会在一个地方托管大量的依赖,我们则可以通过一套非常简单的机制来安装依赖。例如 Ubuntu 系统下面有 Ubuntu 软件包仓库,您可以通过 apt 这个工具来访问, RubyGems 则包含了 Ruby 的相关库,PyPi 包含了 Python 库, Arch Linux 用户贡献的库则可以在 Arch User Repository 中找到。
由于每个仓库、每种工具的运行机制都不太一样,因此我们并不会在本节课深入讲解具体的细节。我们会介绍一些通用的术语,例如 版本控制。大多数被其他项目所依赖的项目都会在每次发布新版本时创建一个 版本号。通常看上去像 8.1.3 或 64.1.20192004。版本号一般是数字构成的,但也并不绝对。版本号有很多用途,其中最重要的作用是保证软件能够运行。试想一下,假如我的库要发布一个新版本,在这个版本里面我重命名了某个函数。如果有人在我的库升级版本后,仍希望基于它构建新的软件,那么很可能构建会失败,因为它希望调用的函数已经不复存在了。有了版本控制就可以很好的解决这个问题,我们可以指定当前项目需要基于某个版本,甚至某个范围内的版本,或是某些项目来构建。这么做的话,即使某个被依赖的库发生了变化,依赖它的软件可以基于其之前的版本进行构建。
这样还并不理想!如果我们发布了一项和安全相关的升级,它并 没有 影响到任何公开接口(API),但是处于安全的考虑,依赖它的项目都应该立即升级,那应该怎么做呢?这也是版本号包含多个部分的原因。不同项目所用的版本号其具体含义并不完全相同,但是一个相对比较常用的标准是 语义版本号,这种版本号具有不同的语义,它的格式是这样的:主版本号.次版本号.补丁号。相关规则有:
如果新的版本没有改变 API,请将补丁号递增;
如果您添加了 API 并且该改动是向后兼容的,请将次版本号递增;
如果您修改了 API 但是它并不向后兼容,请将主版本号递增。
这么做有很多好处。现在如果我们的项目是基于您的项目构建的,那么只要最新版本的主版本号只要没变就是安全的 ,次版本号不低于之前我们使用的版本即可。换句话说,如果我依赖的版本是 1.3.7,那么使用 1.3.8、1.6.1,甚至是 1.3.0 都是可以的。如果版本号是 2.2.4 就不一定能用了,因为它的主版本号增加了。我们可以将 Python 的版本号作为语义版本号的一个实例。您应该知道,Python 2 和 Python 3 的代码是不兼容的,这也是为什么 Python 的主版本号改变的原因。类似的,使用 Python 3.5 编写的代码在 3.7 上可以运行,但是在 3.4 上可能会不行。
使用依赖管理系统的时候,您可能会遇到锁文件(lock files)这一概念。锁文件列出了您当前每个依赖所对应的具体版本号。通常,您需要执行升级程序才能更新依赖的版本。这么做的原因有很多,例如避免不必要的重新编译、创建可复现的软件版本或禁止自动升级到最新版本(可能会包含 bug)。还有一种极端的依赖锁定叫做 vendoring,它会把您的依赖中的所有代码直接拷贝到您的项目中,这样您就能够完全掌控代码的任何修改,同时您也可以将自己的修改添加进去,不过这也意味着如果该依赖的维护者更新了某些代码,您也必须要自己去拉取这些更新。
随着您接触到的项目规模越来越大,您会发现修改代码之后还有很多额外的工作要做。您可能需要上传一份新版本的文档、上传编译后的文件到某处、发布代码到 pypi,执行测试套件等等。或许您希望每次有人提交代码到 GitHub 的时候,他们的代码风格被检查过并执行过某些基准测试?如果您有这方面的需求,那么请花些时间了解一下持续集成。
持续集成(Continuous integration),或者叫做 CI 是一种雨伞术语(umbrella term,涵盖了一组术语的术语),它指的是那些“当您的代码变动时,自动运行的东西”,市场上有很多提供各式各样 CI 工具的公司,这些工具大部分都是免费或开源的。比较大的有 Travis CI、Azure Pipelines 和 GitHub Actions。它们的工作原理都是类似的:您需要在代码仓库中添加一个文件,描述当前仓库发生任何修改时,应该如何应对。目前为止,最常见的规则是:如果有人提交代码,执行测试套件。当这个事件被触发时,CI 提供方会启动一个(或多个)虚拟机,执行您制定的规则,并且通常会记录下相关的执行结果。您可以进行某些设置,这样当测试套件失败时您能够收到通知或者当测试全部通过时,您的仓库主页会显示一个徽标。
本课程的网站基于 GitHub Pages 构建,这就是一个很好的例子。Pages 在每次 master 有代码更新时,会执行 Jekyll 博客软件,然后使您的站点可以通过某个 GitHub 域名来访问。对于我们来说这些事情太琐碎了,我现在我们只需要在本地进行修改,然后使用 git 提交代码,发布到远端。CI 会自动帮我们处理后续的事情。
多数的大型软件都有“测试套件”。您可能已经对测试的相关概念有所了解,但是我们觉得有些测试方法和测试术语还是应该再次提醒一下:
测试套件(Test suite):所有测试的统称。
单元测试(Unit test):一种“微型测试”,用于对某个封装的特性进行测试。
集成测试(Integration test):一种“宏观测试”,针对系统的某一大部分进行,测试其不同的特性或组件是否能 协同 工作。
回归测试(Regression test):一种实现特定模式的测试,用于保证之前引起问题的 bug 不会再次出现。
模拟(Mocking): 使用一个假的实现来替换函数、模块或类型,屏蔽那些和测试不相关的内容。例如,您可能会“模拟网络连接” 或 “模拟硬盘”。
对称加密与非对称加密
熵 (Entropy) 是不确定性的度量,这很有用,可以用来决定密码的强度。
“correcthorsebatterystaple” 这个密码比 “Tr0ub4dor&3” 更安全——可是熵是如何量化安全性的呢?
熵的单位是 比特。对于一个均匀分布的随机离散变量,熵等于 log_2(所有可能的个数,即 n)。 扔一次硬币的熵是 1 比特。掷一次(六面)骰子的熵大约为 2.58 比特。
一般我们认为攻击者了解密码的模型(最小长度,最大长度,可能包含的字符种类等),但是不了解某个密码是如何随机选择的—— 比如 掷骰子。
使用多少比特的熵取决于应用的威胁模型。 上面的 XKCD 漫画告诉我们,大约 40 比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制)。 而对于离线穷举攻击(主要受限于计算速度), 一般需要更强的密码 (比如 80 比特或更多)。
密码散列函数 (Cryptographic hash function) 可以将任意大小的数据映射为一个固定大小的输出。除此之外,还有一些其他特性。 一个散列函数的大概规范如下:
hash(value: array<byte>) -> vector<byte, N> (N对于该函数固定)SHA-1 是 Git 中使用的一种散列函数, 它可以将任意大小的输入映射为一个 160 比特(可被 40 位十六进制数表示)的输出。 下面我们用 sha1sum 命令来测试 SHA1 对几个字符串的输出:
$ printf 'hello' | sha1sumaaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d$ printf 'hello' | sha1sumaaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d$ printf 'Hello' | sha1sum f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0抽象地讲,散列函数可以被认为是一个不可逆,且看上去随机(但具确定性)的函数 (这就是 散列函数的理想模型)。 一个散列函数拥有以下特性:
确定性:对于不变的输入永远有相同的输出。
不可逆性:对于 hash(m) = h,难以通过已知的输出 h 来计算出原始输入 m。
目标碰撞抵抗性/弱无碰撞:对于一个给定输入 m_1,难以找到 m_2 != m_1 且 hash(m_1) = hash(m_2)。
碰撞抵抗性/强无碰撞:难以找到一组满足 hash(m_1) = hash(m_2) 的输入 m_1, m_2(该性质严格强于目标碰撞抵抗性)。
注:虽然 SHA-1 还可以用于特定用途,但它已经 不再被认为 是一个强密码散列函数。 你可参照 密码散列函数的生命周期 这个表格了解一些散列函数是何时被发现弱点及破解的。 请注意,针对应用推荐特定的散列函数超出了本课程内容的范畴。 如果选择散列函数对于你的工作非常重要,请先系统学习信息安全及密码学。
Git 中的内容寻址存储(Content-addressed storage):散列函数 是一个宽泛的概念(存在非密码学的散列函数),那么 Git 为什么要特意使用密码散列函数?
文件的信息摘要(Message digest):像 Linux ISO 这样的软件可以从非官方的(有时不太可信的)镜像站下载,所以需要设法确认下载的软件和官方一致。 官方网站一般会在(指向镜像站的)下载链接旁边备注安装文件的哈希值。 用户从镜像站下载安装文件后可以对照公布的哈希值来确定安装文件没有被篡改。
承诺机制(Commitment scheme): 假设我希望承诺一个值,但之后再透露它—— 比如在没有一个可信的、双方可见的硬币的情况下在我的脑海中公平的“扔一次硬币”。 我可以选择一个值 r = random(),并和你分享它的哈希值 h = sha256(r)。 这时你可以开始猜硬币的正反:我们一致同意偶数 r 代表正面,奇数 r 代表反面。 你猜完了以后,我告诉你值 r 的内容,得出胜负。同时你可以使用 sha256(r) 来检查我分享的哈希值 h 以确认我没有作弊。
密钥生成函数 (Key Derivation Functions) 作为密码散列函数的相关概念,被应用于包括生成固定长度,可以使用在其他密码算法中的密钥等方面。 为了对抗穷举法攻击,密钥生成函数通常较慢。
从密码生成可以在其他加密算法中使用的密钥,比如对称加密算法(见下)。
存储登录凭证时不可直接存储明文密码。
正确的方法是针对每个用户随机生成一个 盐 salt = random(), 并存储盐,以及密钥生成函数对连接了盐的明文密码生成的哈希值 KDF(password + salt)。
在验证登录请求时,使用输入的密码连接存储的盐重新计算哈希值 KDF(input + salt),并与存储的哈希值对比。
说到加密,可能你会首先想到隐藏明文信息。对称加密使用以下几个方法来实现这个功能:
keygen() -> key (这是一个随机方法) encrypt(plaintext: array<byte>, key) -> array<byte> (输出密文)decrypt(ciphertext: array<byte>, key) -> array<byte> (输出明文)加密方法 encrypt() 输出的密文 ciphertext 很难在不知道 key 的情况下得出明文 plaintext。
解密方法 decrypt() 有明显的正确性。因为功能要求给定密文及其密钥,解密方法必须输出明文:decrypt(encrypt(m, k), k) = m。
AES 是现在常用的一种对称加密系统。
key = KDF(passphrase),然后存储 encrypt(file, key)。非对称加密的“非对称”代表在其环境中,使用两个具有不同功能的密钥: 一个是私钥(private key),不向外公布;另一个是公钥(public key),公布公钥不像公布对称加密的共享密钥那样可能影响加密体系的安全性。
非对称加密使用以下几个方法来实现加密/解密(encrypt/decrypt),以及签名/验证(sign/verify):
keygen() -> (public key, private key) (这是一个随机方法) encrypt(plaintext: array<byte>, public key) -> array<byte> (输出密文)decrypt(ciphertext: array<byte>, private key) -> array<byte> (输出明文) sign(message: array<byte>, private key) -> array<byte> (生成签名)verify(message: array<byte>, signature: array<byte>, public key) -> bool (验证签名是否是由和这个公钥相关的私钥生成的)非对称的加密/解密方法和对称的加密/解密方法有类似的特征。
信息在非对称加密中使用 公钥 加密, 且输出的密文很难在不知道 私钥 的情况下得出明文。
解密方法 decrypt() 有明显的正确性。 给定密文及私钥,解密方法一定会输出明文: decrypt(encrypt(m, public key), private key) = m。
对称加密和非对称加密可以类比为机械锁。 对称加密就好比一个防盗门:只要是有钥匙的人都可以开门或者锁门。 非对称加密好比一个可以拿下来的挂锁。你可以把打开状态的挂锁(公钥)给任何一个人并保留唯一的钥匙(私钥)。这样他们将给你的信息装进盒子里并用这个挂锁锁上以后,只有你可以用保留的钥匙开锁。
签名/验证方法具有和书面签名类似的特征。
在不知道 私钥 的情况下,不管需要签名的信息为何,很难计算出一个可以使 verify(message, signature, public key) 返回为真的签名。
对于使用私钥签名的信息,验证方法验证和私钥相对应的公钥时一定返回为真: verify(message, sign(message, private key), public key) = true。
PGP 电子邮件加密:用户可以将所使用的公钥在线发布,比如:PGP 密钥服务器或 Keybase。任何人都可以向他们发送加密的电子邮件。
软件签名:Git 支持用户对提交(commit)和标签(tag)进行 GPG 签名。任何人都可以使用软件开发者公布的签名公钥验证下载的已签名软件。
非对称加密面对的主要挑战是,如何分发公钥并对应现实世界中存在的人或组织。
Signal 的信任模型是,信任用户第一次使用时给出的身份(trust on first use),同时支持用户线下(out-of-band)、面对面交换公钥(Signal 里的 safety number)。
PGP 使用的是 信任网络。简单来说,如果我想加入一个信任网络,则必须让已经在信任网络中的成员对我进行线下验证,比如对比证件。验证无误后,信任网络的成员使用私钥对我的公钥进行签名。这样我就成为了信任网络的一部分。只要我使用签名过的公钥所对应的私钥就可以证明“我是我”。
Keybase 主要使用 社交网络证明 (social proof),和一些别的精巧设计。
每个信任模型有它们各自的优点:我们(讲师)更倾向于 Keybase 使用的模型。
每个人都应该尝试使用密码管理器,比如 KeePassXC、pass 和 1Password)。
密码管理器会帮助你对每个网站生成随机且复杂(表现为高熵)的密码,并使用你指定的主密码配合密钥生成函数来对称加密它们。
你只需要记住一个复杂的主密码,密码管理器就可以生成很多复杂度高且不会重复使用的密码。密码管理器通过这种方式降低密码被猜出的可能,并减少网站信息泄露后对其他网站密码的威胁。
两步验证(2FA)要求用户同时使用密码(“你知道的信息”)和一个身份验证器(“你拥有的物品”,比如 YubiKey)来消除密码泄露或者 钓鱼攻击 的威胁。
对笔记本电脑的硬盘进行全盘加密是防止因设备丢失而信息泄露的简单且有效方法。 Linux 的cryptsetup + LUKS, Windows 的 BitLocker,或者 macOS 的 FileVault 都使用一个由密码保护的对称密钥来加密盘上的所有信息。
Signal 和 Keybase 使用非对称加密对用户提供端到端 (End-to-end) 安全性。
获取联系人的公钥非常关键。为了保证安全性,应使用线下方式验证 Signal 或者 Keybase 的用户公钥,或者信任 Keybase 用户提供的社交网络证明。
我们在 之前的一堂课 讨论了 SSH 和 SSH 密钥的使用。那么我们今天从密码学的角度来分析一下它们。
当你运行 ssh-keygen 命令,它会生成一个非对称密钥对:公钥和私钥 (public_key, private_key)。 生成过程中使用的随机数由系统提供的熵决定。这些熵可以来源于硬件事件(hardware events)等。 公钥最终会被分发,它可以直接明文存储。 但是为了防止泄露,私钥必须加密存储。ssh-keygen 命令会提示用户输入一个密码,并将它输入密钥生成函数 产生一个密钥。最终,ssh-keygen 使用对称加密算法和这个密钥加密私钥。
在实际运用中,当服务器已知用户的公钥(存储在 .ssh/authorized_keys 文件中,一般在用户 HOME 目录下),尝试连接的客户端可以使用非对称签名来证明用户的身份——这便是 挑战应答方式。 简单来说,服务器选择一个随机数字发送给客户端。客户端使用用户私钥对这个数字信息签名后返回服务器。 服务器随后使用 .ssh/authorized_keys 文件中存储的用户公钥来验证返回的信息是否由所对应的私钥所签名。这种验证方式可以有效证明试图登录的用户持有所需的私钥。
去年的讲稿: 更注重于计算机用户可以如何增强隐私保护和安全
Cryptographic Right Answers: 解答了在一些应用环境下“应该使用什么加密?”的问题
修改按键映射,后台进程,备份,api,vpn,docker,github等
作为一名程序员,键盘是你的主要输入工具。它像计算机里的其他部件一样是可配置的,而且值得你在这上面花时间。
一个很常见的配置是修改键位映射。通常这个功能由在计算机上运行的软件实现。当某一个按键被按下,软件截获键盘发出的按键事件(keypress event)并使用另外一个事件取代。比如:
将 Caps Lock 映射为 Ctrl 或者 Escape:Caps Lock 使用了键盘上一个非常方便的位置而它的功能却很少被用到,所以我们(讲师)非常推荐这个修改;
将 PrtSc 映射为播放/暂停:大部分操作系统支持播放/暂停键;
交换 Ctrl 和 Meta 键(Windows 的徽标键或者 Mac 的 Command 键)。
你也可以将键位映射为任意常用的指令。软件监听到特定的按键组合后会运行设定的脚本。
打开一个新的终端或者浏览器窗口;
输出特定的字符串,比如:一个超长邮件地址或者 MIT ID;
使计算机或者显示器进入睡眠模式。
甚至更复杂的修改也可以通过软件实现:
映射按键顺序,比如:按 Shift 键五下切换大小写锁定;
区别映射单点和长按,比如:单点 Caps Lock 映射为 Escape,而长按 Caps Lock 映射为 Ctrl;
对不同的键盘或软件保存专用的映射配置。
下面是一些修改键位映射的软件:
macOS - karabiner-elements, skhd 或者 BetterTouchTool
Windows - 控制面板,AutoHotkey 或者 SharpKeys
QMK - 如果你的键盘支持定制固件,QMK 可以直接在键盘的硬件上修改键位映射。保留在键盘里的映射免除了在别的机器上的重复配置。
即便守护进程(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。
# /etc/systemd/system/myapp.service[Unit]# 配置文件描述Description=My Custom App# 在网络服务启动后启动该进程After=network.target [Service]# 运行该进程的用户User=foo# 运行该进程的用户组Group=foo# 运行该进程的根目录WorkingDirectory=/home/foo/projects/mydaemon# 开始该进程的命令ExecStart=/usr/bin/local/python3.7 app.py# 在出现错误时重启该进程Restart=on-failure [Install]# 相当于Windows的开机启动。即使GUI没有启动,该进程也会加载并运行WantedBy=multi-user.target# 如果该进程仅需要在GUI活动时运行,这里应写作:# WantedBy=graphical.target# graphical.target在multi-user.target的基础上运行和GUI相关的服务如果你只是想定期运行一些程序,可以直接使用 cron。它是一个系统内置的,用来执行定期任务的守护进程。
现在的软件系统一般由很多模块化的组件构建而成。你使用的操作系统可以通过一系列共同的方式使用不同的文件系统上的相似功能。比如当你使用 touch 命令创建文件的时候,touch 使用系统调用(system call)向内核发出请求。内核再根据文件系统,调用特有的方法来创建文件。这里的问题是,UNIX 文件系统在传统上是以内核模块的形式实现,导致只有内核可以进行文件系统相关的调用。
FUSE(用户空间文件系统)允许运行在用户空间上的程序实现文件系统调用,并将这些调用与内核接口联系起来。在实践中,这意味着用户可以在文件系统调用中实现任意功能。
FUSE 可以用于实现如:一个将所有文件系统操作都使用 SSH 转发到远程主机,由远程主机处理后返回结果到本地计算机的虚拟文件系统。这个文件系统里的文件虽然存储在远程主机,对于本地计算机上的软件而言和存储在本地别无二致。sshfs 就是一个实现了这种功能的 FUSE 文件系统。
一些有趣的 FUSE 文件系统包括:
sshfs:使用 SSH 连接在本地打开远程主机上的文件
rclone:将 Dropbox、Google Drive、Amazon S3、或者 Google Cloud Storage 一类的云存储服务挂载为本地文件系统
gocryptfs:覆盖在加密文件上的文件系统。文件以加密形式保存在磁盘里,但该文件系统挂载后用户可以直接从挂载点访问文件的明文
kbfs:分布式端到端加密文件系统。在这个文件系统里有私密(private),共享(shared),以及公开(public)三种类型的文件夹
borgbackup:方便用户浏览删除重复数据后的压缩加密备份
任何没有备份的数据都可能在一个瞬间永远消失。复制数据很简单,但是可靠地备份数据很难。下面列举了一些关于备份的基础知识,以及一些常见做法容易掉进的陷阱。
首先,复制存储在同一个磁盘上的数据不是备份,因为这个磁盘是一个单点故障(single point of failure)。这个磁盘一旦出现问题,所有的数据都可能丢失。放在家里的外置磁盘因为火灾、抢劫等原因可能会和源数据一起丢失,所以是一个弱备份。推荐的做法是将数据备份到不同的地点存储。
同步方案也不是备份。即使方便如 Dropbox 或者 Google Drive,当数据在本地被抹除或者损坏,同步方案可能会把这些“更改”同步到云端。同理,像 RAID 这样的磁盘镜像方案也不是备份。它不能防止文件被意外删除、损坏、或者被勒索软件加密。
有效备份方案的几个核心特性是:版本控制,删除重复数据,以及安全性。对备份的数据实施版本控制保证了用户可以从任何记录过的历史版本中恢复数据。在备份中检测并删除重复数据,使其仅备份增量变化可以减少存储开销。在安全性方面,作为用户,你应该考虑别人需要有什么信息或者工具才可以访问或者完全删除你的数据及备份。最后一点,不要盲目信任备份方案。用户应该经常检查备份是否可以用来恢复数据。
备份不限制于备份在本地计算机上的文件。云端应用的重大发展使得我们很多的数据只存储在云端。当我们无法登录这些应用,在云端存储的网络邮件,社交网络上的照片,流媒体音乐播放列表,以及在线文档等等都会随之丢失。用户应该有这些数据的离线备份,而且已经有项目可以帮助下载并存储它们。
如果想要了解更多具体内容,请参考本课程 2019 年关于备份的 课堂笔记。
关于如何使用计算机有效率地完成 本地 任务,我们这堂课已经介绍了很多方法。这些方法在互联网上其实也适用。大多数线上服务提供的 API(应用程序接口)让你可以通过编程方式来访问这些服务的数据。比如,美国国家气象局就提供了一个可以从 shell 中获取天气预报的 API。
这些 API 大多具有类似的格式。它们的结构化 URL 通常使用 api.service.com 作为根路径,用户可以访问不同的子路径来访问需要调用的操作,以及添加查询参数使 API 返回符合查询参数条件的结果。
以美国天气数据为例,为了获得某个地点的天气数据,你可以发送一个 GET 请求(比如使用 curl)到 https://api.weather.gov/points/42.3604,-71.094。返回中会包括一系列用于获取特定信息(比如小时预报、气象观察站信息等)的 URL。通常这些返回都是 JSON 格式,你可以使用 jq 等工具来选取需要的部分。
有些需要认证的 API 通常要求用户在请求中加入某种私密令牌(secret token)来完成认证。请阅读你想访问的 API 所提供的文档来确定它请求的认证方式,但是其实大多数 API 都会使用 OAuth。OAuth 通过向用户提供一系列仅可用于该 API 特定功能的私密令牌进行校验。因为使用了有效 OAuth 令牌的请求在 API 看来就是用户本人发出的请求,所以请一定保管好这些私密令牌。否则其他人就可以冒用你的身份进行任何你可以在这个 API 上进行的操作。
IFTTT 这个网站可以将很多 API 整合在一起,让某 API 发生的特定事件触发在其他 API 上执行的任务。IFTTT 的全称 If This Then That 足以说明它的用法,比如在检测到用户的新推文后,自动发布在其他平台。但是你可以对它支持的 API 进行任意整合,所以试着来设置一下任何你需要的功能吧!
命令行工具的用法千差万别,阅读 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 的标志参数。
大部分人适应了 Windows、macOS、以及 Ubuntu 默认的“拖拽”式窗口管理器。这些窗口管理器的窗口一般就堆在屏幕上,你可以拖拽改变窗口的位置、缩放窗口、以及让窗口堆叠在一起。这种堆叠式(floating/stacking)管理器只是窗口管理器中的一种。特别在 Linux 中,有很多种其他的管理器。
平铺式(tiling)管理器就是一个常见的替代。顾名思义,平铺式管理器会把不同的窗口像贴瓷砖一样平铺在一起而不和其他窗口重叠。这和 tmux 管理终端窗口的方式类似。平铺式管理器按照写好的布局显示打开的窗口。如果只打开一个窗口,它会填满整个屏幕。新开一个窗口的时候,原来的窗口会缩小到比如三分之二或者三分之一的大小来腾出空间。打开更多的窗口会让已有的窗口进一步调整。
就像 tmux 那样,平铺式管理器可以让你在完全不使用鼠标的情况下使用键盘切换、缩放、以及移动窗口。它们值得一试!
VPN 现在非常火,但我们不清楚这是不是因为 一些好的理由。你应该了解 VPN 能提供的功能和它的限制。使用了 VPN 的你对于互联网而言,最好的情况 下也就是换了一个网络供应商(ISP)。所有你发出的流量看上去来源于 VPN 供应商的网络而不是你的“真实”地址,而你实际接入的网络只能看到加密的流量。
虽然这听上去非常诱人,但是你应该知道使用 VPN 只是把原本对网络供应商的信任放在了 VPN 供应商那里——网络供应商 能看到的,VPN 供应商 也都能看到。如果相比网络供应商你更信任 VPN 供应商,那当然很好。反之,则连接 VPN 的价值不明确。机场的不加密公共热点确实不可以信任,但是在家庭网络环境里,这个差异就没有那么明显。
你也应该了解现在大部分包含用户敏感信息的流量已经被 HTTPS 或者 TLS 加密。这种情况下你所处的网络环境是否“安全”不太重要:供应商只能看到你和哪些服务器在交谈,却不能看到你们交谈的内容。
这一切的大前提都是“最好的情况”。曾经发生过 VPN 提供商错误使用弱加密或者直接禁用加密的先例。另外,有些恶意的或者带有投机心态的供应商会记录和你有关的所有流量,并很可能会将这些信息卖给第三方。找错一家 VPN 经常比一开始就不用 VPN 更危险。
MIT 向有访问校内资源需求的成员开放自己运营的 VPN。如果你也想自己配置一个 VPN,可以了解一下 WireGuard 以及 Algo。
你在职业生涯中大概率会编写各种各样的文档。在很多情况下这些文档需要使用标记来增加可读性,比如:插入粗体或者斜体内容,增加页眉、超链接、以及代码片段。
在不使用 Word 或者 LaTeX 等复杂工具的情况下,你可以考虑使用 Markdown 这个轻量化的标记语言(markup language)。你可能已经见过 Markdown 或者它的一个变种。很多环境都支持并使用 Markdown 的一些子功能。
Markdown 致力于将人们编写纯文本时的一些习惯标准化。比如:
用 * 包围的文字表示强调(斜体),或者用 ** 表示特别强调(粗体);
以 # 开头的行是标题,# 的数量表示标题的级别,比如:##二级标题;
以 - 开头代表一个无序列表的元素。一个数字加 .(比如 1.)代表一个有序列表元素;
反引号 ```(backtick)包围的文字会以 代码字体 显示。如果要显示一段代码,可以在每一行前加四个空格缩进,或者使用三个反引号包围整个代码片段:
就像这样如果要添加超链接,将 需要显示 的文字用方括号包围,并在后面紧接着用圆括号包围链接:[显示文字](指向的链接)。
Markdown 不仅容易上手,而且应用非常广泛。实际上本课程的课堂笔记和其他资料都是使用 Markdown 编写的。点击 这个链接 可以看到本页面的原始 Markdown 内容。
Hammerspoon 是面向 macOS 的一个桌面自动化框架。它允许用户编写和操作系统功能挂钩的 Lua 脚本,从而与键盘、鼠标、窗口、文件系统等交互。
下面是 Hammerspoon 的一些示例应用:
绑定移动窗口到的特定位置的快捷键
创建可以自动将窗口整理成特定布局的菜单栏按钮
在你到实验室以后,通过检测所连接的 WiFi 网络自动静音扬声器
在你不小心拿了朋友的充电器时弹出警告
从用户的角度,Hammerspoon 可以运行任意 Lua 代码,绑定菜单栏按钮、按键、或者事件。Hammerspoon 提供了一个全面的用于和系统交互的库,因此它能没有限制地实现任何功能。你可以从头编写自己的 Hammerspoon 配置,也可以结合别人公布的配置来满足自己的需求。
Getting Started with Hammerspoon:Hammerspoon 官方教程
Sample configurations:Hammerspoon 官方示例配置
Anish’s Hammerspoon config:Anish 的 Hammerspoon 配置
在你的计算机启动时,BIOS 或者 UEFI 会在加载操作系统之前对硬件系统进行初始化,这被称为引导(booting)。你可以通过按下计算机提示的键位组合来配置引导,比如 Press F9 to configure BIOS. Press F12 to enter boot menu。在 BIOS 菜单中你可以对硬件相关的设置进行更改,也可以在引导菜单中选择从硬盘以外的其他设备加载操作系统——比如 Live USB。
Live USB 是包含了完整操作系统的闪存盘。Live USB 的用途非常广泛,包括:
作为安装操作系统的启动盘;
在不将操作系统安装到硬盘的情况下,直接运行 Live USB 上的操作系统;
对硬盘上的相同操作系统进行修复;
恢复硬盘上的数据。
Live USB 通过在闪存盘上 写入 操作系统的镜像制作,而写入不是单纯的往闪存盘上复制 .iso 文件。你可以使用 UNetbootin 、Rufus 等 Live USB 写入工具制作。
虚拟机(Virtual Machine)以及容器化(containerization)等工具可以帮助你模拟一个包括操作系统的完整计算机系统。虚拟机可以用于创建独立的测试或者开发环境,以及用作安全测试的沙盒。
Vagrant 是一个构建和配置虚拟开发环境的工具。它支持用户在配置文件中写入比如操作系统、系统服务、需要安装的软件包等描述,然后使用 vagrant up 命令在各种环境(VirtualBox,KVM,Hyper-V 等)中启动一个虚拟机。Docker 是一个使用容器化概念的类似工具。
租用云端虚拟机可以享受以下资源的即时访问:
便宜、常开、且有公共 IP 地址的虚拟机用来托管网站等服务
有大量 CPU、磁盘、内存、以及 GPU 资源的虚拟机
超出用户可以使用的物理主机数量的虚拟机
受欢迎的 VPS 服务商有 Amazon AWS,Google Cloud、 Microsoft Azure 以及 DigitalOcean。
MIT CSAIL 的成员可以使用 CSAIL OpenStack instance 申请免费的虚拟机用于研究。
交互式记事本 可以帮助开发者进行与运行结果交互等探索性的编程。现在最受欢迎的交互式记事本环境大概是 Jupyter。它的名字来源于所支持的三种核心语言:Julia、Python、R。Wolfram Mathematica 是另外一个常用于科学计算的优秀环境。
GitHub 是最受欢迎的开源软件开发平台之一。我们课程中提到的很多工具,从 vim 到 Hammerspoon,都托管在 Github 上。向你每天使用的开源工具作出贡献其实很简单,下面是两种贡献者们经常使用的方法:
创建一个 议题(issue)。 议题可以用来反映软件运行的问题或者请求新的功能。创建议题并不需要创建者阅读或者编写代码,所以它是一个轻量化的贡献方式。高质量的问题报告对于开发者十分重要。在现有的议题发表评论也可以对项目的开发作出贡献。
使用 拉取请求(pull request) 提交代码更改。由于涉及到阅读和编写代码,提交拉取请求总的来说比创建议题更加深入。拉取请求是请求别人把你自己的代码拉取(且合并)到他们的仓库里。很多开源项目仅允许认证的管理者管理项目代码,所以一般需要 复刻(fork) 这些项目的上游仓库(upstream repository),在你的 Github 账号下创建一个内容完全相同但是由你控制的复刻仓库。这样你就可以在这个复刻仓库自由创建新的分支并推送修复问题或者实现新功能的代码。完成修改以后再回到开源项目的 Github 页面 创建一个拉取请求。
提交请求后,项目管理者会和你交流拉取请求里的代码并给出反馈。如果没有问题,你的代码会和上游仓库中的代码合并。很多大的开源项目会提供贡献指南,容易上手的议题,甚至专门的指导项目来帮助参与者熟悉这些项目。
首先,不清楚你是不是真的需要了解这些更底层的话题。 当你开始编写更加底层的代码,比如实现或修改内核的时候,这些内容是很重要的。除了其他课程中简要介绍过的进程和信号量之外,大部分话题都不相关。
学习资源:
MIT’s 6.828 class - 研究生阶段的操作系统课程(课程资料是公开的)。
现代操作系统 第四版(Modern Operating Systems 4th ed) - 作者是 Andrew S. Tanenbaum 这本书对上述很多概念都有很好的描述。
FreeBSD 的设计与实现(The Design and Implementation of the FreeBSD Operating System) - 关于 FreeBSD OS 不错的资源(注意,FreeBSD OS 不是 Linux)。
其他的指南例如 用 Rust 写操作系统 这里用不同的语言逐步实现了内核,主要用于教学的目的。
值得优先学习的内容:
多去使用键盘,少使用鼠标。这一目标可以通过多加利用快捷键,更换界面等来实现。
学好编辑器。作为程序员你大部分时间都是在编辑文件,因此值得学好这些技能。
学习怎样去自动化或简化工作流程中的重复任务。因为这会节省大量的时间。
学习像 Git 之类的版本控制工具并且知道如何与 GitHub 结合,以便在现代的软件项目中协同工作。
通常来说,Bash 脚本对于简短的一次性脚本有效,比如当你想要运行一系列的命令的时候。但是 Bash 脚本有一些比较奇怪的地方,这使得大型程序或脚本难以用 Bash 实现:
Bash 对于简单的使用情形没什么问题,但是很难对于所有可能的输入都正确。例如,脚本参数中的空格会导致 Bash 脚本出错。
Bash 对于代码重用并不友好。因此,重用你先前已经写好的代码很困难。通常 Bash 中没有软件库的概念。
Bash 依赖于一些像 $? 或 $@ 的特殊字符指代特殊的值。其他的语言却会显式地引用,比如 exitCode 或 sys.args。
因此,对于大型或者更加复杂的脚本我们推荐使用更加成熟的脚本语言例如 Python 和 Ruby。 你可以找到很多用这些语言编写的,用来解决常见问题的在线库。 如果你发现某种语言实现了你所需要的特定功能库,最好的方式就是直接去使用那种语言。
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)以避免影响全局环境。
性能分析方面相当有用和简单工具是 print timing。你只需手动计算代码不同部分之间花费的时间。通过重复执行此操作,你可以有效地对代码进行二分法搜索,并找到花费时间最长的代码段。
对于更高级的工具, Valgrind 的 Callgrind 可让你运行程序并计算所有的时间花费以及所有调用堆栈(即哪个函数调用了另一个函数)。然后,它会生成带注释的代码版本,其中包含每行花费的时间。但是,它会使程序运行速度降低一个数量级,并且不支持线程。其他的, perf 工具和其他特定语言的采样性能分析器可以非常快速地输出有用的数据。Flamegraphs 是对采样分析器结果的可视化工具。你还可以使用针对特定编程语言或任务的工具。例如,对于 Web 开发而言,Chrome 和 Firefox 内置的开发工具具有出色的性能分析器。
有时,代码中最慢的部分是系统等待磁盘读取或网络数据包之类的事件。在这些情况下,需要检查根据硬件性能估算的理论速度是否不偏离实际数值,也有专门的工具来分析系统调用中的等待时间,包括用于用户程序内核跟踪的 eBPF 。如果需要低级的性能分析, bpftrace 值得一试。
我们钟爱的插件主要与安全性与可用性有关:
uBlock Origin - 是一个 用途广泛(wide-spectrum) 的拦截器,它不仅可以拦截广告,还可以拦截第三方的页面,也可以拦截内部脚本和其他种类资源的加载。如果你打算花更多的时间去配置,前往 中等模式(medium mode) 或者 强力模式(hard mode)。在你调整好设置之前一些网站会停止工作,但是这些配置会显著提高你的网络安全水平。另外, 简易模式(easy mode) 作为默认模式已经相当不错了,可以拦截大部分的广告和跟踪,你也可以自定义规则来拦截网站对象。
Stylus - 是 Stylish 的分支(不要使用 Stylish,它会 窃取浏览记录),这个插件可让你将自定义 CSS 样式加载到网站。使用 Stylus,你可以轻松地自定义和修改网站的外观。可以删除侧边框,更改背景颜色,更改文字大小或字体样式。这可以使你经常访问的网站更具可读性。此外,Stylus 可以找到其他用户编写并发布在 userstyles.org 中的样式。大多数常用的网站都有一个或几个深色主题样式。
全页屏幕捕获 - 内置于 Firefox 和 Chrome 扩展程序 中。这些插件提供完整的网站截图,通常比打印要好用。
多账户容器 - 该插件使你可以将 Cookie 分为“容器”,从而允许你以不同的身份浏览 web 网页并且/或确保网站无法在它们之间共享信息。
密码集成管理器 - 大多数密码管理器都有浏览器插件,这些插件帮你将登录凭据输入网站的过程不仅方便,而且更加安全。与简单复制粘贴用户名和密码相比,这些插件将首先检查网站域是否与列出的条目相匹配,以防止冒充网站的网络钓鱼窃取登录凭据。
在数据整理那一节课程中,我们没有时间讨论一些数据整理工具,包括分别用于 JSON 和 HTML 数据的专用解析器, jq 和 pup。Perl 语言是另一个更高级的可以用于数据整理管道的工具。另一个技巧是使用 column -t 命令,可以将空格文本(不一定对齐)转换为对齐的文本。
一般来说,vim 和 Python 是两个不常规的数据整理工具。对于某些复杂的多行转换,vim 宏是非常有用的工具。你可以记录一系列操作,并根据需要重复执行多次,例如,在编辑的 讲义(去年 视频)中,有一个示例是使用 vim 宏将 XML 格式的文件转换为 JSON。
对于通常以 CSV 格式显示的表格数据, Python pandas 库是一个很棒的工具。不仅因为它能让复杂操作的定义(如分组依据,联接或过滤器)变得非常容易,而且还便于根据不同属性绘制数据。它还支持导出多种表格格式,包括 XLS,HTML 或 LaTeX。另外,R 语言(一种有争议的 不好 的语言)具有很多功能,可以计算数据的统计数字,这在管道的最后一步中非常有用。 ggplot2 是 R 中很棒的绘图库。
Docker 基于容器这个更为概括的概念。关于容器和虚拟机之间最大的不同是,虚拟机会执行整个的 OS 栈,包括内核(即使这个内核和主机内核相同)。与虚拟机不同,容器避免运行其他内核实例,而是与主机分享内核。在 Linux 环境中,有 LXC 机制来实现,并且这能使一系列分离的主机像是在使用自己的硬件启动程序,而实际上是共享主机的硬件和内核。因此容器的开销小于完整的虚拟机。
另一方面,容器的隔离性较弱而且只有在主机运行相同的内核时才能正常工作。例如,如果你在 macOS 上运行 Docker,Docker 需要启动 Linux 虚拟机去获取初始的 Linux 内核,这样的开销仍然很大。最后,Docker 是容器的特定实现,它是为软件部署而定制的。基于这些,它有一些奇怪之处:例如,默认情况下,Docker 容器在重启之间不会有以任何形式的存储。
关于 Linux 发行版,尽管有相当多的版本,但大部分发行版在大多数使用情况下的表现是相同的。 可以使用任何发行版去学习 Linux 与 UNIX 的特性和其内部工作原理。 发行版之间的根本区别是发行版如何处理软件包更新。 某些版本,例如 Arch Linux 采用滚动更新策略,用了最前沿的软件包(bleeding-edge),但软件可能并不稳定。另外一些发行版(如 Debian,CentOS 或 Ubuntu LTS)其更新策略要保守得多,因此更新的内容会更稳定,但会牺牲一些新功能。我们建议你使用 Debian 或 Ubuntu 来获得简单稳定的台式机和服务器体验。
Mac OS 是介于 Windows 和 Linux 之间的一个操作系统,它有很漂亮的界面。但是,Mac OS 是基于 BSD 而不是 Linux,因此系统的某些部分和命令是不同的。 另一种值得体验的是 FreeBSD。虽然某些程序不能在 FreeBSD 上运行,但与 Linux 相比,BSD 生态系统的碎片化程度要低得多,并且说明文档更加友好。 除了开发 Windows 应用程序或需要使用某些 Windows 系统更好支持的功能(例如对游戏的驱动程序支持)外,我们不建议使用 Windows。
对于双系统,我们认为最有效的是 macOS 的 bootcamp,长期来看,任何其他组合都可能会出现问题,尤其是当你结合了其他功能比如磁盘加密。
我们三个都使用 vim 作为我们的主要编辑器。但是 Emacs 也是一个不错的选择,你可以两者都尝试,看看那个更适合你。Emacs 不使用 vim 的模式编辑,但是这些功能可以通过 Emacs 插件像 Evil 或 Doom Emacs 来实现。 Emacs 的优点是可以用 Lisp 语言进行扩展(Lisp 比 vim 默认的脚本语言 vimscript 要更好用)。
课程的一些经验可以直接用于机器学习程序。 就像许多科学学科一样,在机器学习中,你需要进行一系列实验,并检查哪些数据有效,哪些无效。 你可以使用 Shell 轻松快速地搜索这些实验结果,并且以合理的方式汇总。这意味着需要在限定时间内或使用特定数据集的情况下,检查所有实验结果。通过使用 JSON 文件记录实验的所有相关参数,使用我们在本课程中介绍的工具,这件事情可以变得极其简单。 最后,如果你不使用集群提交你的 GPU 作业,那你应该研究如何使该过程自动化,因为这是一项非常耗时的任务,会消耗你的精力。
更多的窍门:
插件 - 花时间去探索插件。有很多不错的插件修复了 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 可以用于更改上次搜索的关键字。
双因子验证(Two Factor Authentication 2FA)在密码之上为帐户增加了一层额外的保护。为了登录,你不仅需要知道密码,还必须以某种方式“证明”可以访问某些硬件设备。最简单的情形是可以通过接收手机的 SMS 来实现(尽管 SMS 2FA 存在 已知问题)。我们推荐使用 YubiKey 之类的 U2F 方案。
2020 的浏览器现状是,大部分的浏览器都与 Chrome 类似,因为它们都使用同样的引擎(Blink)。Microsoft Edge 同样基于 Blink,而 Safari 则 基于 WebKit(与 Blink 类似的引擎),这些浏览器仅仅是更糟糕的 Chrome 版本。不管是在性能还是可用性上,Chrome 都是一款很不错的浏览器。如果你想要替代品,我们推荐 Firefox。Firefox 与 Chrome 的在各方面不相上下,并且在隐私方面更加出色。 有一款目前还没有完成的叫 Flow 的浏览器,它实现了全新的渲染引擎,有望比现有引擎速度更快。
NOTE
还有各种题目都没做
分享你的想法,与大家交流讨论
Twikoo 加载失败
Twikoo 脚本加载失败