MIT-计算机科学教育中遗失的一学期笔记-全-

MIT 计算机科学教育中遗失的一学期笔记(全)

001:课程概述与Shell入门 🖥️

在本节课中,我们将要学习课程的整体介绍以及Shell的基础知识。Shell是您与计算机进行高效交互的核心工具,掌握它将为您后续的学习和工作打下坚实基础。

课程概述

这门课程源于我们在麻省理工学院担任助教时的一个观察。我们注意到,尽管计算机科学家都知道计算机擅长处理重复性任务和自动化,但我们常常忽略了那些能让自身开发过程变得更高效的工具。我们可以更高效地使用计算机,将其作为提升个人效率的工具,而不仅仅是用于构建网站或软件。本课程旨在向您展示一些可以在日常学习、研究和工作中发挥巨大效用的工具。

课程结构为一系列时长约一小时的讲座。每场讲座将涵盖一个特定主题。讲座内容大部分是独立的,因此您可以参加您感兴趣的部分。但我们会假设您一直在跟进学习,这样在后续讲座中,我们就无需重复讲解基础知识。

我们会在讲座后发布讲义和录像。课程将由我(John)、Anish和Jose负责。由于我们试图在有限的讲座时间内覆盖大量内容,因此进度会相对较快。但如果您有任何不理解的地方,或者希望我们更详细地讲解某些内容,请随时打断我们提问。

每场讲座后,我们会在32号楼(计算机科学楼)9层的数据中心举行答疑时间。您可以前来尝试讲义中的练习,或询问关于讲座内容及其他高效使用计算机的问题。

由于时间有限,我们无法详尽介绍所有工具。因此,我们将重点介绍有趣的工具及其有趣的使用方式。如果您对这些工具有更多疑问,也欢迎来问我们。我们使用其中许多工具已有多年,或许能为您指出更多有趣的用法。

Shell简介

上一节我们介绍了课程的整体安排,本节中我们来看看今天讲座的核心内容:Shell。Shell是当您想要超越图形界面限制时,与计算机交互的主要方式之一。图形界面因其只能提供按钮、滑块和输入框等功能而受到限制。相反,文本工具通常被设计为既可相互组合,又有多种组合方式或编程自动化方法。这就是为什么在本课程中,我们将专注于这些命令行或基于文本的工具。Shell是您完成大部分此类工作的地方。

对于不熟悉Shell的用户,大多数平台都提供某种Shell。在Windows上,通常是PowerShell,但也有其他Shell可用。在Linux上,您会找到许多终端(用于显示Shell的窗口)和不同类型的Shell,其中最常见的是Bash(Bourne Again Shell)。在macOS上,如果您打开终端应用程序,可能也拥有Bash。本课程的讲解将以Linux为中心,但大多数工具在所有平台上都适用。

当您打开一个终端时,会看到类似这样的界面。通常顶部只有一行,这就是所谓的Shell提示符。我的Shell提示符看起来像这样,它包含我的用户名、当前所在机器名和当前路径。它会在那里闪烁,等待我输入。这就是您告诉Shell您希望它做什么的地方。您可以大量自定义此提示符。

这是您通过Shell与计算机进行文本交互的主要界面。在Shell提示符下,您可以输入命令。命令可以是相对简单的操作,通常是执行带有参数的程序。

例如,我们可以执行一个名为 date 的程序。我们只需输入 date,它就会显示日期和时间。

您也可以执行带有参数的程序。这是修改程序行为的一种方式。例如,有一个名为 echo 的程序,echo 会打印出您给它的参数。参数是跟在程序名后面、由空格分隔的内容。

您可能会注意到,我说参数是由空格分隔的。您可能想知道,如果我想要一个包含多个单词的参数怎么办?您可以使用引号。例如,echo "Hello World"。现在 echo 程序接收一个包含字符串“Hello World”(带空格)的参数。您也可以使用单引号,单引号和双引号的区别我们将在讲解Bash脚本时讨论。您还可以转义单个字符,例如 echo Hello\ World

关于如何转义、解析和引用各种参数及变量的规则,我们稍后会介绍。至少请记住,空格用于分隔参数。因此,如果您想创建一个名为“my photos”的目录,直接输入 mkdir my photos 会创建两个目录,一个叫“my”,一个叫“photos”,这可能不是您想要的。

程序路径与环境变量

您可能会问,当我输入 dateecho 时,Shell如何知道这些程序是什么?答案在于,您的计算机内置了许多随机器提供的程序,就像您的机器可能预装了终端应用程序、Windows资源管理器或某种浏览器一样,它也预装了许多以终端为中心的应用程序,这些程序存储在您的文件系统中。

您的Shell有一种方法来确定程序的位置,基本上是一种搜索程序的方式。它通过一个名为 PATH 的环境变量来实现。环境变量类似于编程语言中的变量。实际上,Shell(特别是Bash)本身就是一种编程语言。您在这里得到的提示符不仅可以运行带参数的程序,还可以执行诸如while循环、for循环、条件判断等操作。您可以在Shell中定义函数、使用变量。我们将在下一讲关于Shell脚本的内容中详细介绍这些。

现在,让我们看看这个特定的环境变量。环境变量是在您启动Shell时设置好的。其中有一个对此目的至关重要的变量,即 PATH 变量。如果我执行 echo $PATH,这将显示我的计算机上Shell将搜索程序的所有路径。

您会注意到这是一个由冒号分隔的列表。其核心是,每当您键入一个程序名时,Bash将遍历您计算机上的这个路径列表,并在每个目录中查找与您尝试运行的命令名称匹配的程序或文件。

如果我们想知道它实际运行的是哪一个,有一个名为 which 的命令可以做到这一点。例如,我可以输入 which echo,它会告诉我,如果我运行一个名为 echo 的程序,我将运行这个。

路径与导航

值得在这里暂停一下,讨论一下路径是什么。路径是命名计算机上文件位置的一种方式。在Linux和macOS上,这些路径由正斜杠分隔。您会看到这里的路径从根目录开始,开头的斜杠表示从文件系统的顶部开始。然后进入名为 usr 的目录,再进入 bin 目录,最后寻找名为 echo 的文件。

在Windows上,此类路径通常由反斜杠分隔。在Linux和macOS上,所有内容都位于根命名空间下,因此所有路径(或所有绝对路径)都以斜杠开头。在Windows上,每个分区都有一个根目录,例如 C:\D:\

我提到了“绝对路径”这个词。绝对路径是完全确定文件位置的路径。但还有相对路径。相对路径是相对于您当前所在位置的路径。

我们通过输入 pwd(打印工作目录)来找出当前所在位置。然后,我可以选择更改当前工作目录,所有相对路径都是相对于当前工作目录的。

例如,我可以执行 cd /home 来更改当前工作目录。有几个特殊的目录存在:. 表示当前目录,.. 表示父目录。这是一种在系统中轻松导航的方式。

一个方便的技巧是使用 ~ 字符。这个字符会将您带到您的主目录。对于 cd 命令,还有一个非常方便的参数 -。如果您执行 cd -,它将切换到您之前所在的目录。

文件操作与权限

lscd 的情况下,可能有您不知道的参数。大多数程序接受所谓的参数,如标志和选项。这些通常以短横线开头。最方便的一个是 -help。大多数程序都实现了这个功能。

ls -l 标志很有用。它使用长列表格式,提供有关文件的更多信息。让我们看看其中一些信息是什么。首先,某些条目开头的 d 表示该条目是一个目录。其后的字母表示为该文件设置的权限。

读取这些权限的方式是:前三个字符是为文件所有者设置的权限,中间三个字符是为拥有该文件的组设置的权限,最后三个字符是为其他所有人设置的权限。

这些权限对于文件和目录的含义不同。对于文件,这很直接:如果您对文件有读取权限,则可以读取其内容;如果有写入权限,则可以保存文件;如果有执行权限,则允许执行该文件。

对于目录,这些权限则有所不同:读取意味着您是否被允许查看该目录中有哪些文件;写入意味着您是否被允许重命名、创建或删除该目录内的文件;执行(在目录上常被称为“搜索”)意味着您是否被允许进入该目录。

还有一些其他方便的程序需要了解。mv 命令允许我重命名文件。cp 命令允许复制文件。rm 命令允许删除文件。mkdir 命令用于创建新目录。

如果您想了解这些平台上任何命令的更多信息,有一个非常方便的命令叫做 man(手册页)。该程序以另一个程序的名称作为参数,并为您提供其手册页。

组合程序与管道

到目前为止,我们只讨论了单独的程序,但Shell真正强大的地方在于您开始组合不同的程序时。您可能希望将多个程序链接在一起,可能希望与文件交互,并在程序之间使用文件进行操作。我们可以使用Shell提供的流的概念来实现这一点。

默认情况下,每个程序都有两个主要流:一个输入流和一个输出流。默认情况下,输入流是您的键盘,输出流是您的终端。但Shell为您提供了一种重定向这些流的方法,以改变程序的输入和输出指向的位置。

最直接的方法是使用尖括号符号。左尖括号表示将此程序的输入重定向为该文件的内容。右尖括号表示将前面程序的输出重定向到此文件。

还有一个双右尖括号,表示追加而不是覆盖。

真正有趣的是Shell提供的另一个操作符,称为管道字符 |。管道意味着获取左侧程序的输出,并将其作为右侧程序的输入。

通过这种方式,您可以实现一些非常简洁的操作。我们将在数据整理讲座中更详细地介绍这些内容。

超级用户与系统文件

我想与您讨论的另一个主题是关于如何以更有趣、或许更强大的方式使用终端。但首先,我们需要介绍Linux系统和macOS中的一个重要概念,即超级用户(root用户)的概念。root用户类似于Windows上的管理员用户,用户ID为0。root用户很特殊,因为它被允许在您的系统上执行任何操作。

大多数时候,您不会以超级用户身份操作。但偶尔,您需要执行一些需要root权限的操作。对于这些情况,您通常会使用一个名为 sudo 的程序。sudo 允许您以超级用户身份运行命令。

您可能需要这样做的场景之一是访问 /sys 文件系统。这个文件系统实际上并不是您计算机上的文件,而是各种内核参数。这是一种通过看起来像文件系统的方式来访问各种内核参数的方法。

因为它们是作为文件暴露的,这意味着我们也可以使用我们迄今为止使用的所有工具来操作它们。一个例子是,如果您进入 /sys/class/backlight,这个目录允许您配置笔记本电脑的背光(如果有的话)。您可以读取和修改亮度文件来改变屏幕亮度,但这需要root权限。

总结与练习

本节课中我们一起学习了Shell的基础知识,包括如何执行命令、理解路径和环境变量、操作文件和目录、查看权限、组合程序使用管道,以及了解超级用户和系统文件访问。

现在,您应该大致了解如何在终端和Shell中操作,并掌握足够的知识来完成至少基本的任务。理论上,您现在不再需要使用点击界面来查找文件了。您可能还需要的一个技巧是打开文件的能力。例如,在Linux上,您可以使用 xdg-open 命令,给它一个文件名,它将在适当的程序中打开该文件。

对于你们中的一些人来说,这一切可能相对基础,但正如我所提到的,这是一个入门阶段。现在我们都知道Shell是如何工作的,我们在未来讲座中将要做的大部分事情就是利用这些知识,使用Shell来做真正有趣的事情。

我们将在下一讲中更多地讨论如何自动化此类任务,如何编写为您运行一系列程序的脚本,以及如何在终端中执行条件判断和循环等操作。

本讲的讲义已经在线发布,文件底部有一系列练习。我们鼓励您尝试完成它们。如果您已经了解这些内容,完成起来会很快;如果您不了解,它可能会教给您许多您可能没有意识到自己不知道的东西。在讲座后的答疑时间里,我们将很乐意帮助您完成所有这些练习。

002:Shell 脚本基础与工具

在本节课中,我们将要学习 Shell 脚本的基础语法和一些能极大提升效率的 Shell 工具。课程分为两部分:首先,我们将深入 Bash 脚本的变量、控制流和函数;然后,我们将介绍一些用于查找文件、搜索内容和浏览历史的强大工具。

Shell 脚本基础

上一节我们介绍了 Shell 的基本概念和命令执行。本节中我们来看看如何编写 Shell 脚本,包括变量、字符串和控制流。

变量与赋值

在 Shell 中定义变量时,等号两边不能有空格。空格在 Shell 中用于分隔命令参数。

foo=bar

使用 $ 符号来访问变量的值。

echo $foo

字符串

定义字符串可以使用单引号或双引号,但两者有重要区别。双引号会进行变量替换,而单引号则保持字面值。

echo "Value is $foo"  # 输出:Value is bar
echo 'Value is $foo'  # 输出:Value is $foo

特殊变量与参数

Shell 脚本中有许多以 $ 开头的特殊变量。

  • $0:脚本名称。
  • $1$9:脚本的第1到第9个参数。
  • $@:所有参数。
  • $#:参数个数。
  • $?:上一个命令的退出码(0通常表示成功)。
  • $_:上一个命令的最后一个参数。
  • $$:当前脚本的进程ID。

例如,一个使用这些变量的简单脚本:

#!/bin/bash
echo "Starting program at $(date)"
echo "Running program $0 with $# arguments with pid $$"
for file in "$@"; do
    grep foobar "$file" > /dev/null 2> /dev/null
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

控制流:条件判断与循环

Shell 支持 ifforwhile 等控制流语句。条件判断通常使用 [[ ... ]] 结构,并使用 -eq-ne-f 等操作符。

# 检查文件是否存在
if [[ -f "$file" ]]; then
    echo "$file exists"
fi

# 遍历所有参数
for arg in "$@"; do
    echo "$arg"
done

命令执行与组合

命令可以通过多种方式组合。

  • 顺序执行:使用分号 ;
  • 逻辑操作:&&(与)和 ||(或)用于基于上一个命令的成功与否执行下一个命令。
    false || echo "Will print"
    true && echo "Will print"
    true || echo "Will not print"
    
  • 命令替换:使用 $(command) 将命令输出捕获到变量中。
    foo=$(pwd)
    echo "We are in $(pwd)"
    
  • 进程替换:<(command) 将命令输出作为临时文件传递给其他命令。
    cat <(ls .) <(ls ..)
    

定义与使用函数

在 Shell 中定义函数如下所示。函数内的 $1 指函数的第一个参数。

mcd () {
    mkdir -p "$1"
    cd "$1"
}

在命令行定义此函数后,执行 mcd test 会创建并进入 test 目录。

若将函数定义写在脚本文件中,可以通过 source 命令加载到当前 Shell 环境。

source mcd.sh

通配与花括号展开

Shell 提供了强大的文件名匹配和生成功能。

  • 通配符:* 匹配任意数量字符,? 匹配单个字符。
    ls *.sh
    ls project?
    
  • 花括号展开:用于生成任意组合,非常适用于创建系列文件或目录。
    touch {foo,bar}/{a..j}
    

使用其他语言编写脚本

Shebang(#!)行告诉 Shell 使用哪个解释器来执行脚本。使用 /usr/bin/env 可以提高脚本的可移植性。

#!/usr/bin/env python3
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

脚本调试工具

编写 Bash 脚本时,可以使用 shellcheck 工具来检查语法错误和潜在问题。

shellcheck script.sh

实用的 Shell 工具

掌握了脚本基础后,我们来看看能帮助你更高效工作的 Shell 工具。

查找命令用法:mantldr

  • man 命令提供命令的完整手册页。
    man ls
    
  • tldr 命令提供更简洁、带示例的常用用法说明,适合快速查阅。
    tldr tar
    

查找文件:find

find 命令用于在目录树中递归搜索文件。

以下是 find 命令的一些常见用法:

  • 按名称查找:find . -name src -type d
  • 按路径模式查找:find . -path '*/test/*.py' -type f
  • 按修改时间查找:find . -mtime -1 (过去24小时内修改的文件)
  • 按大小查找:find . -size +500k -size -10M
  • 找到后执行操作:find . -name '*.tmp' -exec rm {} \;

现代替代工具如 fd 通常更快、默认彩色输出,并且用户友好。

查找文件内容:grep

grep 用于在文件中搜索文本模式。

  • 递归搜索:grep -r foobar .
  • 忽略大小写:grep -i foobar file.sh
  • 显示上下文:grep -C 5 foobar file.sh (显示匹配行前后5行)
  • 只打印文件名:grep -l foobar *.sh
  • 反向匹配:grep -v '^#' file.sh (打印所有不以 # 开头的行)

ripgrep (rg) 是一个更快的现代替代工具,默认递归搜索并智能处理文件。

查找历史命令

  • history 命令显示命令历史。可以配合 grep 进行搜索。
    history | grep convert
    
  • 反向搜索:按 Ctrl+R 可以向后搜索历史命令,输入关键词即可动态匹配。
  • 使用 fzf 进行模糊搜索:这是一个通用的模糊查找器,可以与历史命令结合,实现交互式、模糊的搜索体验。
    history | fzf
    
  • 历史子字符串搜索:在 Bash 中,输入命令的开头部分,按上箭头可以匹配历史中以此开头的命令。Zsh 等 Shell 对此有更好的支持。

目录导航与浏览

  • tree 命令可以以树状图列出目录结构,更直观。
  • brootnnn 等工具提供了交互式、可视化的文件浏览器,允许你快速在目录间导航、预览甚至操作文件。
  • autojumpzoxide 等工具可以学习你常用的目录,让你通过简短的命令快速跳转,例如 j proj 跳转到某个项目目录。

总结

本节课中我们一起学习了 Shell 脚本的核心概念,包括变量、字符串操作、控制流、函数以及参数处理。我们还探索了一系列强大的 Shell 工具,用于查找文件 (find, fd)、搜索文件内容 (grep, rg)、查阅命令手册 (man, tldr)、高效检索历史命令 (history, Ctrl+R, fzf) 以及便捷地浏览和导航文件系统 (tree, broot, autojump)。掌握这些脚本知识和工具将极大提升你在命令行环境下的工作效率和自动化能力。

003:编辑器 (Vim) 🖥️

在本节课中,我们将要学习文本编辑器,特别是 Vim。作为程序员,我们花费大量时间编辑文本和代码,因此掌握一个高效的编辑器能为你节省数百小时的时间。编程与写作散文不同,它涉及大量阅读、导航和局部修改,因此需要专门的工具。Vim 就是这样一款强大的编辑器,它基于命令行,并因其独特的设计理念而广受欢迎。

模态编辑:核心哲学

上一节我们介绍了学习编辑器的重要性,本节中我们来看看 Vim 的核心设计理念:模态编辑

Vim 是一个模态编辑器。“模态”一词源于“模式”,这意味着 Vim 拥有多种操作模式。这个设计源于编程时我们经常在做不同类型的事情:有时是阅读代码,有时是进行小范围修改,有时则是从头编写大量代码。Vim 为这些不同的任务设置了不同的模式。

当你启动 Vim 时,它默认处于 普通模式。在此模式下,所有按键组合都有特定的功能,主要用于导航和编辑,而不是直接输入文本。要从普通模式切换到其他模式,需要按下特定的键。最常用的是 插入模式,用于输入文本。按下 i 键可从普通模式进入插入模式,按下 Esc 键则可以从任何其他模式返回普通模式。

除了普通模式和插入模式,Vim 还有其他模式,例如:

  • 替换模式:按 R 进入,用于覆盖现有文本。
  • 可视模式:按 v 进入,用于选择文本。
  • 可视行模式:按 Shift+v 进入,用于按行选择。
  • 可视块模式:按 Ctrl+v 进入,用于选择矩形文本块。
  • 命令行模式:按 : 进入,用于执行保存、退出等命令。

由于 Esc 键使用频繁,许多程序员会将 Caps Lock 键重新映射为 Esc 键,以方便操作。

基础操作:启动、保存与退出

了解了 Vim 的模态概念后,我们来看看如何执行最基本的操作:启动、编辑、保存和退出。

Vim 是一个基于命令行的程序。要启动它,只需在终端中运行 vim 命令。如果你想直接编辑一个特定文件,可以在命令后加上文件名,例如 vim filename.md

启动后,Vim 处于普通模式。要开始输入文本,需要按 i 进入插入模式。此时,屏幕左下角会显示 -- INSERT --。输入完成后,按 Esc 返回普通模式。

Vim 的所有功能都可以通过键盘完成,无需使用鼠标。例如,保存和退出操作需要在命令行模式下进行。按 : 进入命令行模式,光标会跳转到屏幕底部。以下是几个基本命令:

  • 保存文件:输入 :w 然后按回车。
  • 退出 Vim:输入 :q 然后按回车。
  • 保存并退出:输入 :wq:x 然后按回车。
  • 强制退出(不保存):输入 :q! 然后按回车。

你还可以使用 :help [command] 来获取任何命令的帮助信息。

Vim 的界面模型:缓冲区、窗口与标签页

在深入更多编辑技巧之前,有必要理解 Vim 管理多文件的独特模型,这与许多图形化编辑器不同。

Vim 维护一组打开的 缓冲区,每个缓冲区对应一个打开的文件。然后,你可以有多个 标签页,每个标签页可以包含多个 窗口。关键点在于,缓冲区与窗口之间并非一一对应。同一个缓冲区(文件)可以同时在多个窗口中显示。这对于同时查看一个文件的不同部分非常有用。

要关闭当前窗口,使用 :q 命令。当所有窗口都关闭后,Vim 才会退出。如果你想一次性关闭所有窗口,可以使用 :qa 命令。

普通模式:将编辑变为编程语言

现在,我们来探讨 Vim 最强大、最根本的理念:Vim 的普通模式界面本身就是一个编程语言

这意味着各种按键命令就像函数,你可以将它们组合起来形成复杂的编辑操作。一旦形成肌肉记忆,你就能以思考的速度进行编辑。

普通模式下的命令主要分为几类:移动、编辑、计数和修饰符。让我们逐一了解。

移动命令

在普通模式下,你可以高效地在文件中导航。以下是一些基本移动命令:

  • 基本移动:使用 h(左)、j(下)、k(上)、l(右)代替方向键。
  • 按词移动w 移动到下一个词首,b 移动到上一个词首,e 移动到当前词尾。
  • 按行移动0 移动到行首,$ 移动到行尾,^ 移动到行首第一个非空字符。
  • 屏幕内移动H 移动到屏幕顶部,M 移动到屏幕中部,L 移动到屏幕底部。
  • 滚动Ctrl+u 向上滚动半页,Ctrl+d 向下滚动半页。
  • 文件内跳转gg 跳到文件开头,G 跳到文件末尾。
  • 查找字符f{字符} 向前查找并跳到该字符,F{字符} 向后查找。t{字符} 向前跳到该字符前,T{字符} 向后跳到该字符后。

编辑命令

编辑命令通常与移动命令组合使用,以指定操作范围。

  • 进入插入模式i 在光标前插入,a 在光标后插入,o 在当前行下方新建一行并插入,O 在当前行上方新建一行并插入。
  • 删除d 是删除命令,需要配合移动指令。例如:
    • dw 删除一个词。
    • de 删除到词尾。
    • dd 删除整行。
    • x 删除光标下的字符。
  • 修改c 是修改命令,它先删除指定范围,然后进入插入模式。例如 cw 修改一个词,相当于 dwi
  • 替换r 替换光标下的单个字符。
  • 复制与粘贴y 复制(yank),p 粘贴。例如 yy 复制整行,yw 复制一个词。
  • 撤销与重做u 撤销,Ctrl+r 重做。

可视模式与选择

要进行更直观的文本选择,可以使用可视模式。

  • v 进入字符可视模式,用移动命令选择文本。
  • Shift+v 进入行可视模式,按行选择。
  • Ctrl+v 进入块可视模式,选择矩形区域。
    选择后,可以进行复制 (y)、删除 (d)、修改 (c) 等操作。例如,选择后按 ~ 可以切换所选文本的大小写。

计数与修饰符

计数允许你将一个操作重复多次。只需在命令前加上数字。例如:

  • 5j 向下移动 5 行。
  • 3dw 删除 3 个词。
  • v3e 选择到第 3 个词的词尾。

修饰符可以改变移动命令的含义,在处理成对符号(如括号、引号)时特别有用。

  • i 表示“在...内部”,例如 ci( 表示修改圆括号 () 内部的内容。
  • a 表示“围绕...”,例如 da[ 表示删除方括号 [] 及其内部的所有内容。
    你可以使用 % 键在配对的括号间跳转。

高效编辑实战演示

让我们通过修复一个简单的 Python FizzBuzz 程序来演示如何组合使用这些命令。目标是展示如何用最少的击键快速完成一系列编辑任务。

初始有问题的代码:

for i in range(limit):
    if i % 3 == 0:
        print(“fizz”)
    if i % 5 == 0:
        print(“buzz”)

操作流程:

  1. 添加主函数调用:按 G 跳到文件末尾,按 o 新建一行并进入插入模式,输入 main(),按 Esc
  2. 修正循环起始值:按 /range 搜索,按 w 移动两次到 0 后,按 i 插入 1, ,按 Esc。按 e 跳到 limit 词尾,按 a 插入 +1,按 Esc
  3. 修正 Fizz 条件:按 /fizz 搜索,按 ci” 修改引号内内容为 Fizz,按 Esc
  4. 合并 FizzBuzz 输出:移动到相应 print 行行尾 ($),按 i 插入 , end=“”,按 Esc。移动到下一行,按 . 重复上一次编辑(插入 , end=“”)。
  5. 添加命令行参数解析:按 gg 跳到文件开头,按 O 在上方新建行,插入 import sys 等代码,按 Esc。按 /10 搜索硬编码的数字,按 ci( 修改括号内为 int(sys.argv[1])

这个演示展示了 Vim 的核心工作流:大部分时间停留在普通模式,快速移动到目标位置,进入插入模式做微小改动,然后立刻返回普通模式。使用搜索 (/)、重复操作 (.) 和组合命令能极大提升效率。

自定义与扩展

Vim 高度可定制和可扩展,这使其能适应任何工作流。

Vim 的配置通过一个名为 ~/.vimrc 的纯文本文件完成。你可以在此设置偏好,例如:

“ 启用语法高亮
syntax on
“ 显示行号
set number
“ 显示相对行号
set relativenumber

修改 .vimrc 后,重启 Vim 或执行 :source ~/.vimrc 使配置生效。

此外,Vim 拥有强大的插件生态系统。插件可以添加诸如模糊文件查找、文件树导航、语法检查、集成终端等功能。通常使用插件管理器(如 Vim-plug)来安装和管理插件。

Vim 模式无处不在

Vim 的编辑理念如此受欢迎,以至于许多其他工具都实现了 Vim 模拟模式。这意味着你可以在这些工具中使用熟悉的 Vim 键位进行编辑和导航。

以下是一些支持 Vim 模式或类似绑定的常见工具:

  • 代码编辑器:VS Code (Vim 扩展)、Sublime Text (Vintage 模式)、IntelliJ IDEA (IdeaVim)。
  • Shell/终端:Bash (set -o vi)、Zsh、Fish。
  • 阅读器/交互环境:Python REPL (通过 readline 配置)、Jupyter Notebooks、甚至一些网页浏览器(如 Firefox 的 Vimium 插件)。

启用这些工具的 Vim 模式,能让你的编辑技能在不同环境中无缝迁移,进一步提升整体效率。

总结

本节课中我们一起学习了文本编辑器 Vim。我们从其核心的模态编辑理念开始,了解了普通模式、插入模式等多种模式。我们掌握了启动、保存、退出的基础操作,并理解了 Vim 独特的缓冲区、窗口和标签页模型。

最重要的是,我们深入探讨了 Vim 普通模式作为一门编程语言的哲学,学习了如何组合移动编辑计数修饰符命令来高效地操作文本。通过实战演示,我们看到了这些技巧如何应用于实际编程任务。

最后,我们了解了如何通过 .vimrc 文件自定义 Vim,以及如何用插件扩展其功能。我们还发现,Vim 的编辑范式已扩展到许多其他工具中,形成了无处不在的 Vim 模式

投入时间学习并熟练使用像 Vim 这样的强大编辑器,可能是对你编程效率最有价值的投资之一。我们鼓励你完成课后练习,坚持使用,并在遇到低效操作时积极查找更优方法。祝你编辑愉快!

004:数据整理

在本节课中,我们将要学习数据整理。数据整理是指将数据从一种格式转换为另一种格式的过程,例如从日志文件中提取统计信息或生成图表。我们将探索一系列强大的命令行工具,特别是正则表达式,来高效地完成这类任务。

数据源与初步筛选

为了进行数据整理,我们首先需要一个数据源。本节中,我们将使用一个系统日志文件作为示例。这个日志文件记录了服务器上发生的许多事件,内容非常庞大。

为了聚焦于我们关心的信息,我们首先需要筛选出与SSH登录尝试相关的日志条目。以下是筛选SSH相关日志的命令:

journalctl | grep ssh

然而,这个命令会产生大量输出,难以直接分析。因此,我们需要进一步处理这些数据。

使用正则表达式提取信息

上一节我们介绍了如何筛选数据,本节中我们来看看如何精确地提取我们需要的部分,例如尝试登录的用户名。我们将使用一个名为 sed 的强大工具,它是一个流编辑器。

sed 最常见的用途是执行基于正则表达式的替换。正则表达式是一种用于匹配文本模式的强大语言。以下是一个简单的 sed 替换命令示例,用于移除每行开头我们不关心的部分:

sed 's/.*Disconnected from //'

在这个命令中:

  • s/ 表示“替换”。
  • .*Disconnected from 是我们要查找的模式。. 匹配任意单个字符,* 表示前一个字符(这里是.)出现零次或多次。因此 .* 会匹配任意长度的字符串,直到遇到字面字符串 Disconnected from
  • // 中的第二个斜杠后是空的,表示将匹配到的内容替换为空字符串。

以下是正则表达式中一些核心概念和特殊字符的公式化描述:

  • .:匹配任意单个字符。
  • *:匹配前一个字符零次或多次。公式:A* 表示 ε | A | AA | AAA | ... (其中 ε 为空)。
  • +:匹配前一个字符一次或多次。
  • []:匹配括号内的任意一个字符。例如 [abc] 匹配 abc
  • ^$:分别匹配行的开头和结尾。

构建复杂的匹配模式

简单的 .* 匹配是“贪婪的”,它会尽可能多地匹配字符,这可能导致我们意外地删除用户名本身。为了精确地提取用户名,我们需要构建一个更严谨的正则表达式。

我们需要匹配的日志行格式类似 Disconnected from invalid user <username> <ip> port <number> [preauth]。我们的目标是捕获 <username> 部分。

以下是构建最终匹配模式的思路和命令:

  1. 匹配行首的任意字符,直到 Disconnected from
  2. 可选地匹配 invalidauthenticatinguser
  3. 捕获用户名(由非空白字符组成)。
  4. 匹配后面的IP地址和端口信息。
  5. 使用捕获组 () 来记住用户名,并在替换时通过 \2 引用它。
sed -E 's/^.*Disconnected from (invalid |authenticating )?user (.*) [0-9.]+ port [0-9]+( \[preauth\])?$/\2/'

对于复杂的正则表达式,使用在线调试器(如 regex101.com)非常有帮助,它可以可视化匹配过程和捕获组。

对提取的数据进行聚合分析

现在我们已经提取出了用户名列表,但直接查看这个列表意义不大。我们需要进行统计和聚合。以下是处理数据的一系列有用工具:

以下是处理数据流的核心工具链:

  • sort:对输入行进行排序。
  • uniq -c:统计并合并相邻的重复行,-c 选项显示计数。
  • 结合 sort -n 进行数值排序,可以快速找到出现频率最高的项。
# 提取用户名,排序,统计,然后按频率排序并显示前10个
cat ssh.log | sed -E 's/^.*Disconnected from (invalid |authenticating )?user (.*) [0-9.]+ port [0-9]+( \[preauth\])?$/\2/' | sort | uniq -c | sort -nk1 | tail -n 10

使用 awk 进行列处理

awk 是另一个强大的文本处理语言,特别擅长处理基于列的数据。默认情况下,awk 将输入按空白符分割成列($1, $2, ...)。

例如,从 uniq -c 的输出中(格式为“计数 用户名”),如果我们只想获取用户名列表,可以使用 awk

awk '{print $2}'

awk 本身也是一个完整的编程语言。例如,我们可以用它来计数:

# 使用 awk 统计行数,相当于 `wc -l`
awk 'BEGIN {rows=0} {rows+=1} END {print rows}'

其他有用的数据整理工具

除了文本处理,命令行还能处理其他类型的数据转换。

  • bc(计算器):可以进行数学运算。例如,对一列数字求和:
    echo "1 2 3" | paste -sd+ | bc
    
  • R 语言:用于复杂的统计分析。可以快速计算摘要统计信息:
    echo -e "1\n2\n3\n4\n5" | R --slave -e 'x <- scan(file="stdin", quiet=TRUE); summary(x)'
    
  • gnuplot:用于生成简单的图表。
  • xargs:将输入行转换为命令行参数。例如,批量删除文件:
    find . -name '*.tmp' | xargs rm
    
  • 处理二进制数据:工具链同样适用于非文本数据。例如,使用 ffmpeg 捕获图像,用 convert(来自 ImageMagick)处理,然后进行压缩和传输:
    ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 - | convert - -colorspace gray - | gzip | ssh host 'gzip -d | tee copy.png | display'
    

总结

本节课中我们一起学习了数据整理的核心思想和方法。我们从庞大的原始日志出发,通过 grep 进行筛选,利用 sed 和正则表达式精确提取关键信息(如用户名),再通过 sortuniqawk 等工具对数据进行排序、统计和聚合,最终得到了有意义的洞察(如最常被尝试登录的用户名)。我们还了解到,这套基于管道 (|) 的工具链不仅适用于文本,也能通过 xargsffmpeg 等工具处理参数列表和二进制数据。掌握这些技能能让你在命令行中高效地将数据从任何格式转换为你需要的任何其他格式。

005:命令行环境

在本节课中,我们将学习如何更高效地使用命令行环境。我们将涵盖作业控制、终端复用器、配置文件管理以及远程机器操作等核心主题。掌握这些技能将帮助你更好地管理多个任务、定制工作环境,并轻松地在本地与远程系统间切换。

作业控制

上一节我们介绍了基本的命令执行。本节中我们来看看如何控制和管理在Shell中运行的多个进程。

当我们运行一个命令时,它通常会占用终端,直到执行完毕。作业控制允许我们暂停、恢复以及在后台运行这些进程。

信号

信号是进程间通信的一种机制。例如,按下 Ctrl+C 会向当前前台进程发送一个 SIGINT(中断)信号,通常会导致进程终止。

# 运行一个长时间休眠的命令
sleep 100
# 按下 Ctrl+C 可以中断它

以下是几个常见的信号:

  • SIGINT: 中断信号,通常由 Ctrl+C 触发。
  • SIGQUIT: 退出信号,通常由 Ctrl+\ 触发。
  • SIGTSTP: 终端停止信号,通常由 Ctrl+Z 触发,用于暂停进程。
  • SIGHUP: 挂起信号,通常在终端关闭时发送给其关联的进程。
  • SIGKILL: 强制终止信号,进程无法捕获或忽略此信号。
  • SIGTERM: 终止信号,请求进程优雅地退出。

管理后台作业

我们可以将进程置于后台运行,或管理已暂停的进程。

以下是管理作业的常用命令:

  • &: 在命令末尾添加 & 符号,使其在后台启动。
    sleep 2000 &
    
  • jobs: 列出当前Shell会话中的所有作业。
    jobs
    
  • bg %N: 将暂停的作业 N 在后台恢复运行。
    bg %1
    
  • fg %N: 将后台或暂停的作业 N 带到前台运行。
    fg %1
    
  • kill: 向进程发送信号。默认发送 SIGTERM
    # 发送 SIGSTOP 信号暂停作业1
    kill -STOP %1
    # 发送 SIGKILL 信号强制终止作业1
    kill -KILL %1
    

nohup 命令

nohup 命令可以让进程忽略 SIGHUP 信号,这样即使你关闭了终端,该进程也能继续运行。

nohup long_running_script.sh &

终端复用器

上一节我们学习了如何管理多个作业。本节中我们来看看终端复用器,它能让你在一个终端窗口中轻松管理多个会话、窗口和面板,极大地提升工作效率。

tmux 是一个功能强大的终端复用器。它的核心概念分为三层:会话、窗口和面板。

会话管理

会话是一个独立的终端工作区,你可以在其中创建多个窗口。

以下是 tmux 会话的基本操作(默认前缀键为 Ctrl+b):

  • tmux: 启动一个新的 tmux 会话。
  • tmux new -s <name>: 启动一个命名的新会话。
  • Ctrl+b d: 从当前会话中分离。
  • tmux atmux attach: 重新连接到上一个会话。
  • tmux a -t <name>: 连接到指定名称的会话。
  • tmux ls: 列出所有会话。

窗口管理

窗口类似于浏览器中的标签页,每个窗口包含一个Shell。

以下是窗口管理的常用快捷键:

  • Ctrl+b c: 创建一个新窗口。
  • Ctrl+b p: 切换到上一个窗口。
  • Ctrl+b n: 切换到下一个窗口。
  • Ctrl+b <number>: 切换到指定编号的窗口。
  • Ctrl+b ,: 重命名当前窗口。

面板管理

面板允许你在一个窗口内进行垂直或水平分割,同时查看多个终端。

以下是面板管理的常用快捷键:

  • Ctrl+b ": 水平分割当前面板。
  • Ctrl+b %: 垂直分割当前面板。
  • Ctrl+b <arrow key>: 在面板间移动焦点。
  • Ctrl+b z: 最大化/恢复当前面板。
  • Ctrl+b x: 关闭当前面板。

配置文件(Dotfiles)与环境定制

上一节我们介绍了终端复用器来组织工作空间。本节中我们来看看如何通过配置文件来定制你的Shell和其他工具,使其更符合你的使用习惯。

许多程序使用纯文本文件进行配置,这些文件通常以点号(.)开头,因此被称为“点文件”。

别名

别名可以为长命令或常用命令序列创建简短的替代名称。

# 创建别名
alias ll='ls -lh'
alias gs='git status'
# 使用别名
ll
gs
# 查看已定义的别名
alias ll

要使别名永久生效,需要将其添加到Shell的配置文件中(例如 ~/.bashrc~/.zshrc)。

Shell 配置

Shell的配置文件允许你设置环境变量、定义函数和加载脚本。

# 在 ~/.bashrc 中修改提示符
PS1='\u@\h:\w\$ '
# 设置默认编辑器
export EDITOR=vim

管理点文件

为了版本控制和在多台机器间同步配置,通常将点文件集中管理。

以下是管理点文件的常见方法:

  1. 创建一个Git仓库来存放所有配置文件。
  2. 在仓库中为每个配置文件(如 ~/.bashrc)创建对应的文件。
  3. 使用符号链接将主目录中的配置文件指向仓库中的文件。
    ln -s ~/dotfiles/.bashrc ~/.bashrc
    ln -s ~/dotfiles/.vimrc ~/.vimrc
    
  4. 使用专用工具(如 GNU Stow)自动化创建符号链接的过程。

远程机器操作

上一节我们学习了如何配置本地环境。本节中我们来看看如何高效地连接和操作远程服务器,这是软件开发、数据分析和系统管理中的常见任务。

SSH 基础

ssh 是连接远程机器的标准工具。

# 基本连接
ssh username@remote_host
# 执行单条命令
ssh username@remote_host ls -l

SSH 密钥认证

使用密钥对进行认证比密码更安全、更方便。

以下是设置SSH密钥认证的步骤:

  1. 在本地生成密钥对。
    ssh-keygen -t ed25519 -C "your_email@example.com"
    
  2. 将公钥复制到远程服务器。
    ssh-copy-id username@remote_host
    
  3. 现在你可以无需密码登录了。
    ssh username@remote_host
    

文件传输

有多种方式可以在本地和远程机器间传输文件。

以下是文件传输的常用方法:

  • scp: 安全复制。
    # 复制本地文件到远程
    scp local_file.txt username@remote_host:/path/to/destination/
    # 从远程复制文件到本地
    scp username@remote_host:/path/to/file.txt ./
    
  • rsync: 更高效的文件同步工具,支持增量复制。
    rsync -avz local_dir/ username@remote_host:/path/to/remote_dir/
    

SSH 配置

通过 ~/.ssh/config 文件可以简化SSH连接。

# ~/.ssh/config 示例
Host myserver
    HostName server.example.com
    User username
    IdentityFile ~/.ssh/id_ed25519
    Port 2222

配置后,连接命令可以简化为:

ssh myserver

在远程机器上使用 Tmux

结合 tmuxssh 可以让你在断开连接后,任务仍在远程服务器上继续运行。

  1. 通过SSH连接到远程服务器。
  2. 在远程服务器上启动 tmux 会话并运行任务。
  3. 使用 Ctrl+b d 分离 tmux 会话。
  4. 断开SSH连接。
  5. 重新连接后,使用 tmux a 重新附加到之前的会话,所有任务都保持原样。

总结

本节课中我们一起学习了命令行环境的核心进阶技能。我们了解了如何通过作业控制信号来管理进程的生命周期;使用 tmux 终端复用器高效组织多个工作区;通过点文件定制Shell和工具配置以实现个性化工作流;最后,掌握了使用SSH连接远程机器、配置密钥认证、传输文件以及结合 tmux 进行持久化工作的完整流程。这些工具和概念将帮助你构建一个强大、灵活且高效的命令行工作环境。

006:Git 版本控制系统教程

概述

在本节课中,我们将要学习版本控制系统,特别是 Git。我们将从 Git 的数据模型和内部原理开始,理解其如何跟踪文件和文件夹的变化,然后学习如何使用 Git 命令行工具进行实际操作。通过理解其底层设计,你将能够更有效地使用 Git,而不仅仅是记住一些命令。

版本控制系统简介

版本控制系统是用于跟踪源代码或其他文件集合变化的工具。它们帮助记录文档的历史变更,并促进团队协作。这些工具通过一系列快照来跟踪文件夹及其内容的变化,每个快照都封装了某个顶级目录下的所有文件和文件夹。

上一节我们介绍了版本控制的基本概念,本节中我们来看看 Git 的具体数据模型。

Git 的数据模型

Git 将历史建模为一个有向无环图。每个快照(在 Git 中称为提交)都有一组父提交,用于表示变更的先后顺序。这种模型允许分支和合并操作,使得可以在不同的开发线上并行工作。

文件和文件夹的表示

在 Git 中,文件和文件夹被表示为以下对象:

  • Blob:代表一个文件,本质上是一个字节数组。
    blob = array<byte>
    
  • Tree:代表一个文件夹,是文件名或目录名到其内容的映射。内容可以是另一个 Tree(子树)或一个 Blob(文件)。
    tree = map<string, tree | blob>
    
  • Commit:代表一个历史快照。它包含父提交(数组)、作者、消息等元数据,以及一个指向顶级 Tree 的指针,该 Tree 代表了该提交时刻的项目状态。
    commit = struct {
        parents: array<commit>
        author: string
        message: string
        snapshot: tree
    }
    

对象与引用

Git 将所有对象(Blob、Tree、Commit)统一存储在内容寻址存储中。每个对象的键是其内容的 SHA-1 哈希值。

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

这些哈希值是长字符串,对人类不友好。因此,Git 使用引用(References)来为图中的特定节点提供人类可读的名称。引用是一个从字符串(如分支名)到哈希值的可变映射。

references = map<string, string>

例如,masterHEAD 就是常见的引用。

基本 Git 命令

理解了数据模型后,我们来看看 Git 命令如何操作这个图结构。

初始化与首次提交

首先,我们需要初始化一个 Git 仓库并创建第一个提交。

以下是创建初始提交的步骤:

  1. git init:初始化一个新的 Git 仓库。
  2. git add <file>:将文件更改添加到暂存区,准备包含在下一次提交中。
  3. git commit:创建一个新的提交(快照),将暂存区的内容永久保存到历史中。

查看历史与状态

要了解仓库的当前状态和历史,可以使用以下命令:

  • git status:查看工作目录和暂存区的状态。
  • git log:以线性方式显示提交历史。使用 git log --all --graph --decorate 可以图形化显示分支和合并历史。
  • git diff:显示工作目录与上次提交(或指定提交)之间的差异。

移动与恢复

git checkout 命令用途广泛:

  • git checkout <commit-hash>:将工作目录的内容恢复到指定提交的状态。这会移动 HEAD 引用。
  • git checkout <branch-name>:切换到指定分支。
  • git checkout -- <file>:丢弃指定文件在工作目录中的修改,将其恢复为 HEAD 所指向提交中的状态。

分支与合并

Git 的核心优势在于其强大的分支和合并功能,允许并行开发。

创建与切换分支

  • git branch <branch-name>:创建一个指向当前 HEAD 的新分支(引用)。
  • git checkout -b <branch-name>:创建并立即切换到新分支(等价于上述两个命令)。

合并分支

git merge <branch-name> 用于将指定分支的更改合并到当前分支。

  1. 快进合并:如果目标分支是当前分支的直接祖先,Git 只需将当前分支指针向前移动。
  2. 三方合并:如果分支已经分叉,Git 会创建一个新的“合并提交”,该提交有两个父提交。如果 Git 无法自动解决冲突,会产生合并冲突,需要手动解决。

解决合并冲突后,使用 git add 标记冲突已解决,然后使用 git merge --continue 完成合并。

远程协作

Git 支持分布式协作。你可以拥有仓库的远程副本(例如在 GitHub 上),并与他人同步更改。

远程仓库基础

  • git remote add <name> <url>:添加一个远程仓库,并为其命名(通常叫 origin)。
  • git push <remote> <local-branch>:<remote-branch>:将本地分支的更改推送到远程仓库的指定分支。使用 git push -u origin master 可以设置上游分支,之后只需 git push
  • git fetch <remote>:从远程仓库获取所有最新信息(如下载新的提交和分支),但不改变你本地的任何工作文件或分支。它更新的是远程跟踪分支(如 origin/master)。
  • git pull:相当于 git fetch 后接 git merge,用于获取远程更改并合并到当前分支。
  • git clone <url>:克隆(下载)一个远程仓库到本地。

高级功能与配置

Git 功能丰富,以下是一些有用的高级主题:

实用命令与配置

  • git config:配置 Git 行为,如用户信息、别名、颜色输出等。配置存储在 ~/.gitconfig 文件中。
  • git add -p:交互式暂存,允许你选择性地将文件的特定更改加入暂存区,非常适合提交前整理更改。
  • git blame <file>:显示指定文件的每一行最后一次是由谁在哪个提交中修改的。
  • git stash:临时保存工作目录的修改,以便清理工作区。之后可用 git stash pop 恢复。
  • git bisect:使用二分查找在历史提交中定位引入错误的提交,非常适合调试回归问题。

忽略文件

创建 .gitignore 文件来指定 Git 应忽略的文件模式(如编译产物、日志文件、系统文件等)。被忽略的文件不会出现在 git status 中,也不会被意外提交。

工具集成

  • 图形化客户端:如 GitKraken、Sourcetree,提供可视化界面。
  • Shell 集成:在 Shell 提示符中显示 Git 仓库状态(当前分支、是否有修改等)。
  • 编辑器集成:许多编辑器(如 VSCode、Vim)有插件支持直接在编辑器内执行 Git 操作、查看 git blame 信息等。

总结

本节课中我们一起学习了 Git 版本控制系统。我们从其优雅的数据模型(对象、引用、有向无环图)开始,理解了其内部如何表示历史和文件。然后,我们学习了核心命令,包括提交、查看历史、分支、合并以及与远程仓库协作。最后,我们概述了一些高级功能和配置选项。记住,理解底层模型是掌握 Git 接口的关键。要深入学习,推荐阅读《Pro Git》一书并完成课程提供的练习。

007:调试与性能分析 🐛⚡

在本节课中,我们将要学习当程序出现错误或性能不佳时,可以使用的各种工具和方法。我们将从最简单的打印调试开始,逐步深入到日志记录、调试器、静态分析,最后探讨性能分析工具。掌握这些技能将帮助你更高效地定位和解决问题。


调试

上一节我们介绍了课程概述,本节中我们来看看调试。调试可以通过多种方式进行。最简单的方法是打印调试,即通过添加打印语句来探查代码行为。这种方法简单快捷,但可能会产生大量输出。

以下是打印调试的一个简单示例:

print(f"当前变量 x 的值是:{x}")

日志记录

当打印调试不够时,或者你需要为更复杂的软件系统记录事件时,可以使用日志记录。使用日志库的核心优势在于可以定义严重性级别并基于这些级别进行过滤。

以下是如何使用日志库的示例:

import logging

logging.basicConfig(level=logging.WARNING)
logging.warning("这是一个警告信息")
logging.error("这是一个错误信息")

通过设置不同的日志级别,你可以控制输出信息的详细程度。例如,在生产环境中,你可能只关心错误和严重信息。

终端颜色输出

人类是视觉动物,使用颜色可以更直观地识别日志信息。终端通过特殊的转义序列来显示颜色。

以下是如何在输出中使用颜色的示例:

echo -e "\033[31m这是红色文本\033[0m"

这段代码中,\033[31m 设置颜色为红色,\033[0m 重置颜色。

系统日志

许多系统组件(如Web服务器、数据库)会将日志记录到系统日志中。在类Unix系统中,系统日志通常位于 /var/log 目录下。

你可以使用 journalctl(Linux)或 log(macOS)等命令来查看系统日志。例如,查看最近10秒的日志:

# Linux
journalctl --since "10 seconds ago"

# macOS
log show --last 10s

你还可以使用 logger 命令将自己的消息写入系统日志:

logger "Hello logs"

调试器

当打印调试和日志记录不足以解决问题时,下一步是使用调试器。调试器允许你控制程序的执行,例如设置断点、单步执行和检查变量状态。

以下是使用Python调试器 pdb 的示例:

import pdb

def buggy_function():
    arr = [3, 1, 4, 1, 5]
    pdb.set_trace()  # 在此处设置断点
    # ... 其余代码

在调试器中,你可以使用命令如 l(列出代码)、s(单步执行)、c(继续执行)和 q(退出)。

对于低级语言如C/C++,可以使用 gdb 调试器。它甚至可以用于分析任何可执行的二进制文件。

系统调用跟踪

有时,你需要以黑盒方式调试程序,查看它执行了哪些系统调用(如文件读写、网络请求)。strace 命令可以用于此目的。

以下是跟踪 ls 命令系统调用的示例:

strace ls -l 2>&1 | head -20

静态分析工具

静态分析工具可以在不运行代码的情况下检查源代码,发现潜在的错误、代码风格问题或类型不一致。

在Python中,可以使用 pyflakesmypy 等工具。例如:

# 检查语法错误和未定义变量
pyflakes your_script.py

# 进行类型检查
mypy your_script.py

许多编辑器可以集成这些工具,在你编写代码时实时提供反馈。

静态分析甚至可用于自然语言,例如检查英语写作中的拼写和语法问题。

特定领域的调试器

根据你正在执行的任务,可能需要使用特定的调试工具。例如,在Web开发中,浏览器内置的开发者工具(如Chrome DevTools)非常强大,允许你检查HTML、修改CSS、调试JavaScript等。


性能分析

调试解决了程序行为错误的问题,而性能分析则关注如何优化代码,使其运行更快或消耗更少资源(如CPU、内存、网络)。

简单计时

最直接的性能分析方法是测量代码段的执行时间。在Python中,可以使用 time 模块。

以下是测量执行时间的示例:

import time

start = time.time()
# 执行一些操作
time.sleep(0.5)
end = time.time()
print(f"执行耗时:{end - start} 秒")

需要注意的是,time.time() 测量的是“真实时间”,它可能受到系统其他进程的影响。time 命令可以区分“用户时间”(程序在CPU上执行用户代码的时间)和“系统时间”(程序执行内核代码的时间)。

性能分析器

性能分析器提供了更深入的洞察。它们主要分为两类:追踪分析器和采样分析器。

追踪分析器会在每个函数调用时记录信息,从而精确了解时间花费在哪里,但可能带来较大开销。Python的 cProfile 就是一个追踪分析器。

以下是使用 cProfile 的示例:

python -m cProfile -s time your_script.py

采样分析器会定期中断程序,查看当前的调用栈。虽然精度稍低,但开销小。perf 是Linux系统上一个强大的采样分析工具。

行分析器和内存分析器

对于更细粒度的分析,可以使用行分析器(如Python的 line_profiler),它显示每行代码的执行时间。

当内存是瓶颈时,可以使用内存分析器(如Python的 memory_profiler)来跟踪内存分配情况。

可视化分析结果

为了更直观地理解分析数据,可以使用可视化工具。

  • 火焰图:一种可视化采样分析结果的方式,y轴表示调用栈深度,x轴表示时间占比,宽度越大的函数消耗时间越多。
  • 调用图:以图形方式显示函数之间的调用关系和时间成本。

资源监控工具

有时,你甚至不清楚是哪种资源(CPU、内存、磁盘I/O、网络)成为了瓶颈。以下是一些有用的监控工具:

  • htop:一个交互式的进程查看器,可以实时监控CPU和内存使用情况。
  • du / ncdu:用于分析磁盘空间使用情况。ncdu 提供了一个交互式界面。
  • lsof:列出打开的文件。可以用来查找哪个进程正在使用某个特定文件或网络端口。
  • hyperfine:一个命令行基准测试工具,可以比较不同命令或脚本的执行速度。

例如,使用 hyperfine 比较 findfd 命令的速度:

hyperfine 'find . -name "*.jpg"' 'fd -e jpg'

本节课中我们一起学习了调试和性能分析的完整工具箱。我们从简单的打印语句开始,探索了日志记录、调试器、静态分析,然后深入了解了各种性能分析技术和资源监控工具。关键不在于精通所有工具,而在于知道它们的存在。当遇到问题时,你可以选择合适的工具,而不是重新发明轮子。希望这些知识能帮助你在编程和系统管理中更加得心应手。

008:元编程

在本节课中,我们将要学习元编程。元编程并非指编程本身,而是围绕软件开发过程的一系列实践,例如如何构建系统、如何测试、如何管理依赖等。这些概念在构建大型软件时至关重要。

构建系统

上一节我们介绍了元编程的概念,本节中我们来看看构建系统。构建系统的核心思想是,当你需要执行一系列命令来完成特定任务(如编译论文或运行测试)时,可以将这些命令规则编码到一个工具中。这个工具能理解不同构建产物之间的依赖关系,并自动执行必要的命令。

构建系统有很多种,有些是为特定语言或目的设计的。在本课程中,我们将重点介绍一个名为 make 的工具,它几乎存在于所有现代操作系统中,非常适合处理简单到中等复杂度的项目。

当你运行 make 命令时,它会在当前目录中寻找一个名为 Makefile 的文件。在这个文件中,你需要定义目标依赖规则

  • 目标:你想要构建的东西,例如 paper.pdfrun tests
  • 依赖:构建目标所需要的东西,例如源文件或图像。
  • 规则:定义了如何从依赖项生成目标的一系列命令。

以下是一个简单的 Makefile 示例,用于构建一个包含图表的 LaTeX 论文:

paper.pdf: paper.tex plot-data.png
    pdflatex paper.tex

plot-%.png: %.dat plot.py
    ./plot.py -i $*.dat -o $@

在这个例子中:

  • paper.pdf 是目标,它依赖于 paper.texplot-data.png。规则是运行 pdflatex
  • plot-%.png 是一个模式规则,% 是通配符。它表示任何形如 plot-XXX.png 的文件都依赖于 XXX.datplot.py。规则是运行 Python 绘图脚本。
  • $*$@make 的自动变量,分别代表匹配到的通配符部分和目标文件名。

当你运行 make 时,它会检查目标及其依赖项的时间戳。只有当依赖项比目标更新,或者目标不存在时,make 才会执行相应的规则来重建目标。这确保了每次构建只进行最少必要的工作。

依赖管理

在软件开发中,依赖关系不仅限于文件。你的项目可能依赖于其他程序、库或系统组件。为了管理这些依赖,我们通常使用软件仓库

软件仓库是软件包的集合,例如:

  • PyPI: Python 包仓库。
  • RubyGems: Ruby 包仓库。
  • npm: Node.js 包仓库。
  • APT: Debian/Ubuntu 的系统包仓库。

软件通常带有版本号(如 8.1.7),这很重要,因为它能确保你的软件与依赖库的特定版本兼容,避免因依赖库更新导致你的软件崩溃。

为了解决版本兼容性问题,社区广泛采用语义化版本规范。一个版本号通常格式为 主版本号.次版本号.修订号

  • 当你做了不兼容的 API 修改时,递增主版本号
  • 当你做了向下兼容的功能性新增时,递增次版本号
  • 当你做了向下兼容的问题修正时,递增修订号

遵循此规范,如果你的软件依赖某个库,你可以指定版本范围(例如 ^8.1.0),表示接受主版本号为 8,且次版本号不低于 1 的任何版本。这既保证了兼容性,又允许自动接收安全更新。

为了确保每次构建的一致性,许多项目使用锁文件。锁文件记录了项目当前使用的所有依赖的确切版本。这带来了两个好处:

  1. 加速构建:无需每次检查并下载最新版本。
  2. 可重现的构建:无论何时何地构建,都能得到完全相同的结果,这对于安全审计至关重要。

依赖管理的极端形式是代码库内嵌,即直接将依赖的源代码复制到你的项目中。这确保了绝对的控制和一致性,但代价是你无法自动获取依赖库的更新。

持续集成

对于大型项目,你通常希望自动化一些流程,例如在每次提交代码时自动运行测试,或者自动发布新版本。这就是持续集成系统的用武之地。

持续集成系统本质上是一个云端自动化系统。它监听你代码仓库的特定事件(如推送提交、创建拉取请求),并触发预定义的动作(如运行测试套件、检查代码风格、构建文档、发布包等)。

常见的通用 CI 平台有 Travis CI、GitHub Actions、Azure Pipelines 等。也有更专业的 CI 服务,例如专注于测试覆盖率或依赖更新的机器人。

使用 CI 系统通常需要在你的仓库中添加一个配置文件(如 .travis.yml 或 GitHub Actions 的 YAML 文件),在其中定义触发事件和执行的任务。

CI 系统的一个强大之处在于其可协作性。例如,你可以设置一个 CI 任务,在有人提交拉取请求时,自动用语法检查工具检查文档的拼写。其他人也可以复用你写好的这个 CI 配置。

测试

测试是确保软件质量的关键环节。随着项目变得复杂,测试也会变得更加系统化。以下是一些常见的测试术语:

  • 测试套件:项目中所有测试的集合。
  • 单元测试:小而专注的测试,用于验证单个功能模块的正确性。
  • 集成测试:测试多个子系统或模块协同工作是否正常。
  • 回归测试:针对过去出现过并已修复的 bug 编写的测试,防止问题再次出现。
  • 模拟:在测试中,用可控的“假”对象来替代系统的某些部分(如网络、数据库)。例如,测试文件上传功能时,可以模拟一个网络连接,而不是真正发起 HTTP 请求。大多数编程语言都有辅助创建模拟对象的库。

总结

本节课中我们一起学习了元编程的核心概念。我们了解了构建系统如何自动化编译流程,依赖管理语义化版本如何维护项目的稳定与安全,持续集成如何将测试、部署等流程自动化,以及测试的不同类型和策略。掌握这些围绕编程的“元”技能,能极大地提升你开发和管理软件项目的效率与可靠性。

009:安全与密码学

在本节课中,我们将要学习安全与密码学的基础概念。我们将探讨熵、哈希函数、密钥派生函数、对称与非对称加密等核心概念,并了解它们在实际工具(如Git、SSH)中的应用。请注意,本课程旨在帮助你理解现有工具的原理,而非指导你自行构建加密系统。


熵:随机性的度量

上一节我们介绍了课程概述,本节中我们来看看第一个核心概念:熵。熵是随机性的度量单位,通常以比特表示。它对于评估密码强度非常有用。

一个简单的例子是抛硬币。抛一枚公平的硬币有两种可能性,其熵为 log₂(2) = 1 比特。掷骰子有六种可能性,其熵约为 log₂(6) ≈ 2.6 比特。

在密码学中,我们通过计算密码生成模型的可能性数量来估算其熵值。熵值越高,密码在暴力破解攻击下就越安全。

以下是评估密码强度时需要考虑的几点:

  • 密码的生成模型决定了其熵值。例如,从包含2000个单词的词典中随机选择四个单词组成的密码,其熵值远高于对单个单词进行简单字符替换后形成的密码。
  • 所需的熵值取决于你的威胁模型。防御在线猜测攻击可能需要约40比特的熵,而防御离线破解攻击则可能需要80比特或更高。
  • 人类不擅长生成真正的随机数。建议使用物理随机源(如骰子)或专门的工具(如Diceware方法)来生成高熵密码。

哈希函数

上一节我们介绍了如何量化随机性,本节中我们来看看哈希函数。哈希函数能将任意长度的输入数据映射为固定长度的输出,这个输出通常看起来像一串随机数。

一个典型的例子是SHA-1哈希函数,它输出一个160比特(40位十六进制字符)的值。

$ printf "hello" | sha1sum
aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

哈希函数有几个关键特性:

  • 确定性:相同的输入总是产生相同的输出。
  • 抗碰撞性:很难找到两个不同的输入产生相同的哈希值。
  • 不可逆性:给定哈希输出,很难(几乎不可能)反推出原始输入。

哈希函数有多种应用:

  • 内容寻址:Git使用SHA-1哈希来唯一标识提交和文件,这依赖于其抗碰撞性来保证仓库的完整性。
  • 文件完整性校验:下载大文件(如操作系统镜像)时,你可以从可信源获取该文件的哈希值,然后与从任何镜像下载的文件计算出的哈希值进行比对。如果一致,即可确认文件未被篡改。
  • 承诺方案:你可以先公布某个值(如抛硬币结果)的哈希值作为“承诺”,稍后再公布原始值。其他人可以通过重新计算哈希来验证你未曾更改过最初的选择。

密钥派生函数

上一节我们了解了哈希函数的多种用途,本节中我们来看看一个相关的概念:密钥派生函数。密钥派生函数与哈希函数类似,但它被设计为计算缓慢

一个常见的例子是PBKDF2。它之所以要设计得慢,是为了抵御暴力破解攻击。例如,在验证用户密码时,计算一次慢速的KDF是可以接受的;但攻击者尝试数百万次密码时,这种缓慢的特性会极大地增加其攻击成本和时间。

KDF的输出通常用作其他加密算法(如下一节将介绍的对称加密)的密钥。


对称密钥加密

上一节我们介绍了如何从密码派生出密钥,本节中我们来看看如何使用这个密钥进行加密。对称密钥加密使用同一个密钥进行加密和解密。

一个对称加密系统包含三个部分:

  • 密钥生成:生成一个高熵的随机密钥 K
  • 加密函数:接收明文 P 和密钥 K,输出密文 CC = Encrypt(P, K)
  • 解密函数:接收密文 C 和密钥 K,输出原始明文 PP = Decrypt(C, K)

其核心特性是:不知道密钥 K,就无法从密文 C 中获取明文 P 的任何信息;同时,使用相同密钥加解密可以正确还原数据。

对称加密的一个典型应用是加密存储在不可信云服务上的文件。你可以使用一个由高熵密码通过KDF生成的密钥来加密文件,然后将密文上传。这样,即使云服务提供商也无法查看你的文件内容。

你可以使用像openssl这样的命令行工具进行对称加密操作。

# 使用AES-256-CBC算法和密码加密文件
$ openssl aes-256-cbc -salt -in README.md -out README.md.enc

# 使用相同密码解密文件
$ openssl aes-256-cbc -d -in README.md.enc -out README.decrypted.md

关于“盐”的补充说明:在上述命令和许多密码系统中,会使用一个称为“盐”的随机值。盐不是秘密,它会与密文一起存储。它的主要作用是防止彩虹表攻击,即确保即使两个用户使用了相同的密码,其最终的加密密钥和密文也会因盐的不同而不同,从而迫使攻击者必须针对每个目标单独进行暴力破解。


非对称密钥加密

上一节我们讨论了共享同一密钥的加密方式,本节中我们来看看一个更强大的概念:非对称加密,它使用一对密钥而非单个密钥。

一个非对称加密系统包含以下部分:

  • 密钥生成:生成一对密钥:公钥 PK(可公开)和私钥 SK(必须保密)。
  • 加密函数:使用接收方的公钥 PK 加密明文 P,得到密文 CC = Encrypt(P, PK)
  • 解密函数:使用接收方自己的私钥 SK 解密密文 C,得到明文 PP = Decrypt(C, SK)

其核心思想是:任何人都可以用你的公钥加密信息,但只有持有对应私钥的你才能解密。这解决了对称加密中需要安全交换密钥的难题。

非对称加密还可用于数字签名

  • 签名函数:使用签名者的私钥 SK 对消息 M 进行签名,得到签名 SS = Sign(M, SK)
  • 验证函数:使用签名者的公钥 PK 验证消息 M 和签名 S 是否匹配。Verify(M, S, PK) 返回真或假。

签名可以证明消息的真实性(来自特定私钥持有者)和完整性(未被篡改)。

非对称加密的应用非常广泛:

  • 加密通信:如PGP加密电子邮件、Signal/WhatsApp等即时通讯工具。
  • 软件签名:开发者用私钥为软件发布包(如Debian软件包、Git标签)签名,用户可用其公钥验证下载的软件是否真实且未被篡改。

密钥分发问题:如何确保你获取的公钥确实属于目标对象,而非攻击者伪造的?解决方案包括:

  • 线下交换:面对面交换公钥指纹。
  • 信任网络:依赖你信任的人所信任的人(如PGP的Web of Trust)。
  • 社交证明:将公钥与多个社交网络身份(Twitter、GitHub等)绑定(如Keybase)。
  • 证书颁发机构:由受信的第三方机构验证并签发数字证书(HTTPS的基础)。

性能优化:混合加密系统 由于非对称加密计算较慢,实际应用中(如加密邮件)常采用混合加密:

  1. 发送方随机生成一个对称密钥 K
  2. 使用对称密钥 K对称加密算法快速加密大体积的消息 M,得到密文 C_sym
  3. 使用接收方的公钥 PK非对称加密算法加密对称密钥 K,得到加密的密钥 C_key
  4. C_symC_key 一起发送给接收方。
  5. 接收方用自己的私钥解密 C_key 得到 K,再用 K 解密 C_sym 得到原始消息 M

本节课中我们一起学习了安全与密码学的基础知识。我们从衡量密码强度的开始,探讨了具有不可逆和抗碰撞特性的哈希函数及其应用。接着,我们了解了设计为计算缓慢的密钥派生函数,以及使用单一密钥进行加解密的对称加密。最后,我们学习了使用公钥/私钥对的非对称加密,它实现了安全通信和数字签名,并讨论了与之相关的密钥分发挑战和混合加密的实用方案。理解这些概念有助于你更深入地认识日常使用的数字工具背后的安全原理。

010:杂项主题 (2020)

在本节课中,我们将学习一系列有趣但不足以构成独立讲座的杂项主题。这些主题涵盖了从键盘重映射到GitHub贡献等多个方面,旨在扩展你的计算机使用技能。

键盘重映射 ⌨️

上一节我们介绍了课程概述,本节中我们来看看如何通过键盘重映射提升效率。我们鼓励将键盘作为主要输入方式,因为使用鼠标通常较慢。键盘并非不可配置,许多默认设置可能并非最优。

最简单的修改是重映射按键。例如,大写锁定键位置便利但使用频率低,可以将其映射为更实用的按键,如Esc(Vim用户)或Ctrl(Emacs用户)。同样,功能键或打印屏幕键也可以重映射为媒体控制键。

以下是你可以进行的键盘重映射操作:

  • Caps Lock映射为EscCtrl
  • 将不常用的功能键(如F1-F12)映射为媒体控制键(播放/暂停)。
  • Print Screen键映射为打开特定应用程序。

你还可以创建更复杂的按键组合来触发操作。例如,我可以设置Ctrl+Enter来打开新终端窗口,或Ctrl+Shift+Enter来打开新浏览器窗口,从而避免使用鼠标。

此外,你还可以将按键组合映射为输入特定文本,例如你的邮箱地址或学号,这样就不必每次都手动输入。

更高级的配置包括键盘序列和区分短按与长按。例如,在Tmux中,你可以先按前缀键(如Ctrl+A),再按其他键来执行命令。我个人将Caps Lock键设置为:轻按时发送Esc,按住时则作为Ctrl键使用。许多工具都支持此类高级配置。

各操作系统都有相应的配置工具,具体推荐工具列表可在课程笔记中找到。

守护进程 (Daemons) 👻

上一节我们介绍了键盘重映射,本节中我们来看看守护进程。你可能已经熟悉了通过命令行启动并结束的程序。守护进程则是在后台持续运行的程序,它们等待事件发生或为计算机提供特定功能。

例如,当你通过SSH连接到一台计算机时,接收方必须运行一个名为sshd的SSH守护进程。如果没有它,你就无法连接。当连接请求到达时,守护进程会检查授权并启动登录会话。

不同的操作系统以不同方式管理守护进程。在Linux中,常用的工具是systemd(系统守护进程)。你可以使用systemctl命令来检查、启动、停止、启用或禁用守护进程。

更重要的是,你可以配置自己的systemd单元。例如,如果你想运行一个Web服务器,可以创建一个配置文件(单元文件)来告诉systemd如何执行它。这个文件会描述服务、依赖关系(如需要在网络启动后运行)、运行用户以及要执行的命令。

如果你需要定期运行命令(例如每天上午8点),可以使用cron守护进程。你只需在cron配置文件中指定命令和执行时间,cron就会自动处理。

用户空间文件系统 (FUSE) 💾

上一节我们介绍了守护进程,本节中我们来看看用户空间文件系统。现代操作系统支持多种文件系统(如EXT4、NTFS)。当你在用户层面执行文件操作时,内核会判断文件所在位置,并使用相应的文件系统模块来执行操作。

但用户代码通常无法定义如何创建文件。FUSE(用户空间文件系统)解决了这个问题。它允许在用户空间实现文件系统。当操作指向FUSE文件系统时,请求会被转发到用户空间的代码,由这段代码决定如何处理(例如发送邮件通知),然后再决定是否将操作转发回内核。

一个实用的例子是SSHFS。它通过FUSE创建一个本地目录,对该目录的所有文件操作(如创建、读取)都会通过SSH连接转发到远程服务器执行。对于本地程序而言,这个目录就像本地的一样。

人们利用FUSE实现了许多有趣的文件系统:

  • 云存储文件系统:将Dropbox、Google Drive、Amazon S3等云存储挂载为本地文件系统。
  • 加密文件系统:写入时自动加密,读取时自动解密。卸载后,磁盘上只留下加密文件。

数据备份 💾

上一节我们介绍了用户空间文件系统,本节中我们来看看数据备份。核心原则是:对于任何你关心的文件,如果没有备份,你随时可能失去它。

需要考虑多种故障场景:硬盘损坏、房屋火灾等。因此,仅在同一硬盘或同一地点做副本是不够的,你需要进行异地备份。

请注意,同步或镜像(如Google Drive、RAID)不等于备份。如果文件被误删、恶意加密或损坏,这些方案只会同步或复制出无用的数据。真正的备份方案需要能恢复历史版本。

除了本地文件,现代越来越多的数据只存在于云端(如仅通过网页访问的邮件)。如果忘记密码、账户被黑或服务商终止服务,这些数据就会丢失。因此,你也应该寻找工具来定期备份这些云端数据。

应用程序接口 (APIs) 🔌

上一节我们介绍了数据备份,本节中我们来看看如何通过API与外部世界交互。我们之前主要关注如何高效地在本地计算机上完成任务。但很多时候,你可以与外部服务集成。

大多数日常服务(如Facebook、Twitter、Google Drive)都提供API,允许你通过编程方式与它们的数据或服务交互。这些API通常有完善的文档。

你可以将这些API与我们之前学过的数据整理技巧结合。例如,美国政府提供一个免费API,通过特定URL返回指定地点的天气预报(JSON格式)。你可以使用curl获取数据,然后用jq解析JSON并提取所需信息,最终在终端中创建一个显示天气预报的别名。

使用API时通常涉及URL和参数。curl是一个用于获取URL内容的命令行工具。jq则是一个强大的JSON查询和解析工具。

许多API需要认证(如OAuth)。你会获得一个秘密令牌,需要将其包含在请求中(如在URL或HTTP头中)。请像保护密码一样保护这些令牌,不要将其提交到GitHub等公开仓库。

此外,还有像IFTTT这样的在线服务,可以集成多种服务并创建自动化工作流。

命令行惯例与技巧 💻

上一节我们介绍了API,本节中我们来看看一些常见的命令行惯例和技巧。虽然不同命令的参数各异,但存在一些通用的模式和概念。

以下是常见的命令行标志和概念:

  • --help / -h:显示命令的简要帮助信息。
  • --version / -V:显示软件的版本号,对提交错误报告很有用。
  • --verbose / -v:增加程序的输出信息,便于调试。通常可以重复使用(如-vvv)以获得更详细输出。
  • --quiet / -s:静默模式,只输出错误信息。
  • --dry-run:试运行模式,显示将会执行的操作,但不实际执行。对于破坏性操作非常有用。
  • -i (交互模式):在执行不可逆操作前进行确认提示(如rm -i, mv -i)。
  • -r / -R (递归):默认情况下,许多破坏性命令(如rm, cp)不会递归操作目录,需要显式使用此标志。
  • - (连字符):作为文件名参数时,表示从标准输入读取或输出到标准输出。
  • -- (双连字符):用于分隔选项和参数。之后的内容即使以-开头,也不会被解释为选项。例如,要删除名为-i的文件,应使用rm -- -i

窗口管理器 🪟

上一节我们介绍了命令行技巧,本节中我们来看看窗口管理器。你可能习惯了浮动窗口管理器(如Windows、macOS默认),窗口可以重叠、拖拽。

但还有另一种类型:平铺窗口管理器。在这种管理器下,所有窗口以平铺方式排列,不重叠,桌面背景不可见(除非没有打开任何程序)。打开新程序时,现有窗口会自动调整大小以容纳新窗口。

这类似于Tmux的分屏。平铺窗口管理器通常完全通过键盘快捷键进行操作(移动焦点、调整大小、交换窗口位置),无需使用鼠标,效率很高。值得尝试。

虚拟专用网络 (VPNs) 🔒

上一节我们介绍了窗口管理器,本节中我们来看看VPN。VPN最近很流行,但你需要了解它能做什么,不能做什么。

VPN本质上只是改变了你的互联网服务提供商(ISP),让你的流量看起来来自其他地方。这改变了你信任的对象:从你的本地网络提供商变为VPN服务商。你需要考虑这种信任转移是否值得。

在安全性方面,许多敏感流量(如HTTPS)本身已是加密的。即使在不安全的Wi-Fi上,加密数据也不会被窃听。VPN提供商如果配置不当、记录日志甚至出售你的数据,反而可能带来风险。

对于DNS查询等未加密流量,更好的解决方案是使用DNS over TLS/HTTPS等技术进行加密,而不是依赖VPN。

当然,在某些情况下VPN是有用的,例如连接到一个你信任的机构提供的VPN(如MIT VPN),这比公共Wi-Fi更可信。

Markdown 标记语言 📝

上一节我们介绍了VPN,本节中我们来看看Markdown。在未来的学习和工作中,你很可能需要撰写带简单格式的文本。

Markdown是一种轻量级标记语言,它让你能用近乎自然的书写方式添加格式。例如,用星号包围文字表示强调。本课程的所有讲义都是用Markdown编写的。

以下是Markdown的基本语法:

  • *文本*_文本_斜体
  • **文本**__文本__粗体
  • - 项目:无序列表项
  • 1. 项目:有序列表项
  • # 标题:一级标题
  • ## 标题:二级标题
  • `代码`:行内代码
  • ```代码块```:代码块(可在开头指定语言以实现语法高亮)

Markdown还支持链接、图片等。它被GitHub、许多聊天工具和网站广泛支持,非常值得学习。

桌面自动化 (Hammerspoon) 🤖

上一节我们介绍了Markdown,本节中我们来看看桌面自动化工具Hammerspoon。这是一个macOS上的工具(其他平台有类似工具),允许你编写Lua脚本来与操作系统各种功能交互(键盘、鼠标、窗口、显示器、文件系统、Wi-Fi等)。

通过几行代码,你可以实现许多酷炫功能:

  • 绑定热键移动窗口:例如,按Option+Cmd+右箭头将当前窗口移动到屏幕右侧。
  • 创建自定义菜单栏按钮:点击可执行特定操作,如“拯救窗口”(将移出屏幕的窗口拉回)。
  • 自动应用窗口布局:为不同场景(如实验室、宿舍)预设窗口布局,一键应用。
  • 基于情境的自动化:例如,检测到连接实验室Wi-Fi时自动静音扬声器;检测到使用了别人的电源适配器时弹出警告。

Hammerspoon让你能通过编程深度定制和自动化你的工作环境。

启动过程与Live USB 💻

上一节我们介绍了桌面自动化,本节中我们来看看计算机的启动过程和Live USB。操作系统并非计算机启动时运行的第一样东西。在启动过程中,你可以进入BIOS/UEFI设置或启动菜单(通常按F2、F12等键),配置硬件或选择从其他设备启动。

Live USB是一个包含完整操作系统的U盘。你可以让计算机从它启动,而不是从内置硬盘启动。这在以下情况非常有用:

  • 操作系统损坏,需要修复或取回数据。
  • 忘记了密码,需要修改系统文件来重置。
  • 想在不影响现有系统的情况下测试新操作系统。

课程笔记中提供了创建Live USB的工具链接。

虚拟机、容器与云 ☁️

上一节我们介绍了Live USB,本节中我们来看看虚拟机、容器和云服务。虚拟机(VM)和容器(如Docker)允许你在当前计算机内模拟一个完整的、隔离的计算机系统。

这对于创建隔离的测试或开发环境非常有用。例如,你在macOS上开发一个需要在Ubuntu上运行并依赖PostgreSQL的Web应用,你可以使用Vagrant这样的工具。通过一个简单的配置文件,指定操作系统和所需软件包,然后运行vagrant up,它就会自动创建并配置好这个虚拟机。之后你可以通过vagrant ssh登录进去进行开发。

除了在本地运行,你还可以在云服务商(如AWS、Google Cloud)那里租用虚拟机。这让你可以:

  • 获得一台永久在线、有公网IP的机器来运行网站或服务。
  • 临时使用拥有大量CPU、内存或GPU的强大机器进行深度学习等计算。
  • 快速创建大量机器(如1000台)来执行并行任务,用完后立即释放。

MIT CSAIL成员还可以通过OpenStack获取免费的用于研究的虚拟机。

笔记本编程环境 (Jupyter Notebooks) 📓

上一节我们介绍了虚拟机,本节中我们来看看笔记本编程环境。作为程序员,你可能习惯用Vim等编辑器编写完整程序后一次性运行。

Jupyter Notebook等工具提供了一种更交互式的编程方式。它将代码分成一个个“单元格”,你可以单独编写和执行每个单元格,并立即看到结果。这便于逐步构建和调试程序,尤其适用于数据科学和机器学习等领域的研究工作。

Jupyter Notebook通常在浏览器中运行,但后端(Python内核)可以运行在本地或远程机器上。例如,你可以在一台拥有强大GPU的远程服务器上运行Jupyter,然后在本地笔记本上连接并进行机器学习实验。

GitHub 贡献指南 🐙

上一节我们介绍了笔记本编程环境,本节中我们来看看如何为GitHub上的开源项目做贡献。GitHub是流行的开源软件开发平台,托管了许多项目的代码和协作工具。

为GitHub项目做贡献主要有两种方式:

  1. 提交问题 (Issues):当你使用某个软件时发现bug或有功能建议,可以到项目的GitHub页面提交一个高质量的问题报告。详细描述问题、复现步骤和期望行为,这对开发者非常有帮助。

  2. 提交拉取请求 (Pull Requests):这是直接贡献代码的方式。流程如下:

    • Fork项目:在GitHub上创建该仓库的一个副本到你自己的账户下。
    • 克隆到本地:将你Fork的仓库下载到本地计算机。
    • 创建分支并修改:创建一个新分支,进行代码修改(修复bug或添加功能)。
    • 提交并推送:提交更改并推送到你Fork的仓库。
    • 发起拉取请求:在你的Fork仓库页面向原始项目发起“拉取请求”,请求他们合并你的修改。
    • 代码审查与合并:项目维护者会审查你的代码,提出反馈。经过修改和讨论后,如果被接受,你的代码就会被合并到原始项目中。

通过这两种方式,你可以帮助改进你日常使用的工具,并为开源社区做出贡献。

总结 📚

本节课中我们一起学习了一系列实用的计算机杂项主题。我们从提升输入效率的键盘重映射开始,了解了在后台提供各种功能的守护进程,以及通过FUSE实现的灵活文件系统。我们强调了数据备份的重要性,并探索了如何通过API与外部服务交互。

我们还回顾了一些命令行惯例,介绍了平铺窗口管理器、VPN的利弊、轻量的Markdown标记语言,以及强大的桌面自动化工具Hammerspoon。接着,我们探讨了启动过程、Live USB、虚拟机/容器和云服务,以及交互式的Jupyter Notebook编程环境。最后,我们学习了如何通过提交问题和拉取请求为GitHub上的开源项目做贡献。

希望这些知识能帮助你更高效、更深入地使用计算机,并解决未来遇到的各种问题。

011:问答环节 (2020)

在本节课中,我们将对课程中涉及的各种主题进行自由形式的问答。我们将回答一些预先收集的问题,并尽力提供有帮助的解答。

操作系统相关主题学习建议

上一节我们介绍了课程的整体安排,本节中我们来看看关于学习操作系统底层概念的建议。

问题:对于学习进程、虚拟内存、中断、内存管理等操作系统相关主题,有什么推荐?

回答
这些是相当底层的概念,除非你需要直接处理它们(例如编写内核代码),否则通常不需要深入了解。进程是一个更通用的概念,我们在课程中已通过 htoppgrepkill 和信号等工具讨论过。

以下是学习这些主题的一些方法:

  • 实践课程:例如,麻省理工学院的 6.828 课程,它引导你基于给定的代码构建自己的操作系统。该课程的所有实验和资源都是公开的。
  • 在线教程:可以搜索“如何从零开始编写内核”之类的教程。这些教程通常使用 C++ 或 Rust 等语言,指导你完成一个基础内核的编写,虽不复杂,但能教授核心概念。
  • 书籍:对于想了解概念而非动手编程的学习者,推荐 Andrew Tanenbaum 的《现代操作系统》。此外,《FreeBSD 操作系统》一书详细介绍了 BSD 内核,其组织结构和文档可能比 Linux 内核更清晰,是更温和的入门选择。

应优先学习哪些工具?

在了解了操作系统学习路径后,我们来看看在日常工作中应优先掌握哪些工具。

问题:你会优先学习哪些工具?

回答
优先学习的工具取决于你的具体工作。核心思路是:识别你重复执行的任务,并寻找更高效的完成方式。

以下是几位讲师的观点:

  • 高效编辑:熟练掌握你的编辑器(如 Vim)至关重要,因为编辑文件将占据你大量时间。使用键盘而非鼠标能显著提升效率。
  • 依赖具体任务:优先学习对你日常工作最有用的工具。例如,如果你经常使用 Jupyter Notebooks 进行机器学习,那么熟悉其键盘快捷键就很重要。
  • 版本控制:在课程涵盖的主题中,版本控制(如 Git)和文本编辑器最为实用。与编辑器不同,如果不精通 Git,可能会导致数据丢失或协作困难。学习 Git 能为你未来省去很多麻烦,并且是与他人协作开发大型项目的必备技能。

如何选择脚本语言(Python vs Bash)?

掌握了核心工具后,我们常常需要编写自动化脚本。那么,该如何在 Python、Bash 或其他语言之间做出选择呢?

问题:我应该在何时使用 Python,何时使用 Bash 脚本,何时使用其他语言?

回答
这取决于你的具体任务。

  • Bash 脚本:适用于自动化运行一系列命令,特别是短小的、一次性的脚本。一旦脚本开始处理参数或进行复杂的文本处理,Bash 就会变得笨拙。
    • 经验法则:如果脚本少于 100 行左右,可以使用 Bash;超过这个长度,最好切换到更成熟的编程语言,如 Python。
  • Python(或其他语言):当需要进行任何业务逻辑、文本处理、配置管理,或者存在可复用的库时,应选择 Python 等语言。Bash 没有“库”的概念,只能调用系统上的程序。
  • 注意:Bash 脚本很难写得健壮(例如,处理带空格的文件名很容易出错),而使用真正的编程语言可以避免这类问题。

执行脚本与 Source 脚本的区别

在编写 Bash 脚本时,你可能会遇到两种运行方式。我们来澄清它们的区别。

问题:执行 (./script.sh) 一个脚本和 Source (source script.sh. script.sh) 一个脚本有什么区别?

回答
两者都会执行脚本中的代码,但关键区别在于执行环境

  • 执行脚本 (./script.sh):会启动一个新的 Bash 进程来运行脚本。脚本中的操作(如 cd 改变目录、定义函数)仅影响这个新进程。当脚本退出后,返回到原来的 Shell 时,原 Shell 的环境(如当前目录、定义的函数)保持不变。
  • Source 脚本 (source script.sh):是在当前的 Bash 会话进程中直接执行脚本。因此,脚本中的操作会直接影响当前 Shell 的环境。例如,脚本中的 cd 会改变你当前终端的目录,定义的函数也会在当前 Shell 中可用。

核心区别公式

  • ./script.sh => 新进程,不影响当前 Shell。
  • source script.sh => 当前进程,影响当前 Shell。

系统目录结构解析

运行脚本和程序时,系统需要知道它们的位置。接下来,我们看看系统和工具包是如何在文件系统中组织的。

问题:各种软件包和工具存储在什么地方?引用它们是如何工作的?/bin/usr/lib 到底是什么?

回答
这涉及到文件系统层次结构标准(FHS)的约定。

  • PATH 环境变量:当你输入命令时,Shell 会在一系列由冒号分隔的目录(存储在 $PATH 变量中)里查找可执行文件。使用 echo $PATH 可以查看这些目录,使用 which command 可以查找命令的具体位置。
  • 常见目录及其用途
    • bin:包含二进制可执行文件
      • /bin:系统必备工具。
      • /usr/bin:用户安装的程序。
      • /usr/local/bin:用户自己编译安装的程序。
    • lib:包含库文件,供程序链接使用。
    • /etc:系统配置文件
    • /home用户主目录
    • /tmp临时目录,重启后可能被清空。
    • /var:存放经常变化的文件,如日志
    • /dev设备文件
    • /opt:通常用于安装第三方商业软件。

注意:不同 Linux 发行版或操作系统(如 BSD、macOS)的目录结构可能略有差异。

系统包管理器与语言包管理器的选择

安装软件时,我们面临另一个选择:使用系统包管理器还是语言特定的包管理器?

问题:我应该用 apt-get install python-package 还是 pip install package 来安装 Python 包?

回答:这取决于你的需求。

  • 系统包管理器 (如 apt)
    • 优点:集中管理,所有软件通过一个工具安装和升级。可能更稳定,并且针对特定系统架构有预编译包(在某些平台如 ARM 上安装更快)。
    • 缺点:软件版本可能较旧
  • 语言包管理器 (如 pip)
    • 优点:通常能获得最新版本的软件。与虚拟环境(如 virtualenv)结合,可以为不同项目创建独立的依赖环境,避免版本冲突。
    • 缺点:可能需要从源码编译(耗时),并且管理上不如系统包集中。

建议:对于需要最新功能的 Python 包,使用 pip。对于希望系统全局一致、或者在不常见架构上希望快速安装的情况,可以考虑系统包。虚拟环境是解决依赖隔离的推荐做法。

代码性能分析工具推荐

在开发中,优化代码性能是一个常见需求。我们来探讨一下有哪些好用的性能分析工具。

问题:有哪些最简单、最好的性能分析工具可以用来改进我的代码性能?

回答

  • 最简单的方法:使用 print 语句和 time 模块进行二分查找。在代码开始记录时间,然后在不同位置打印耗时,逐步定位最耗时的代码段。这种方法直接有效。
  • 更专业的工具
    • Valgrind (Callgrind/Cachegrind):可以详细分析程序运行时间、函数调用关系,并生成代码行的热点图。但会使程序运行慢很多。
    • perf (Linux):进行采样分析,可以快速获得性能数据,但解读起来可能更复杂。
    • 火焰图:是可视化性能分析数据(如来自 perf)的绝佳方式。
    • 语言特定工具:许多语言有内置的优秀分析器,如 Go 的 pprof、浏览器开发者工具中的 JavaScript 分析器,应优先使用。
  • 高级场景:如果性能瓶颈在于 I/O 等待(如网络、磁盘),可以使用像 BPF (eBPF) 这样的内核追踪工具(如 bpftrace)来分析系统调用耗时等底层信息。

实用的浏览器插件

工欲善其事,必先利其器。在网页浏览方面,一些插件能极大提升效率和安全性。

问题:你们使用哪些浏览器插件?

回答

  • uBlock Origin:强大的广告和网络请求过滤工具。可以拦截广告、跟踪器,甚至通过高级模式精细控制脚本、图片等资源的加载,提升安全和速度。
  • Stylus:用于自定义网站 CSS 样式。可以修改网站外观(如启用深色模式)、隐藏不需要的页面元素。注意:请使用开源的 Stylus,而非已被收购的 “Stylish”,后者曾有窃取用户历史记录的不良行为。
  • 多账户容器 (Firefox):允许你在浏览器内创建多个隔离的会话容器。例如,为 Google、Amazon 分别设置容器,防止它们之间相互跟踪。
  • 密码管理器集成插件:与密码管理器(如 Bitwarden、1Password)的浏览器扩展集成,可以自动填充登录信息,并有效防范钓鱼网站(因为扩展只会在你访问正确的域名时才填充密码)。

其他有用的数据处理工具

在数据整理方面,除了课程介绍的 grep, sed, awk,还有哪些利器?

问题:有哪些其他有用的数据整理工具?

回答

  • curl:强大的命令行工具,用于发送网络请求、下载/上传文件。
  • jqpup:分别用于在命令行中处理 JSON 和 HTML 数据。
  • Perl:特别擅长文本处理,对于复杂的单行命令或脚本,有时比组合使用 grep/sed/awk 更简洁高效。
  • Vim 宏:对于复杂的、难以用管道命令表述的文本转换,录制并执行 Vim 宏可能更快。
  • Python (Pandas):处理表格数据、进行分组统计、生成汇总报表的绝佳选择,尤其在 Jupyter Notebook 中交互使用。
  • Pandoc:文档格式转换的“瑞士军刀”,支持在 Markdown、LaTeX、HTML、Word 等众多格式间相互转换。
  • R (ggplot2):虽然 R 语言本身可能不那么令人愉悦,但其 ggplot2 库在生成复杂的、多变量的统计可视化图表方面非常强大和便捷。

Docker 与虚拟机的区别

在部署和隔离应用环境时,Docker 和虚拟机是两种常见技术。我们来辨析它们的核心区别。

问题:Docker 和虚拟机有什么区别?

回答

  • 虚拟机:通过** Hypervisor** 层模拟完整的硬件,并在其上运行一个完整的客户操作系统内核。隔离性强,但开销大(需要分配独占的内存、磁盘,运行完整的 OS)。
  • Docker (容器):利用宿主机的 Linux 内核功能(如 namespaces, cgroups),提供隔离的运行环境。容器与宿主机共享同一个内核,因此启动更快、资源开销更小。
  • 核心区别
    • 隔离性:虚拟机提供更强的硬件和内核级隔离;容器共享内核,隔离性相对较弱。
    • 性能:容器性能开销远小于虚拟机。
    • 镜像:Docker 镜像通常更轻量,专注于应用及其依赖;虚拟机镜像包含整个操作系统。
    • 持久化:Docker 容器默认是无状态的,持久化数据需要显式使用卷;虚拟机磁盘通常是持久化的。

公式化理解

  • 虚拟机 = 虚拟硬件 + 完整客户 OS + 应用
  • Docker 容器 = 共享宿主机内核 + 隔离环境 + 应用

如何选择操作系统和 Linux 发行版

面对众多的操作系统和 Linux 发行版,初学者该如何选择?

问题:不同操作系统有什么优势?我们如何选择,例如为我们的目的选择最佳的 Linux 发行版?

回答

  • Linux 发行版选择
    • 新手/求稳:推荐 UbuntuDebian。它们用户友好、软件丰富、社区支持好。Debian 更注重稳定性。
    • 喜欢折腾/追新:可以考虑 Arch Linux 等滚动更新发行版。软件版本最新,但可能需要自己解决更多问题。
    • 通用建议:对于大多数任务,发行版的选择并不关键。不同发行版主要在软件更新策略(稳定 vs 激进)、包管理工具(apt, yum, pacman)和预装软件上有所区别。共同点远多于差异。
  • BSD:如 FreeBSD,提供一个由同一团队维护的完整、文档清晰的系统。与 Linux 的“内核+分散软件”模式不同,但软件生态相对较小。
  • macOS:介于 Windows 和 Linux 之间,提供了相对精致的界面和底层命令行工具的访问,是开发者的一个流行选择。
  • Windows:除非你需要开发 Windows 特定应用或进行游戏,对于编程而言,Linux 或 macOS 通常是更高效的选择。可以通过双系统或虚拟机解决兼容性问题。

Vim 使用技巧补充

在编辑器的世界里,Vim 以其高效著称。除了课程内容,这里还有一些进阶技巧。

问题:更多的 Vim 技巧?

回答

  • 插件:探索 Vim 插件生态,有很多能极大提升效率的插件(讲师们的配置文件中包含了许多)。
  • Leader 键:将常用操作映射到 Leader 键(如空格)加其他键的组合上,可以节省大量击键。
  • :学习录制和使用宏,自动化重复的编辑任务。
  • 标记:使用 m{a-z} 设置标记,再用 ‘{a-z} 跳回,方便在文件不同位置间快速切换。
  • 跳转历史Ctrl-o 跳回之前的位置,Ctrl-i 向前跳转,可以在文件内甚至跨文件导航。
  • 旧版本:earlier 1m 可以将文件恢复到1分钟前的状态,基于时间而非操作历史。
  • 撤销树:Vim 的撤销历史是一棵树,有插件可以可视化浏览,方便找回历史状态。
  • 搜索作为文本对象d/pattern 可以删除从当前位置到下一个匹配 pattern 处的文本,非常强大。
  • 持久化撤销:设置 undodir,让 Vim 在退出后仍保存文件的撤销历史。
  • 寄存器:熟悉不同的寄存器(如无名寄存器 、复制寄存器 0、系统剪贴板寄存器 +/*),实现高效的复制粘贴。

双因素认证的重要性

在安全方面,双因素认证是一个简单而有效的增强措施。

问题:关于双因素认证?

回答
对于任何安全敏感的账户(如 GitHub、主要邮箱),都强烈建议启用双因素认证

  • 认证方式
    • U2F/WebAuthn 安全密钥:物理密钥(如 YubiKey),是目前最安全的方式,能有效防范钓鱼攻击。
    • TOTP 验证器应用:使用手机应用(如 Google Authenticator, Authy)生成一次性密码,是很好的选择。
    • SMS 短信验证安全性较低,容易受到 SIM 卡交换攻击,不推荐作为首选。
  • 核心建议:优先使用 U2F 安全密钥,其次使用 TOTP 验证器应用

网页浏览器之间的差异

最后,我们简要讨论一下不同网页浏览器的现状。

问题:对网页浏览器之间的差异有什么评论?

回答
如今,主流浏览器之间的差异正在变小。

  • Chromium 内核阵营:包括 Google ChromeMicrosoft Edge新版 Opera 以及许多其他浏览器。它们共享相同的渲染引擎,在兼容性和性能上很接近。
  • Firefox:使用自家的 Gecko 引擎。它是重要的多元化选择,更注重隐私、可定制性,且不与某一家公司深度绑定。其 Servo 项目(用 Rust 编写的新引擎)的部分成果已集成到 Firefox 中,值得关注。
  • Safari:主要在苹果生态内使用。新版也紧跟网页标准。
  • 新兴浏览器:如 F,正在尝试用 Rust 编写全新的浏览器引擎,虽然尚未成熟,但代表了有益的探索。

简单建议:出于隐私和生态健康考虑,推荐使用 Firefox。如果追求极致的网页兼容性和性能,Chrome 或基于 Chromium 的 Edge 也是可靠的选择。


本节课中,我们一起探讨了从操作系统学习到工具选择,从脚本编写到性能优化,再到浏览器和安全等多个方面的实用问题。希望这些问答能帮助你更好地利用计算机工具,解决实际工作中遇到的挑战。感谢参与“计算机科学教育中遗漏的一学期”课程!

posted @ 2026-03-29 09:23  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报