Shell 执行环境与初始化
本章回答:你在终端敲命令时,底层发生了什么?环境变量从哪来?登录 Shell 和非登录 Shell 的初始化路径为什么不同?
一、Shell 交互的底层机制
当你在终端输入命令并按下回车时,Shell 执行以下操作:
- 读取输入:从终端(或 PTY)读取命令行字符串。
- 解析与展开:进行词法分析、通配符展开(glob)、变量替换、命令替换、别名展开等。
- 重定向处理:处理
<、>、|等 I/O 重定向。 - 查找命令:
- 若为内置命令(如
cd、echo、exit),由 Shell 自身执行,不创建新进程。 - 若为外部命令,按
PATH顺序搜索可执行文件。
- 若为内置命令(如
- 执行外部命令:调用
fork()创建子进程,在子进程中调用execve()加载目标程序。execve()接受三个参数:文件路径、参数数组(argv)、环境变量数组(envp)。 - 等待与状态返回:父进程(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 扩展) |
| 循环 | for、while、until | select、until(部分扩展) |
| 函数 | 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 | 系统启动 | 极其精简,通常只含 TERM 和 PATH 基值 |
/etc/environment | 系统全局 | PAM 会话初始化 | 所有用户的 envp 基座,语法为单行 KEY=val,不支持变量展开 |
/etc/profile | 系统全局 | 登录 Shell | 设置系统级 PATH、LANG、umask |
~/.profile | 用户登录 | 登录 Shell | POSIX 通用,用户级环境变量 |
~/.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 守护进程启动,环境变量极其精简(通常只有 HOME、LOGNAME、SHELL、PATH=/usr/bin:/bin)。若依赖自定义环境变量,需在 crontab 中显式设置,或使用绝对路径调用命令。
Systemd 定时器:与 Systemd 服务单元共享环境配置,可通过 Environment= 或 EnvironmentFile= 指定。
SSH 远程命令(ssh host 'cmd'):通过非交互式登录 Shell 执行,会加载 /etc/profile 和 ~/.profile(取决于发行版),但不会加载 ~/.bashrc(除非显式 source)。
笔记结语
Shell 执行环境的核心认知:
- 环境变量是
execve()的第三个参数 —— 每个进程从父进程继承,单向传递,export控制的是它是否被写入envp。 - 登录 Shell 与交互式 Shell 的初始化路径不同 —— 前者用于“一次性初始化”,后者用于“每次加载偏好”。
- POSIX Shell 是脚本可移植性的基线 —— 交互式配置可用 Shell 专属扩展,但跨发行版脚本应优先遵循 POSIX 语法。
- 不同启动上下文的环境来源不同 —— 登录会话、Systemd 用户服务、Cron 任务各自继承不同的环境基座,理解这一点是排障“命令找不到”类问题的核心。