Zhenyuan Yang Operating Systems & ML Systems

Shell 基础知识

从 shell 谈起

“shell”,本意指(坚果、蛋类、贝类等)坚硬的外壳,在计算机科学中,特别是在类 Unix 系统(如 Linux、macOS)领域,是一个核心概念。它本质上是一种特殊的应用程序,充当用户与操作系统内核(Kernel)之间的命令解释器(Command Interpreter)。(同时也有 Linux 内核的一个外层保护工具的说法,“坚硬的外壳”)

用户通过文本命令行界面(CLI)输入命令。shell 程序负责接收这些命令,解析其语法和语义,将其转换为操作系统内核能够理解和执行的操作指令,并管理命令的执行过程(包括输入/输出重定向、管道连接、后台运行等)。当需要自动化执行一系列命令时,用户可以将这些命令以及必要的控制逻辑(如条件判断、循环)编写成一个文本文件,称为 shell 脚本(shell Script),然后交由相应的 shell 程序解释执行。

用户、Shell 与操作系统内核的交互关系

存在多种不同的 shell 程序实现,每一种都定义了自己的命令解释语法和脚本语言。其中比较流行的包括:

  • Bourne shell (sh) - 经典 Unix shell
  • C shell (csh)
  • Korn shell (ksh)
  • Bourne Again shell (bash) - 目前大多数 Linux 发行版的默认 shell
  • Z shell (zsh) - macOS Catalina (10.15) 及之后版本的默认 shell
  • Fish shell (fish)
  • Almquist shell (dash / ash) - 常用于轻量级系统或作为 /bin/sh 的替代

聊聊 Bash

Bash 是什么?

Bash (Bourne-Again Shell) 是 GNU 项目开发的一个 shell 程序,旨在兼容并扩展传统的 Bourne shell (sh)。它曾经是大多数 Linux 发行版和旧版本 macOS (Catalina 之前) 的默认交互式 shell 和脚本解释器。

Bash 是一个遵循 POSIX 标准的 Unix shell 和命令语言解释器。

Bash 的主要特点和增强包括:

  • 高度兼容传统的 Bourne shell (sh)。
  • 强大的命令行编辑功能(如历史命令回查 history、命令补全 Tab)。
  • 支持数组变量。
  • 更丰富的条件测试和流程控制语法(如 [[ ]] 条件判断)。
  • 改进的变量处理机制(如变量扩展 ${var})。
  • 内置命令集丰富。

理解 Bash shell 的角色与语言

需要明确的是,不同的 shell 程序(如 Bash, Zsh, Fish, Dash)虽然共享一些基本概念和核心语法(特别是基于 Bourne shell 的),但它们在具体功能、扩展语法、配置方式以及脚本兼容性上存在差异。因此,在编写脚本或输入复杂的命令时,了解当前正在使用的是哪一种具体的 shell 实现非常重要。

用户通常笼统地称他们编写的自动化命令文件为 “shell 脚本”。从广义上讲,这指的是使用某种 shell 语言编写的脚本。更严谨的说法是指明具体的 shell 类型,例如 “Bash 脚本”、“Zsh 脚本” 或 “POSIX shell 脚本”(指仅使用标准 sh 特性的脚本)。这类似于区分 “Python 代码” 和 “Java 代码”,虽然都是“源码”,但语言不同。

核心理解:

  • shell 程序(如 Bash)本身就是一个应用程序。 它的核心功能是读取用户的输入(命令或脚本),解释并执行它们
  • shell 语言 是用户与 shell 程序交互时所遵循的语法规则。它定义了如何组合命令、使用变量、进行流程控制等。
  • Bash 作为最广泛使用的 shell 程序之一,实现了 Bash shell 语言。这种语言使得用户能够通过编写命令和脚本,高效地调用操作系统功能、管理文件和进程、组合实用程序以及实现自动化任务。

简言之:Bash 是一个实现了 Bash shell 语言的程序,其设计目标是为用户提供一个强大的命令行环境和脚本解释能力,用于与操作系统交互和执行任务。

Bash shell 的工作方式

  • 交互模式

    交互模式下,Bash shell 会等待用户发出命令然后再执行他们。Bash 会执行用户的每一条命令,且在它执行这条命令的期间,你将无法和 bash shell 交互。一旦它执行完这条命令,你就可以继续向它发出你的下一条指令。

  • 非交互模式

    Bash shell 也可以执行脚本。所谓脚本,就是提前写好的一系列指令,Bash 可以直接执行它们而无需停下来询问用户下一步要做什么。脚本通常以文件形式保存,多用来自动化执行各种任务。

除了 Bash 执行的命令来源不同,这两种模式非常相似。现在基本可以这样总结:如果 Bash 等待你给出执行任务的指令,你就处于交互模式中;如果它执行的是存在某文件中的指令,那它就是在非交互模式下运行一个脚本。

Bash 程序运行于基于文本的界面,本身不提供图形用户界面(GUI),因此用户必须通过终端(terminal)与其进行交互。现代终端通常是终端模拟器,即运行在图形界面中的软件程序,它们模拟传统物理终端,提供文本输入输出功能。

终端模拟器与 Shell 的关系

终端模拟器的选择依赖于操作系统:

  • Linux/BSD
    • 常见选项包括:rxvtxtermgnome-terminalkonsole
  • macOS
    • 可使用:Terminal.appiTerm2
  • Windows
    • 可用程序包括:cmd.exeConsole2mintty 等。

选择一个你喜欢的终端模拟器并启动它,是开始与 Bash 进行有效交互的第一步。

当用户在图形界面中打开终端模拟器时,会弹出一个窗口,展示程序的输入与输出。窗口内的文本既有运行在终端中的各类程序输出的结果,也有你通过键盘输入的命令。需要强调的是,负责在屏幕上渲染文本的是终端模拟器,而非 Bash;终端读取来自 Bash(或其他程序)的文本,并将其显示在窗口中。终端同样为邮件客户端、聊天工具等基于文本的程序提供渲染功能,与 Bash 无关。

文本,终端,bash,程序,输入,输出

终端模拟器内往往同时运行多个程序,它们通过输入输出流相互协作,却缺乏直观的可视化提示。因此,了解进程何时启动、如何通信以及何时终止,对清晰把握基于文本的用户界面尤为重要。

下面以本地与远程交互为例:

  1. 在本地终端中,你启动 Bash,然后通过 SSH 发起到远程主机(例如 IP 为 192.168.1.1)的连接。
  2. 远程主机上,会新建一个 Bash 进程,其输入输出通过网络隧道回传到本地终端。
  3. 在远程 Bash 中,你启动了 screen(终端复用器),它能够在同一个终端窗口内模拟多个子终端,通过快捷键切换或分屏并行查看。
  4. screen 的一个子终端中,再启动一个 Bash,并运行邮件程序(如 mail),输入邮件内容。

其中,“文本”在计算机科学中通常称为“字符串”,即由字符序列组成的数据结构。字符串既可以是简短的姓名(例如 “Leonard Cohen”),也可以是多行诗歌。

本地终端、远程 Shell 与子进程的层级关系

那么,程序究竟是什么?以及它们如何通信?

  • 程序:预先编写并存储在磁盘上的指令集合。操作系统的内核负责加载并执行程序指令。
  • 进程:当内核运行某个程序时,会为其创建一个进程实例,该实例包含程序的指令和内存空间。多个同一程序的进程可以并行运行,彼此互不干扰。
  • 文件描述符(file descriptor, FD):进程用来连接外部资源(文件、设备、其他进程)的整数标识符。

    • FD 0:标准输入(stdin),默认与终端接收的键盘输入相连;
    • FD 1:标准输出(stdout),默认与终端显示相连;
    • FD 2:标准错误(stderr),也默认与终端显示相连;
    • 进程可根据需要创建新的 FD,并将其重定向至文件、管道或套接字。

进程通过文件描述符连接输入与输出

当一个程序需要将输出传递给另一个程序时,会请求内核将其 stdout 重定向到目标进程的 stdin,形成管道(pipe)。此时,一方的输出流在内核中链接至另一方的输入流,字节按顺序到达并被读取,无法倒转。读取后字节会从管道中移除,流继续前行。

例如,对以下 Bash 脚本:

#!/usr/bin/env bash
$ (echo "Your name?" >&2; read name; echo "$name" ) | ( while read name; do echo "Hello, $name"; done )
Your name?
Maarten Billemont
Hello, Maarten Billemont

这条 Bash 命令通过两个子 shell 和管道连接实现交互流程:左侧子 shell 使用 echo “Your name?” >&2 将提示信息重定向到标准错误输出(stderr),避免干扰标准输出(stdout);随后读取用户输入并将其通过 stdout 传给右侧子 shell;右侧子 shell 从标准输入(stdin)读取该数据并输出个性化问候语。

两个进程通过管道连接输出流与输入流