MSTcheng头像
【Linux进程篇】Linux学习第十七弹——Linux 命令行参数与环境变量详解:从 Bash 到 exec,搞懂进程如何传参与继承封面图

【Linux进程篇】Linux学习第十七弹——Linux 命令行参数与环境变量详解:从 Bash 到 exec,搞懂进程如何传参与继承


在这里插入图片描述


🎬 个人主页:MSTcheng · CSDN
🌱 代码仓库 :MSTcheng · Gitee

🔥 精选专栏:
《C语言》《数据结构》 《算法学习》
《C++由浅入深》
《Linux操作系统》
《Mysql数据库》 《白话大模型》

💬座右铭: 路虽远行则将至,事虽难做则必成!

上一篇文章中,我们给大家介绍了Linux进程,讲解了Linux进程到底是怎么组织起来的,其中还讲了进程调度,学习了进程调度以后进程就能在脑子里“跑起来”了!本篇文章我们就进入到命令行参数和环境变量的学习。

文章目录

前置知识:从开机到 Bash,进程如何一路继承

在正式介绍命令行参数与环境变量之前,先带大家站在更高的视角看一眼:我们平时写的程序,到底是在一条怎样的“进程链条”上运行的。

Linux 开机后的第一站:systemd

当 Linux 加电启动后,内核完成自身初始化,随后会创建第一个用户态进程——systemd,它的 PID 恒为 1。systemd 是整个用户态进程的“老祖宗”,随后它会拉起一大批系统服务进程和用户会话相关的进程。

从图形界面到终端:GDM → gnome-shell → bash

在众多被 systemd 拉起的进程中,有一个负责图形登录的进程叫 GDM。GDM 在用户登录成功后,会进一步创建桌面环境的核心进程 gnome-shell,把我们看到的图形界面跑起来。

当我们打开一个终端窗口时,事情还没有结束:gnome-shell 会读取 bashrc 配置文件,并据此创建出一个 bash 进程。这个 bash,正是我们平时敲命令、写代码、跑程序的最直接“上级”。

子进程继承自父进程:命令行参数与环境变量的由来

理解了上面的进程链,就很容易明白下面要讨论的核心事实——子进程会继承父进程的很多东西:

  1. 命令行参数:我们在 bash 中执行一个程序,本质上是 bash 作为父进程,创建出一个子进程去运行我们的代码。这个子进程的 main 函数所接收到的命令行参数,正是由父进程 bash 传给它的。
  2. 环境变量:bash 自身拥有一套环境变量,当它创建子进程时,子进程同样会继承这套环境变量。

所以,接下来要学习的命令行参数与环境变量,讨论的核心就是:子进程 main 的参数继承自 bash,环境变量也同样继承自 bash。理解了这条继承链,再看 main 函数的参数 argc、argv 以及环境变量的具体规则,就会顺畅很多。

了解完上面这这个完整链路之后,下面就来具体的看看命令行参数和环境变量!

一、命令行参数

1.1 问题一:main 函数可以有参数吗?

很多人误以为main函数是没有参数的,是因为以前写的是语言层的代码用不到,当是如果写系统层的代码就会用到了。main函数其实是有参数的!

示例:

int main(int argc, char *argv[]) {
    // ...
    return 0;
}

参数含义:

参数全称含义
argcargument count命令行参数 个数
argvargument vector命令行参数 列表(指针数组)
  • char *argv[] 是一个指针数组:数组的每个元素是一个指针,指向一个字符串;
  • argv[0]、argv[1]、argv[2]……分别指向不同的字符串。

1.2 实验:打印 argc 与 argv

代码:

#include <stdio.h>

int main(int argc, char *argv[]) {
    for (int i = 0; i < argc; i++) {
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    printf("argc = %d\n", argc);
    return 0;
}
命令argcargv
./commendTest1[0]=./code
./commendTest a2[0]=./code, [1]=a
./commendTest a b c d5[0]=./code, [1..4]=a b c d
…

在这里插入图片描述

规律:有几个"以空格分隔的字符串",argc 就是几(含程序名本身)

1.3 命令行参数的本质:shell 切割字符串

我们在命令行里输入的这一行东西本质是一个字符串。这个字符串被拿到了之后,会以空格作为分隔符,把这一个字符串转化成若干个字符串。"

关键点:

  • 我们输入的 ./commandTest a b c d(含空格)——本质上是一个字符串;
  • shell(未来的内容)把它按空格切割成若干子串;
  • 子串的个数 = argc;每个子串依次由 argv 的元素指向。

这个工作是 shell 做的,后面会手搓一个shell(bash)。

注意:多个连续空格会被当作一个分隔符,不会产生空的 argv 元素。

所以为什么要切割字符串?

命令行参数的本质应用,是为了能够实现一个命令可以根据不同的选项,实现不同的子功能!也是Linux中所有命令选项功能的实现方式!!(只有把字符串切割了,才能根据切割后的内容匹配不同的选项执行不同的内容!)

1.4 为什么要有命令行参数?(本质应用)

下面有一段看起来莫名其妙的代码演示:根据不同的选项(-a / -b / -c / -d)执行不同的功能。

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("请这样使用: %s -a|-b|-c|-d\n", argv[0]);
        return 1;
    }
    if (strcmp(argv[1], "-a") == 0) {
        printf("执行功能一\n");
    } else if (strcmp(argv[1], "-b") == 0) {
        printf("执行功能二\n");
    } else if (strcmp(argv[1], "-c") == 0) {
        printf("执行功能三\n");
    } else {
        printf("默认功能\n");
    }
    return 0;
}

结论:

命令行参数的本质应用,是为了实现 一个命令可以根据不同的选项实现不同的子功能,这就是 Linux 中所有命令选项的来源。

  • ls 是命令它是 C 写的,它有 main 函数、有参数,参数传进去后自己判断,根据判断实现不同功能。

对于命令行参数有三个细节要注意:

① 细节一:argc ≥ 1,argv[0] 一定是程序名

  • 命令行参数至少是1,即argc>=1,所以argv[o]一定会有元素,其所指向的就是层序名!!例如只输入ls或pwd命令argv[0]就指向ls或pwd.

②细节二:选项是"以空格分隔的字符串"

  • -a、-b 是一个个的字符串(哪怕只有一个字符);
  • 我们上面只是"打印"它,若加逻辑判断(如 strcmp),一个程序就能实现多种功能。

③细节三:argv 最后一个元素永远指向 NULL

  • 如果有argc个命令行参数,那么argv[argc-1]是最后一个,argv[argc]=NULL。
  • 因此可以这样遍历(更简洁):
for (int i = 0; argv[i] != NULL; i++) {   // i < argc 等价写法
    printf("%s\n", argv[i]);
}

1.5 用命令行参数理解 cat 命令

cat 后面跟文件名,它是怎么把文件名拿到的?

答案: cat是也命令!catfile.txt为什么呢能打印文件内容?cat命令也是C语言写的,所以它有 main 函数参数(argc/argv),能从命令行参数里获取文件名,那么cat命令内部就可以使用fopen以读的形式打开,然后再全部读取打印出来。所以本质就是命令行参数获取到文件内容,最后再打印文件内容!

cat 打印文件内容的本质流程:

① 通过命令行参数获取文件名(argv[1])
        │
        ▼
② 以只读方式打开文件(open / fopen)
        │
        ▼
③ 读取文件内容(read / fread)
        │
        ▼
④ 打印到显示器(stdout)

cat 打印文件内容的本质就是——通过命令行参数获取文件名,打开文件,读取文件,打印。

关联旧知识:之前讲过的输入重定向(cat < file)也是从文件读取(即cat识别到了<(符号)和file(文件)知道是从文件中读取)即将来<和file都会传入到cat程序的mian函数中的argv[]中,谁传的?—bash! 终于从"命令行参数"角度把它讲透了。

二、进一步理解命令行参数,引出环境变量

2.1 我们自己实现一个命令

#include <stdio.h>

int main(void)
{
    printf("我是一个命令,自己实现的\n");
    return 0;
}

编译:

gcc mycmd.c -o mycmd

运行:

./mycmd

输出:

我是一个命令,自己实现的

结论:命令不是什么神秘东西,就是“特定路径下的一个可执行文件”。 系统自带的 ls 本质就是 /usr/bin/ls 这个文件。

2.2 为什么必须写 ./mycmd

在这里插入图片描述

  • 直接输入 mycmd 会报 command not found。
  • 原因:shell 只去 PATH 列出的目录里找命令,默认不搜索当前目录 .。
  • ./ 表示“当前目录”,./mycmd 是 明确告诉 shell:就执行当前目录下这个文件。
  • 也可以用绝对路径:/home/whb/mycmd。
  • 我们自己写的命令,系统不应该就一定要认识,系统只认 PATH 里的路径!

2.3 PATH 的本质

查看:

echo $PATH

典型输出(冒号分隔):

/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
  • PATH 是一串目录,冒号 : 分隔。
  • shell 查找命令时从左到右依次查找,找到第一个就执行,后面的不再找。
  • 这就是“同名命令谁生效”的根源:排在前面的目录优先。

which 可以看到命令最终用的是哪个路径:

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which ls
/usr/bin/ls

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which pwd
/usr/bin/pwd

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which mycmd
         👈没反应,因为mycmd是我们自己实现的,不在环境变量PATH里面

2.4 如何把当前目录加入 PATH?

正确追加:

PATH=$PATH:$(pwd)

或规范写法(export 后面会讲):

export PATH=$PATH:$(pwd)

在这里插入图片描述
把当前目录导入到PATH这个环境变量中,那么PATH这个环境变量里就有了当前路径了!(记住PATH的本质就是一串目录)

  • PATH 是目录列表,bash 按顺序搜索不带路径的外部命令,找到第一个同名可执行文件就执行;优先级由 PATH 先后顺序决定,不是系统命令天然优先。你写的 ls,如果目录不在 PATH 里,bash 就找不到它,只能执行系统 ls;
    但如果你把自定义目录加到 PATH前面,你的 ls 就会覆盖系统 ls;所以生产环境一般不建议把自定义目录前置,追加到末尾是安全做法。
//此时我们再which我们自己的命令 它就能查到是在这个目录底下了!!!!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which myprocess
/home/mstcheng/117/code/linux-code/process/myprocess

//并且不用加./了,因为当前目录已经在环境变量PATH里了,所以我们直接写程序名
//即可运行我们自己写的程序了 就像ls,pwd,find这类命令一样!!!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ myprocess
i am a process pid:406557 ppid:45955

错误覆盖:

PATH=$(pwd)
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ PATH=$(pwd)

//如果改错了,那么PATH里面就只有当前路径了
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ echo $PATH
/home/mstcheng/117/code/linux-code/process

//我们再去执行ls,whoami,这样的命令就执行不了了!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ls
命令 “ls” 可在以下位置找到
 * /bin/ls
 * /usr/bin/ls
由于 /usr/bin:/bin 不在 PATH 环境变量中,故无法找到该命令。
ls:未找到命令

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ whoami
命令 “whoami” 可在以下位置找到
 * /bin/whoami
 * /usr/bin/whoami
由于 /usr/bin:/bin 不在 PATH 环境变量中,故无法找到该命令。
whoami:未找到命令

//那为什么pwd还能执行??因为它是内建命令(后面具体介绍)
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ pwd
/home/mstcheng/117/code/linux-code/process


通过上面的现象结论就是:

  • PATH 里只剩当前目录。
  • ls、cat、vim 等 外部命令全部失效(找不到)。
  • 但 cd、echo、pwd 等 内建命令仍然可用,因为它们不经过 PATH 查找。

恢复方法

  • 重新登录(退出当前 shell 再登录)即可恢复。
  • 原因后面讲:PATH 的真实来源是配置文件,你改的只是内存。

注意:

  • echo PATH 打印的是字面量 PATH;echo $PATH 才是变量值。
  • PATH=... 是覆盖,PATH=$PATH:... 才是追加。
  • 在 PATH 里放 . 或当前目录有安全风险:别人在你目录下放一个同名恶意程序,你敲命令就可能执行它。
  • 修改 PATH 只对当前 shell 及其子进程有效,不会影响其他终端,也不会持久化。

2.5 内建命令为什么不受影响

  • cd、echo、pwd、export、unset、set 等是 shell 内建(builtin)。
  • 它们由 bash 自己实现并直接执行,不需要 fork 新进程,也不需要去 PATH 找文件。
  • 所以 PATH 改坏后,你仍然能用 echo 打印、用 cd 切目录,但没法用 ls 看目录内容。

三、深入认识环境变量

3.1 env(environment) 命令

//直接输入env 你会看见有很多信息
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env
SHELL=/bin/bash
PWD=/home/mstcheng/117/code/linux-code/process  //环境变量里面也存在着当前的工作路径
PATH=/home/mstcheng/.npm-global/bin:/home/mstcheng/........//可以注意到PATH是在环境变量里面存在的
......

打印当前进程能看到的所有环境变量,格式为 NAME=value。

env 是独立可执行程序(外部命令,不是 shell 内置),它做的事情很简单:

  • 读取自身进程的环境变量表;
  • 逐行打印出来。

3.2 常见环境变量

变量含义课堂演示/说明
PATH命令搜索路径冒号分隔,决定命令去哪找
HOSTNAME主机名当前机器叫什么
TERM终端类型如 xterm,决定终端能力
SHELL当前使用的 shell通常是 /bin/bash
HISTSIZE历史命令保存条数history | wc -l 验证
SSH_CLIENT客户端 IP 和端口记录“谁从哪连过来”
SSH_TTY当前终端设备如 /dev/pts/1,每次 SSH 登录新建
PWD当前工作目录cd 后自动更新
OLDPWD上一次所在目录cd - 的本质
HOME用户家目录cd ~ / cd 使用
USER / LOGNAME当前用户名登录身份
LANG语言与字符编码影响中文显示/排序
MAIL邮件存放位置传统邮件程序使用

3.3 灵魂拷问:bash 的环境变量,到底是从哪来的?

看完上面这一大堆变量,你肯定想问一个问题:这些环境变量到底是从哪来的?为什么我自己改了,退出重进又复原了?

先做个实验:

# 自己造一个变量并导出
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export MY_VAR="hello"
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep MY_VAR
MY_VAR=hello

# 关掉终端,重新登录,再来看——
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep MY_VAR
# 空!啥都没有了,复原了

答案分两层:

  1. 环境变量的来源:来自 Linux 的配置文件——在我们启动 Linux / 登录的时候,系统自动帮我们配置;
  2. 声明周期(寿命):bash 进程的环境变量是存在内存中的,是临时的,是 内存级别的!!

我们使用 export、unset、改 PATH,改的都是当前 bash 进程内存里的那张表,不会自动写回配置文件。所以:当前 shell 立即生效 → 子进程也生效 → 退出重登后恢复原样。

与前置知识呼应:你登录后看到的命令行,是 bash 进程在跑。bash 也是进程——就是前置知识里那条链的末端:内核 → systemd(PID 1) → sshd / GDM → gnome-shell → bash。bash 启动时读取配置文件,把变量载入自己的内存;bash 退出,内存里的表就没了,所以"重登复原"。

验证一下 bash 真的是"一个进程一份内存":

mstcheng@ubuntu-dev:~$ ps axj | grep bash
# 每多开一个 SSH 会话,就多一个 bash 进程
# 每退出一个会话,就少一个
# 全部退出后,系统里就没有你的 bash 了

在这里插入图片描述

那"配置文件"具体是哪些?调用链放到第五节细讲,这里先记住结论。接下来先看几个常见变量。

3.4 cd - 与 OLDPWD(上一次所在目录)

//先进入到/etc这个目录
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ cd /etc

//再打印OLDPWD 
mstcheng@ubuntu-dev:/etc$ echo $OLDPWD
/home/mstcheng/117/code/linux-code/process  #发现打印出来的是我们上一个路径! 

//使用cd -就能迅速回到上一次的路径下
mstcheng@ubuntu-dev:/etc$ cd -
/home/mstcheng/117/code/linux-code/process

每次 cd,shell 会把"旧目录"写进 OLDPWD,把"新目录"写进 PWD。所以:

cd - 不是特殊魔法,本质就是 cd $OLDPWD。

  • PWD/OLDPWD 是 shell 维护的,还是系统调用?——shell 在 cd 时更新环境变量;pwd 内建命令也可通过 getcwd() 获取。

3.5 专题:cd ~ 与 HOME

mstcheng@ubuntu-dev:/etc$ cd

mstcheng@ubuntu-dev:~$ pwd
/home/mstcheng

cd 不带参数、cd ~ 都会回到 HOME。root 的 HOME 是 /root,普通用户通常是 /home/用户名。

与前知识置呼应:为什么每个用户必须有自己的家目录?因为配置文件放在各自家目录下(如 /home/mstcheng/.bash_profile)——每个用户登录读的是自己的配置文件,所以不同用户可以有不同环境变量;你改了 mstcheng 的配置,root 登录不受影响。

3.6 SSH_TTY 与"一切皆文件"

每建立一次 SSH 连接,系统就分配一个新的终端设备,如 /dev/pts/1、/dev/pts/2。SSH_TTY 记录当前会话对应的设备文件。

mstcheng@ubuntu-dev:~$ echo $SSH_TTY
/dev/pts/1	

# Linux 一切皆文件:终端也是文件,可以向它写数据
mstcheng@ubuntu-dev:~$ echo "hello" > /dev/pts/1

在这里插入图片描述
在这里插入图片描述
注意我这里演示的是本地的(同一台机器),不是SSH,但是跟SSH是一个效果的!

如果另一个终端正好是 /dev/pts/1,且你有权限,就能在对方终端上看到 hello。

补充(SSH_CLIENT 的精确格式):SSH_CLIENT=192.168.1.100 50000 22是三个字段:客户端 IP、客户端端口、服务器端口(22)。它和 SSH_TTY 正是 sshd 这条链传给 bash 的"证据"。

与前置知识呼应:你看到的 SSH_CLIENT / SSH_TTY 这两个环境变量为什么存在? 因为你是通过 sshd 登录的——systemd → sshd → bash,这两个变量就是 sshd 传下来的。

注意:

  • 为什么每次登录 /dev/pts/N 编号会变?——伪终端按需分配。
  • "一切皆文件"如何体现?——设备、管道、socket 都可按文件接口操作。

3.7 环境变量像什么?

环境变量就像你的身份信息、你的学籍信息等,这些东西一生都跟着你。

对应到程序:一个进程启动后,父进程给它的环境变量会一直跟着它;它 fork 出来的子进程、孙子进程也都能看到。

所以环境变量具有"全局"属性(准确说是沿进程树继承)。那问题来了:环境变量是谁给你的?——这个答案就是第四节的正片。

3.8 C 程序获取环境变量的三种方式

方式一:main 的第三个参数(main 函数是有参数的!)

#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;

    for (int i = 0; env[i] != NULL; i++)
        printf("env[%d]: %s\n", i, env[i]);

    return 0;
}

第三个参数就是环境变量表,同样以 NULL 结尾。注意:它没有"个数"参数(不像 argv 有 argc),只能靠 NULL 判断结束。

与前置知识呼应:这个 env[] 就是父进程 bash 传给子进程的——前置知识说"子进程 main 的参数继承自 bash",在这里具象化了:argv 和 env 都是 bash 在 fork/exec 时准备好的。

方式二:environ 全局变量

#include <stdio.h>
#include <unistd.h>

extern char **environ;

int main(void)
{
    for (int i = 0; environ[i] != NULL; i++)
        printf("environ[%d]: %s\n", i, environ[i]);

    return 0;
}

environ 是 libc 提供的全局指针,指向环境变量表;声明在 <unistd.h> 中。它和 main 的第三参是同一张表。

方式三:getenv(最常用、最佳实践)

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *path = getenv("PATH");
    if (path == NULL)
        printf("PATH 不存在\n");
    else
        printf("PATH = %s\n", path);

    return 0;
}
  • 函数原型:char *getenv(const char *name);,头文件 <stdlib.h>;
  • 按名字查找,找到返回值指针,找不到返回 NULL;
  • 实际开发优先用 getenv:简单、按名取值、不用自己遍历。

编译运行:

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ gcc -o myenv myenv.c
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ./myenv
env[0]: XDG_SESSION_ID=36981
env[1]: HOSTNAME=ubuntu-dev
env[2]: TERM=xterm
env[3]: SHELL=/bin/bash
env[4]: HISTSIZE=1000

注意:

  • getenv 返回的是表内字符串的指针:不要试图 free;也不要长期保存后假设它不变(环境变量表可能被修改)。
  • getenv 返回 NULL 必须判断,直接 printf("%s", p) 可能崩溃。
  • 环境变量名大小写敏感。

3.9 安全案例:用 getenv 做"用户白名单"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *who = getenv("USER");

    if (who == NULL)
        printf("无法执行,因为我不认识你\n");
    else if (strcmp(who, "root") == 0)
        printf("我也不准 root 执行\n");
    else if (strcmp(who, "mstcheng") == 0)   
        printf("恭喜你,你是合法用户,我让你执行\n");

    printf("who am I: %s\n", who);
    return 0;
}

以 mstcheng 登录运行 → 放行;以 root 登录运行 → 拒绝。

mstcheng@ubuntu-dev:~$ USER=mstcheng ./check
# 程序立刻认为你是 mstcheng,直接放行!
# 或者:
mstcheng@ubuntu-dev:~$ env USER=mstcheng ./check

你看,我们是不是骗过它了?这说明环境变量是可以被改的。

结论(重要)

  • 环境变量是用户可控的输入,绝不能用于身份认证 / 权限判断;
  • 真实系统做权限校验,必须用内核提供的可信身份:getuid()、geteuid();
  • 这是典型的信任边界问题:跨越信任边界的数据都必须校验。

注意注意:

  • 为什么不能用环境变量做鉴权?——可被调用者任意设置。
  • getuid 与 geteuid 的区别?——真实 UID vs 有效 UID(setuid 程序)。
  • 环境变量注入还能导致什么?——PATH 劫持(把恶意目录放 PATH 前面,同名命令被替换)、LD_PRELOAD 注入(预加载恶意共享库)、IFS 篡改等。
  • 不能以为"只有我能改我自己的环境变量"——任何能启动进程的人都能给子进程设置环境变量。
  • 用 USER 判断身份,属于典型安全漏洞。

四、核心:环境变量到底存在哪里

4.1 两张表:argv 表与环境变量表

把两张表画在一起:命令行参数表 char *argv[] 和环境变量表 char *env[] / char **environ。

它们的本质完全一样:

  • 都是指针数组,每个元素是 char *,指向一个字符串;
  • 都以 NULL 结尾;
  • 都存放在进程的用户空间内存里。
    在这里插入图片描述

在这里插入图片描述

4.2 是"内存级"的,不是磁盘上的

在前面的3.3节中我们已经铺垫过:export、unset、改 PATH,改的都是当前 bash 进程内存中的表,不会自动写回配置文件。所以:当前 shell 立即生效;所有子进程也生效;退出重登录后恢复原样。

4.3 bash 也是进程

你登录后看到的命令行,是 bash 进程在跑。bash 自己启动时,也要从某处拿到这两张表—— 命令行参数表 + 环境变量表。这两张表就是 bash 进程里的两个全局指针数组。

前置呼应:bash 的这两张表不是凭空来的,是它的父进程(sshd / gnome-shell)传下来的,加上配置文件载入的内容。整条链上每个进程的这两张表都这样。

4.4 为什么子进程不传参也能拿到环境变量

关键在 fork:你在 bash 里执行的每一条外部命令,都是 bash 通过 fork 创建的子进程。fork 之后,子进程拥有父进程地址空间的副本(写时拷贝,后面进程控制说),父进程内存里的两张全局表,子进程天然能看到。所以你的 C 程序 main 不写第三个参数,也能用 environ / getenv——因为表本来就在它继承来的内存里。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

char *argv_tbl[100];
char *env_tbl[100];

int main(void)
{
    argv_tbl[0] = "AAA";
    argv_tbl[1] = NULL;
    env_tbl[0] = "PATH=/usr/bin:/bin";
    env_tbl[1] = NULL;

    pid_t id = fork();

    if (id == 0) {
        // 子进程:直接遍历父进程的"全局表"
        for (int i = 0; argv_tbl[i]; i++)
            printf("argv_tbl[%d] = %s\n", i, argv_tbl[i]);
        for (int i = 0; env_tbl[i]; i++)
            printf("env_tbl[%d] = %s\n", i, env_tbl[i]);
    } else {
        // 父进程:先不退出,方便观察
        sleep(10000);
    }

    return 0;
}

结论:子进程能直接看到父进程未修改的全局数据。 这就是环境变量"全局"的底层原因。

4.5 进程树:bash 是"祖宗"

//这个是远程登录进程树
systemd / init
    └── sshd
         └── bash(你登录后的 shell)
              ├── ls
              ├── vim
              │    └── (vim 再 fork 的子进程)
              └── ./myenv
//这是本地 Ptyxis 终端进程树版本
systemd / init
    └── gnome-shell          # GNOME桌面外壳
         └── ptyxis         # Ptyxis GUI窗口前端进程
              └── ptyxis-agent(4817)  # PTY代理、bash的直接父进程
                   ├── bash(pts/0,第一个终端窗口)
                   │    ├── ls
                   │    ├── vim
                   │    └── ./myenv
                   ├── bash(pts/1,第二个终端窗口)
                   │    └── ...
                   └── bash(pts/2,第三个终端窗口)
                        └── ...

从 bash 开始,所有后代进程都能继承环境变量;自己写的进程还可以继续 fork 出"孙子进程",孙子同样能看到。

所以环境变量不是"系统全局",而是"从 bash 往下的整棵子树全局"。

前置呼应:把这条链往上接到前置知识——内核 → systemd(PID 1) → sshd / GDM → gnome-shell → bash → 你的命令,就是一个完整的"祖孙继承"树。systemd 是用户态老祖宗,它的环境变量由内核在启动时直接初始化;此后每一级都是父进程 fork 传下去的。

4.6 只读共享与写时拷贝

子进程只是"读"环境变量时,和父进程共享同一份物理内存(写时拷贝,Copy-On-Write)。一旦子进程要"写",内核才会给它复制一份新的物理页。所以:

  • 子进程修改环境变量,不会影响父进程;
  • 父进程修改,也不会影响已经启动的子进程(子进程看到的是 fork 那一刻的快照)。

4.7 结论:环境变量是父进程给的

给进程传环境变量的,是父进程,不是操作系统!

系统只负责在你登录时启动第一个 bash,并让 bash 读取配置文件;之后所有进程的环境变量,都是父进程 fork 时传下去的。至于 fork 内部到底怎么复制、怎么传这两张表,进程控制会介绍。

注意:

  • 环境变量是"操作系统全局变量",任何进程随便读——实际是继承来的。
  • export 后所有已存在进程都能看到——只有之后 fork 的子进程才能看到。

五、bash 的环境变量从哪来:配置文件

5.1 登录时发生了什么(完整流程)

  1. 你通过 SSH/Xshell 登录;
  2. 系统校验身份,启动一个 bash 进程;
  3. bash 启动时,读取配置文件;
  4. 配置文件是 shell 脚本,里面会设置 PATH、HOME、umask 等;
  5. bash 执行这些脚本,把变量载入自己的进程上下文(构建环境变量表);
  6. 之后你执行的命令,都是 bash 的子进程,继承这张表。

5.2 配置文件的调用链

层级文件说明
用户级~/.bash_profile登录 shell 启动时读取,内部通常 source ~/.bashrc
用户级~/.bashrc交互式 shell 配置,常被 .bash_profile 调用
系统级/etc/profile所有用户的登录配置
系统级/etc/bashrc(或 /etc/bash.bashrc)依发行版而定
系统级/etc/profile.d/*.sh按片段拼装系统配置

简化调用链:

登录 → ~/.bash_profile → source ~/.bashrc → /etc/bashrc → /etc/profile.d/*

5.3 为什么每个用户必须有自己的家目录

配置文件放在用户家目录下,如 /home/mstcheng/.bash_profile。每个用户登录,读的是自己的配置文件。所以:不同用户可以有不同环境变量;你改了 mstcheng 的配置,root 登录不会受影响。

5.4 "载入进程上下文"是什么意思

配置文件里不是简单的 PATH=... 一行,而是脚本。脚本里可能有 for 循环、if 判断、export、umask 等。bash 执行脚本,把字符串读进来,用 malloc 分配空间、用指针拼成数组,最终形成进程内存中的环境变量表。这个过程就叫"载入进程上下文"。

5.5 验证实验:配置文件真的被读了吗

在 mstcheng 用户的 ~/.bashrc 末尾加:

echo "果然登录时 .bashrc 被执行了"
env

然后:关闭 Xshell → 重新以 mstcheng 登录 → 屏幕上先打印欢迎语,再打印一大堆环境变量 → 说明登录时确实执行了 .bashrc。如果你是本地的虚拟机,那么另起一个窗口就能看到:

在这里插入图片描述

再用 root 登录:什么额外输出都没有。因为改的是 mstcheng 的文件,root 读的是 root 自己的配置。

结论:每个用户登录都会启动自己的 bash,并读取自己家目录下的配置文件。

5.6 用 ps 观察 bash 进程

mstcheng@ubuntu-dev:~$ ps axj | grep bash

每多开一个 SSH 会话,就多一个 bash 进程;每退出一个会话,就少一个。全部退出后,系统里就没有你的 bash 了。这个实验在上面演示过!

这再次证明:环境变量是 bash 进程内存里的东西,bash 没了,内存里的表也就没了。

5.7 完整因果链

在这里插入图片描述

  • 登录 shell 会读哪些文件?——/etc/profile、/.bash_profile、/.bashrc 等(依发行版和 shell 而定)。
  • .bash_profile 和 .bashrc 的区别?——前者用于登录 shell,后者用于交互式非登录 shell;很多发行版在 .bash_profile 里 source .bashrc。
  • source 和直接执行脚本的区别?——source 在当前 shell 执行,变量留在当前 shell;直接执行是 fork 子 shell,变量不影响父 shell。

注意事项:

  • 改了 ~/.bashrc 却不生效:可能你用的是登录 shell,只读 .bash_profile;或没重新登录 / 没 source。
  • 在 root 下改配置,普通用户不生效。
  • 不同 shell(bash/zsh/fish)配置文件不同,zsh 读 .zshrc。
  • 图形界面终端不一定是"登录 shell",读取的文件可能不同。

六、本地变量与环境变量

6.1 自己造一个变量

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ test_env=123456789
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ echo $test_env
123456789

# 但是——
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep test_env
# 查不到!

# 写个 C 程序去 getenv("test_env"),也拿不到

结论:test_env 只是一个本地变量(shell 变量)。

6.2 本地变量 vs 环境变量

对比项本地变量环境变量
定义方式name=valueexport name=value 或先定义再 export
当前 shell 可见是是
env 能查到否是
子进程能继承否是
作用范围仅当前 shell当前 shell 及所有后代进程
类比 C/C++局部变量全局变量

6.3 export 的本质

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export test_env

把已经存在的本地变量写进当前 bash 的环境变量表。之后 env 能看到,子进程也能继承。注意:export 只影响内存,不修改配置文件。也可以一步到位:export AAA=12345(定义 + 导出同时完成)。

6.4 子进程继承验证

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export TEST_ENV=ABCDEFG
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ./myenv     # 程序里 getenv("TEST_ENV")
ABCDEFG

如果变量不存在:char *p = getenv("NOT_EXIST"); // p == NULL

注意:

  • 赋值时等号两边不能有空格:A = 1 会被当成命令 A 加参数。
  • $ 只在取值时用:定义时写 A=1,不是 A=$1。
  • 单引号内的变量不会展开:echo '$HOME' 输出 $HOME。
  • export 后仍然不持久化,重登就没了。

6.5 unset:删除变量

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ unset TEST_ENV
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ unset AAA

从当前 bash 的环境变量表中删除,同样是内存级操作,不影响配置文件。删除后 env 查不到,子进程也拿不到。

6.6 set:查看所有变量

mstcheng@ubuntu-dev:~/117/code/linux-code/process$ set

显示环境变量 + 本地变量 + shell 函数等;env 只显示环境变量。所以:想确认某个变量是不是环境变量 → env | grep 名字;想看全部(含本地)→ set。

补充(内建命令辨析,接上 3.1 的伏笔):

  • env 是外部命令(/usr/bin/env),它打印的正是"从 bash 继承来的那张表"——又一次验证继承;
  • export / unset / set / cd / pwd 是 bash 内建命令(builtin)——在 bash 进程内部直接执行,不 fork 新进程,直接改 bash 自己的内存表,所以 export 立即生效;
  • 这就是为什么"改了配置要 source 才生效":source 也是让 bash 在自己进程里执行脚本。

6.7 本地变量的用途

  • 支撑 shell 自身语法:循环变量、条件判断、临时计算:
for i in 1 2 3; do
    echo $i
done
  • 临时保存信息:比如记住一个时间、一个路径。
  • 控制 shell 行为:PS1、PS2、HISTSIZE 等。

6.8 PS1 与 PS2

  • PS1:主命令提示符格式。常见组成:\u(用户名)、\h(主机名)、\w(当前路径)、\$(提示符 $ 或 #)。例:PS1='[\u@\h \w]\$ ' → [mstcheng@ubuntu-dev ~]$。
  • PS2:续行提示符,默认 >。命令没输完(比如引号没闭合、反斜杠续行)时出现。

七、持久化:写进配置文件(呼应 3.3 的"复原"问题)

3.3 问"为什么重登复原"——答案:只改了内存。想不复原,就把 export 写进配置文件。

7.1 写入 ~/.bash_profile

mstcheng@ubuntu-dev:~$ vim ~/.bash_profile
# 加入一行:
export haha="woqu"

保存退出,重新登录:

mstcheng@ubuntu-dev:~$ echo $haha
woqu

原因:每次登录 bash 都会重新读取 ~/.bash_profile 并执行 export。

7.2 系统级持久化

想让所有用户生效:改 /etc/profile、/etc/bashrc 或 /etc/profile.d/*.sh。需要 root 权限。改错可能影响所有用户登录,务必先备份。

7.3 安全提醒

  • 不要在配置文件里明文写密码、Token;
  • 配置文件权限建议 600,避免其他用户读取;
  • 不要随意 source 来路不明的脚本。

注意:

  • 写完配置没重新登录,以为没生效——可以 source ~/.bash_profile 立即生效;
  • 写到了错误的文件(如改了 .bashrc 但登录 shell 只读 .bash_profile);
  • 引号使用错误:export A="hello world" 需要引号,否则空格会

八、总结

一句话:命令行参数是 bash 传给子进程的“指令”,环境变量是 bash 传给子进程的“档案”,都靠 fork 继承、都存在内存里;想持久化,就写进配置文件。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2403_86779341/article/details/168340401

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--