Shell 执行环境与初始化

本章回答:你在终端敲命令时,底层发生了什么?环境变量从哪来?登录 Shell 和非登录 Shell 的初始化路径为什么不同?

一、Shell 交互的底层机制

当你在终端输入命令并按下回车时,Shell 执行以下操作:

  1. 读取输入:从终端(或 PTY)读取命令行字符串。
  2. 解析与展开:进行词法分析、通配符展开(glob)、变量替换、命令替换、别名展开等。
  3. 重定向处理:处理 <>| 等 I/O 重定向。
  4. 查找命令
    • 若为内置命令(如 cdechoexit),由 Shell 自身执行,不创建新进程。
    • 若为外部命令,按 PATH 顺序搜索可执行文件。
  5. 执行外部命令:调用 fork() 创建子进程,在子进程中调用 execve() 加载目标程序。execve() 接受三个参数:文件路径、参数数组(argv)、环境变量数组(envp
  6. 等待与状态返回:父进程(Shell)等待子进程退出,捕获退出状态码($?),然后回到步骤 1。

关键认知

  • 环境变量不是“系统全局的”,而是 按进程传递的字符串数组。每个进程从父进程继承环境变量,在 execve() 时传递给子进程。
  • 子进程无法修改父进程的环境变量(单向继承)。
  • export 的本质是:告诉 Shell 在调用 execve() 时,将该变量包含在 envp 数组中。

二、POSIX Shell 标准

POSIX(可移植操作系统接口)定义了 Shell 命令语言的标准语法和行为。符合 POSIX 的 Shell(如 dash,或 bash --posix 模式)提供一组最小公分母功能:

特性POSIX 定义非 POSIX 扩展(常见于 Bash/Zsh)
变量赋值VAR=value关联数组 declare -A
条件判断[ ]test 命令)[[ ]](Bash 扩展)
循环forwhileuntilselectuntil(部分扩展)
函数func() { ...; }function func { ...; }
数组无原生数组支持(a b c) 索引数组

实践意义

  • 编写需要在多发行版、多 Shell 下运行的脚本时,遵循 POSIX 语法(#!/bin/sh)确保最大可移植性。
  • 日常交互式配置(如提示符、补全)可使用 Shell 专属扩展,不受 POSIX 约束。

三、初始化文件层级(登录 Shell vs 非登录 Shell)

Shell 启动时读取的配置文件由 Shell 类型 决定:

  • 登录 Shell:通过 --login 选项启动,或 su -、SSH 登录、TTY 登录。其初始化行为是 “一次性环境搭建”,用于加载系统级和用户级的环境变量、启动窗口管理器等。
  • 非登录交互式 Shell:在已有用户会话中打开的终端模拟器。其初始化行为是 “每次加载用户偏好”,用于加载别名、函数、提示符等。
  • 非交互式 Shell:执行 Shell 脚本时启动。通常不加载 rc 文件,仅继承父进程的环境变量。

POSIX 定义的标准文件(由 sh 及其兼容 Shell 遵循):

  • /etc/profile:系统级登录 Shell 配置(所有用户共享)。
  • ~/.profile:用户级登录 Shell 配置(POSIX 通用,bash/sh/zsh 登录时都会读取,前提是 ~/.bash_profile 不存在)。

Bash 特定的扩展文件(实际应用最广):

  • ~/.bash_profile:若存在,bash --login 会优先读取此文件,而非 ~/.profile
  • ~/.bashrc:交互式非登录 Bash 会话的配置文件。

加载顺序汇总(Bash 为主流基线)

Shell 类型加载顺序(按优先级)
登录 Shell/etc/profile~/.bash_profile(或 ~/.bash_login~/.profile
交互式非登录 Shell/etc/bash.bashrc(发行版差异) → ~/.bashrc
非交互式 Shell不加载任何 rc 文件(但 BASH_ENV 变量可指定脚本)

典型配置策略

  • ~/.bashrc:存放别名、函数、提示符、PATH 追加(每次交互式终端都加载)。
  • ~/.bash_profile:存放仅登录时执行的任务(如启动图形界面),并手动 source ~/.bashrc

四、环境变量的来源与作用域

来源作用范围加载时机说明
内核初始环境PID 1系统启动极其精简,通常只含 TERMPATH 基值
/etc/environment系统全局PAM 会话初始化所有用户的 envp 基座,语法为单行 KEY=val,不支持变量展开
/etc/profile系统全局登录 Shell设置系统级 PATHLANGumask
~/.profile用户登录登录 ShellPOSIX 通用,用户级环境变量
~/.bash_profile用户登录Bash 登录 Shell优先于 ~/.profile
~/.bashrc用户交互交互式非登录 Bash别名、函数、提示符
Systemd 用户服务用户会话用户服务启动~/.config/environment.d/User= 的环境变量定义

Systemd 与 SSH 登录的环境差异

  • systemctl --user 启动的服务继承的是 Systemd 用户会话环境,而非 ~/.bashrc 中的配置。
  • 若要在 Systemd 用户服务中设置环境变量,应使用 ~/.config/environment.d/ 或 Unit 文件中的 Environment= 指令,而非依赖 Shell 启动文件。

五、脚本执行的环境隔离

Cron 任务:由 cron 守护进程启动,环境变量极其精简(通常只有 HOMELOGNAMESHELLPATH=/usr/bin:/bin)。若依赖自定义环境变量,需在 crontab 中显式设置,或使用绝对路径调用命令。

Systemd 定时器:与 Systemd 服务单元共享环境配置,可通过 Environment=EnvironmentFile= 指定。

SSH 远程命令ssh host 'cmd'):通过非交互式登录 Shell 执行,会加载 /etc/profile~/.profile(取决于发行版),但不会加载 ~/.bashrc(除非显式 source)。

笔记结语

Shell 执行环境的核心认知:

  1. 环境变量是 execve() 的第三个参数 —— 每个进程从父进程继承,单向传递,export 控制的是它是否被写入 envp
  2. 登录 Shell 与交互式 Shell 的初始化路径不同 —— 前者用于“一次性初始化”,后者用于“每次加载偏好”。
  3. POSIX Shell 是脚本可移植性的基线 —— 交互式配置可用 Shell 专属扩展,但跨发行版脚本应优先遵循 POSIX 语法。
  4. 不同启动上下文的环境来源不同 —— 登录会话、Systemd 用户服务、Cron 任务各自继承不同的环境基座,理解这一点是排障“命令找不到”类问题的核心。