Tutorial for Missing Semester - MIT
1 | This notes is translated from [The Missing Semester of Your CS Education](https://missing.csail.mit.edu/), 更多详情请参看原文。 |
Topic 1 Shell
如今我们使用的大部分都是GUI,而有些时候GUI并不能帮我们完成所有的事情,这时候我们不得不回到Shell。
1 | echo hello |
Shell会执行echo,将指定的hello参数打印。
1 | missing:~$ echo $PATH |
$PATH 指的是环境变量,如果你是Windows系统,你可以在此电脑>属性>高级系统设置里面找到它。通常来说,配置全局环境就与环境变量有关。
shell 中的路径是一组被分割的目录,在 Linux 和 macOS 上使用 / 分割,而在Windows上是 \。假设你进入了一个服务器,在根目录下使用ls
命令可能会发现什么都没有,这是因为你没有进入磁盘的根目录。在路径中,.
表示的是当前目录,而 ..
表示上级目录。
1 | missing:~$ pwd |
还有一些常用命令:
pwd
命令查看我们当前所在的路径
cd
进入指定的路径
ls
命令查看指定目录下包含哪些文件
1 | ls 显示当前目录下包含的文件 |
man
命令查看程序名的文档
1 | man ls |
mv
用于重命名或移动文件
cp
用于拷贝文件
mkdir
新建文件夹
cat
连接文件,打印输出
我们可以对程序进行重定向。最简单的重定向是 < file
和 > file
。这两个命令可以将程序的输入输出流分别重定向到文件:
1 | missing:~$ echo hello > hello.txt |
使用 >>
来向一个文件追加内容。使用管道( pipes ),我们能够更好的利用文件重定向。|
操作符允许我们将一个程序的输出和另外一个程序的输入连接起来。需要注意的是,|
、>
、和 <
是通过 shell 执行的,而不是被各个程序单独执行。 echo
等程序并不知道 |
的存在,它们只知道从自己的输入输出流中进行读写。
1 | missing:~$ ls -l / | tail -n1 |
另外,有些操作需要你作为root
用户才能进行,比如与操作系统有关的文件sysfs
。/sys
目录挂载了系统,你可以在/sys
目录下找到一些.sysfs
文件控制内核参数。当然,专属于Linux系统。
所以以控制屏幕亮度的程序为例,即使像下面这样写:
$ sudo find -L /sys/class/backlight -maxdepth 2 -name '*brightness*' /sys/class/backlight/thinkpad_screen/brightness $ cd /sys/class/backlight/thinkpad_screen $ sudo echo 3 > brightness An error occurred while redirecting file 'brightness' open: Permission denied
我们已经使用了sudo
命令(su是super user的意思,即root用户),系统在设置 sudo echo
前尝试打开 brightness 文件并写入,但系统拒绝了shell的操作,因为此时shell不是根用户。
1 | $ echo 3 | sudo tee brightness |
在这个程序中,打开/sys
的是tee
程序,而这个程序以root权限在运行,所以可以操作。
最后,如果你想更愉快的使用你的shell,你可以安装fish、oh-my-bash或是oh-my-zsh,通过命令行安装,网上都有很简单的教程。、
Topic 2 Shell工具和脚本
包含以bash作为脚本语言的一些基础操作,以及几种最常用的shell工具。
Topic 3 编辑器 (Vim)
目前最流行的代码编辑器无疑是VS Code,而VIM是最流行的基于命令行的编辑器。熟练使用VIM之后,编写代码可以完全脱离鼠标且效率极高,但这往往需要很久的时间进行练习。
VIM具有多种操作模式:
- 正常模式:在文件中四处移动光标进行修改
- 插入模式:插入文本
- 替换模式:替换文本
- 可视化模式(一般,行,块):选中文本块
- 命令模式:用于执行命令
我们了解VIM可以通过这样一个例子开始,在不同的操作模式下,键盘敲击的含义也不同。比如,x 在插入模式会插入字母 x,但是在正常模式 会删除当前光标所在的字母,在可视模式下则会删除选中文块。
你可以按下 ESC(退出键)从任何其他模式返回正常模式。在正常模式,键入 i 进入插入 模式,R 进入替换模式,小写的v 进入可视(一般)模式,大写的V 进入可视(行)模式,C-v (Ctrl-V, 有时也写作 ^V)进入可视(块)模式,引号: 进入命令模式。
正常模式
- 基本移动: hjkl (左, 下, 上, 右)
- 词: w (下一个词), b (词初), e (词尾)
- 行: 0 (行初), ^ (第一个非空格字符), $ (行尾)
- 屏幕: H (屏幕首行), M (屏幕中间), L (屏幕底部)
- 翻页: Ctrl-u (上翻), Ctrl-d (下翻)
- 文件: gg (文件头), G (文件尾)
- 行数: :{行数}CR 或者 {行数}G ({行数}为具体的行数)
- 杂项: % (找到配对,比如括号或者 /* */ 之类的注释对)
- 查找: f{字符}, t{字符}, F{字符}, T{字符}
- 查找/到 向前/向后 在本行的{字符}
- , / ; 用于导航匹配
- 搜索: /{正则表达式}, n / N 用于导航匹配
命令行模式
在正常模式下键入 : 进入命令行模式。 在键入 : 后,你的光标会立即跳到屏幕下方的命令行。 这个模式有很多功能,包括打开,保存,关闭文件,以及 退出 Vim。
- :q 退出(关闭窗口)
- :w 保存(写)
- :wq 保存然后退出
- :e {文件名} 打开要编辑的文件
- :ls 显示打开的缓存
- :help {标题} 打开帮助文档
- :help :w 打开 :w 命令的帮助文档
- :help w 打开 w 移动的帮助文档
可视化模式
可以用移动命令来选中。
- 可视化:v
- 可视化行: V
- 可视化块:Ctrl+v
插入模式
- i 进入插入模式
- O / o 在之上/之下插入行
- d{移动命令} 删除 {移动命令}
- 例如,dw 删除词, d$ 删除到行尾, d0 删除到行头。
- c{移动命令} 改变 {移动命令}
- 例如,cw 改变词
- 比如 d{移动命令} 再 i
- x 删除字符(等同于 dl)
- s 替换字符(等同于 xi)
- 可视化模式 + 操作
- 选中文字, d 删除 或者 c 改变
- u 撤销, C-r 重做
- y 复制 / “yank” (其他一些命令比如 d 也会复制)
- p 粘贴
- ~ 改变字符的大小写
计数和修饰语
可以用一个计数来结合“名词”和“动词”,这会执行指定操作若干次。
- 3w 向后移动三个词
- 5j 向下移动5行
- 7dw 删除7个词
可以用修饰语改变“名词”的意义。修饰语有 i,表示“内部”或者“在内”,和 a, 表示“周围”。
- ci( 改变当前括号内的内容
- ci[ 改变当前方括号内的内容
- da’ 删除一个单引号字符串, 包括周围的单引号
最后要说的是,如果你真的非常有耐心且通过长时间的练习掌握了以上这些命令,你可以在网上查找更多关于VIM的扩展资料,它远比目前介绍的要强。不过上手难度确实是很大,如果坚持不下去,还是选择VS Code吧。
Topic 4 数据整理
数据整理与两样东西密切相关:用来整理的数据,以及相关的应用场景。
Topic 5 命令行环境
信号机制
shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。
-
SIGINT
信号:输入Ctrl-C
时,shell 会发送一个SIGINT信号到进程,通知前台进程组终止进程。 -
SIGQUIT
信号:输入Ctrl-\
时,shell会发送SIGQUIT信号到进程,进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。 -
SIGTERM
信号:通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。命令是kill -TERM <PID>
。 -
SIGSTOP(SIGTSTP)
信号:输入Ctrl-Z
时,shell会发送SIGTSTP信号(terminal版本的SIGSTOP)让进程暂停。
终端多路复用
在使用命令行时通常会希望同时执行多个任务,例如运行编辑器的同时,在终端的另外一侧执行程序,这时候就可以使用终端多路复用器,如tmux。
以下是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> <空格>
在不同的面板排布间切换
别名
别名相当于一个长命令的缩写,shell会将其自动替换成原本的命令,语法如下:
1 | alias alias_name="command_to_alias arg1 arg2" |
等号的两边没有空格,因为alias
是一个shell命令,只接受一个参数。
以下是一些别名的特性应用:
1 | # 创建常用命令的缩写 |
配置文件
shell的配置通过点文件来完成,一般来说点文件的文件名以.
开头,默认是隐藏文件,直接使用ls
并不会显示它们。
管理配置文件有很多好处,比如可以移植你的工具配置,快速安装,同步配置文件,追踪配置文件的版本历史等等。不同的工具有不同的配置文件目录,可以根据自己的工具查找配置。
可以通过在线文档和手册了解工具的设置项,网上也有很多其他人的配置文件,当然,每个人的操作习惯不同,建议不要直接复制别人的配置文件。
配置文件的一个常见的痛点是它可能并不能在多种设备上生效。可以通过配置文件的if
语句解决。如果你有多个设备,你可以通过配置文件的管理实现特定配置需求。
远端设备
最常用的工具是SSH。可以使用ssh连接到其他服务器,例如ssh foo@bar.mit.edu
。服务器可以通过 URL 指定(例如bar.mit.edu
),也可以使用 IP 指定(例如foobar@192.168.1.42
)。
ssh可以直接执行远程命令,ssh foobar@server ls
可以直接在用foobar的命令下执行 ls
命令。 想要配合管道来使用也可以, ssh foobar@server ls | grep PATTERN
会在本地查询远端 ls
的输出而 ls | ssh foobar@server grep PATTERN
会在远端对本地 ls
输出的结果进行查询。
ssh有基于密钥的验证机制,我们只需要向服务器证明客户端持有对应的私钥,而不需要公开其私钥。这样就可以避免每次登录都输入密码的麻烦了。
使用ssh-keygen
命令可以生成一对密钥。要检查是否持有密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key
。ssh 会查询 .ssh/authorized_keys
来确认那些用户可以被允许登录。
通过ssh复制文件
ssh+tee
, 最简单的方法是执行ssh
命令,然后通过这样的方法利用标准输入实现cat localfile | ssh remote_server tee serverfile
。scp
:当需要拷贝大量的文件或目录时,使用scp
命令则更加方便,因为它可以方便的遍历相关路径。语法如下:scp path/to/local_file remote_host:path/to/remote_file;
rsync
对scp
进行了改进,它可以检测本地和远端的文件以防止重复拷贝。它还可以提供一些诸如符号连接、权限管理等精心打磨的功能。甚至还可以基于--partial
标记实现断点续传。rsync
的语法和scp
类似;
端口转发
在本地设备上的一个端口建立连接,转发到远程端口上。例如在远程服务器上运行Jupyter notebook,监听8888端口,建立从本地9999端口的转发。使用 ssh -L 9999:localhost:8888 foobar@remote_server
。这样只需要访问本地的 localhost:9999
即可。
ssh配置
可以使用~/.ssh/config
文件创建别名,这个文件也可以被当作配置文件,不过请注意,最好不要将它公开到互联网上,因为这会暴露服务器的各种信息。服务器侧的配置通常放在/etc/ssh/sshd_config
。
shell框架
可以通过框架来改进shell,让其拥有更友好的功能,包括但不限于向右对齐、命令语法高亮、历史子串查询、选项补全、智能补全、提示符主题等等,可以在网上自行了解,按需选取安装。
Topic 6 版本控制(Git)
首先要明确,Git和GitHub完全是两回事!!
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。版本控制系统有很多, 其事实上的标准则是 Git。
Git 的数据模型
先用伪代码的形式看下数据模型:
1 | // 文件就是一组数据 |
blob object
当我们提交一些文件的时候,git会创建第一类对像blob object,每一个blob object对应提交的每个文件。
tree object
另外,git会创建一种对象tree object,包含项目中所有文件的列表,其中包含分配的blob object的指针,通过这种机制,git将文件与blob object相关联。
commit object
最后git会创建一个对象commit object,具有指向它的tree object的指针以及一些其他信息。
暂存区
暂存区允许我们指定下次快照中要包括哪些改动,比如你可以把需要提交的文件逐个放入暂存取,然后进行提交。通过这个机制,我们可以创建不同的独立提交。
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 mergegit clone
: 从远端下载仓库
撤销
git commit --amend
: 编辑提交的内容或信息git reset HEAD <file>
: 恢复暂存的文件git checkout -- <file>
: 丢弃修改git restore
: git2.32版本后取代git reset 进行许多撤销操作
Git也可以通过VS Code使用。
Topic 7 调试及性能分析
调试
调试的第一种方法比较简单,在可能出现问题的地方添加一些打印语句,比如print、console等等,直到发现问题。
第二种方法是使用日志,可以将日志写入文件,socket甚至是发送到远端服务器;日志可以支持严重等级,比如哪些是ERROR、哪些是WARN;另外,日志包含的信息很多,很有可能直接包含了问题信息。
在与Web 服务器、数据库或消息代理等第三方依赖交互的时候,阅读日志非常重要。不过大多数的程序都会把日志保存在系统的某个地方。根据不同的系统有不同的保存位置。如果希望将日志加入到系统日志中,可以使用logger
(一个shell程序):
1 | logger "Hello Logs" |
如果通过打印已经不足以满足调试的需求了,可以使用调试器。调试器可以设置断点,从断点开始逐步执行程序,即时查看变量的值,满足条件时暂停程序等。
这里介绍一下Python的调试器pdb
。
- l(ist) - 显示当前行附近的11行或继续执行之前的显示;
- s(tep) - 执行当前行,并在第一个可能的地方停止;
- n(ext) - 继续执行直到当前函数的下一条语句或者 return 语句;
- b(reak) - 设置断点(基于传入的参数);
- p(rint) - 在当前上下文对表达式求值并打印结果。还有一个命令是pp ,它使用 pprint 打印;
- r(eturn) - 继续执行直到当前函数返回;
- q(uit) - 退出调试器。
此外,针对不同的程序,可以使用不同的开发工具,请针对具体情况具体分析。
静态分析
静态分析会将程序的源码作为输入,基于编码规则对其进行分析,并对代码的正确性进行推理。可以使用静态分析工具。大多数的编辑器和 IDE 都支持在编辑界面显示这些工具的分析结果、高亮有警告和错误的位置。 这个过程通常称为 code linting 。风格检查或安全检查的结果同样也可以进行相应的显示。
不同的开发工具有不同的静态分析工具,网上有很多优秀的静态分析工具和使用教程。
性能分析
如何将一个代码从”能跑“进化到”优秀的代码“呢?性能分析是其中很重要的一环。我们可以学习性能分析和监控工具,借此找到程序中最耗时、最耗资源的部分。
通常来说,用户时间+系统时间代表了进程所消耗的实际CPU。
- 真实时间 - 从程序开始到结束流失掉的真实时间,包括其他进程的执行时间以及阻塞消耗的时间(例如等待 I/O或网络);
- User - CPU 执行用户代码所花费的时间;
- Sys - CPU 执行系统内核代码所花费的时间。
性能分析工具通常指CPU性能分析工具。CPU 性能分析工具有两种:追踪分析器(tracing)及采样分析器(sampling)。追踪分析器 会记录程序的每一次函数调用,而采样分析器则只会周期性的监测(通常为每毫秒)程序并记录程序堆栈。比如Python使用的就是cProfile
模块分析每次函数调用消耗的时间。
对于分析器的可视化,采样分析器常见的显示 CPU 分析数据的形式是火焰图,火焰图会在 Y 轴显示函数调用关系,并在 X 轴显示其耗时的比例。火焰图同时还是可交互的,可以深入程序的某一具体部分,并查看其栈追踪。
最后,有很多很多的工具可以监控不同的系统资源,比如CPU占用、内存使用、网络、磁盘使用等。
Topic 8 安全和密码学
这部分会说明一些关于安全和密码学的简单概念。
熵
熵的单位是比特。对于一个均匀分布的随机离散变量,熵等于log_2(所有可能的个数,即n)。例如 “correcthorsebatterystaple” 这个密码比 “Tr0ub4dor&3” 更安全,因为前者有44比特的熵,后者只有28比特。假设每秒能进行1000次猜测,猜出前者的2^44种可能需要550年,而后者的2^28种可能只需要3天。
大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制)。 而对于离线穷举攻击(主要受限于计算速度), 一般需要更强的密码 (比如80比特或更多)。
散列函数
密码散列函数 (Cryptographic hash function) 可以将任意大小的数据映射为一个固定大小的输出。
密钥生成函数
密钥生成函数 (Key Derivation Functions) 被应用于包括生成固定长度,可以使用在其他密码算法中的密钥等方面。 为了对抗穷举法攻击,密钥生成函数通常较慢。
对称加密与非对称加密
对称加密,也称为私钥加密,是指加密和解密使用相同密钥的加密算法。在大多数对称算法中,加密密钥和解密密钥是相同的,因此也称为秘密密钥算法或单密钥算法。对称加密要求发送方和接收方在安全通信之前商定一个密钥。优势在于加密效率高,因为加密和解密使用相同的算法和密钥。然而,对称加密的安全性较低,因为如果密文被拦截且密钥被破解,信息就很容易被破译。常见的对称加密算法包括DES、AES和3DES等。
非对称加密,与对称加密不同,它使用一对密钥:公钥和私钥。公钥用于加密数据,而私钥用于解密数据。私钥只能由一方安全保管,不能外泄,而公钥可以发给任何请求它的人。非对称加密提供了更高的安全性,因为公钥和私钥是相互独立的,使用其中一个密钥加密的数据只能由另一个密钥解密。因此,不需要将私钥通过网络发送出去,安全性大大提高。目前最常用的非对称加密算法是RSA算法。