欢迎光临
我们一直在努力

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

在这里插入图片描述

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

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

《Mysql数据库》
《白话大模型》

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


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

文章目录

  • 前置知识:从开机到 Bash,进程如何一路继承
    • Linux 开机后的第一站:systemd
    • 从图形界面到终端:GDM → gnome-shell → bash
    • 子进程继承自父进程:命令行参数与环境变量的由来
  • 一、命令行参数
    • 1.1 问题一:main 函数可以有参数吗?
    • 1.2 实验:打印 argc 与 argv
    • 1.3 命令行参数的本质:shell 切割字符串
    • 1.4 为什么要有命令行参数?(本质应用)
    • 1.5 用命令行参数理解 `cat` 命令
  • 二、进一步理解命令行参数,引出环境变量
    • 2.1 我们自己实现一个命令
    • 2.2 为什么必须写 `./mycmd`
    • 2.3 PATH 的本质
    • 2.4 如何把当前目录加入 PATH?
    • 2.5 内建命令为什么不受影响
  • 三、深入认识环境变量
    • 3.1 `env`(environment) 命令
    • 3.2 常见环境变量
    • 3.3 灵魂拷问:bash 的环境变量,到底是从哪来的?
    • 3.4 cd – 与 OLDPWD(上一次所在目录)
    • 3.5 专题:cd ~ 与 HOME
    • 3.6 SSH_TTY 与"一切皆文件"
    • 3.7 环境变量像什么?
    • 3.8 C 程序获取环境变量的三种方式
    • 3.9 安全案例:用 getenv 做"用户白名单"
  • 四、核心:环境变量到底存在哪里
    • 4.1 两张表:argv 表与环境变量表
    • 4.2 是"内存级"的,不是磁盘上的
    • 4.3 bash 也是进程
    • 4.4 为什么子进程不传参也能拿到环境变量
    • 4.5 进程树:bash 是"祖宗"
    • 4.6 只读共享与写时拷贝
    • 4.7 结论:环境变量是父进程给的
  • 五、bash 的环境变量从哪来:配置文件
    • 5.1 登录时发生了什么(完整流程)
      • 5.2 配置文件的调用链
    • 5.3 为什么每个用户必须有自己的家目录
    • 5.4 "载入进程上下文"是什么意思
    • 5.5 验证实验:配置文件真的被读了吗
    • 5.6 用 ps 观察 bash 进程
    • 5.7 完整因果链
  • 六、本地变量与环境变量
    • 6.1 自己造一个变量
    • 6.2 本地变量 vs 环境变量
    • 6.3 export 的本质
    • 6.4 子进程继承验证
    • 6.5 unset:删除变量
    • 6.6 set:查看所有变量
    • 6.7 本地变量的用途
    • 6.8 PS1 与 PS2
  • 七、持久化:写进配置文件(呼应 3.3 的"复原"问题)
    • 7.1 写入 ~/.bash_profile
    • 7.2 系统级持久化
    • 7.3 安全提醒
    • 八、总结

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

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

Linux 开机后的第一站:systemd

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

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

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

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

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

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

  • 命令行参数:我们在 bash 中执行一个程序,本质上是 bash 作为父进程,创建出一个子进程去运行我们的代码。这个子进程的 main 函数所接收到的命令行参数,正是由父进程 bash 传给它的。
  • 环境变量:bash 自身拥有一套环境变量,当它创建子进程时,子进程同样会继承这套环境变量。
  • 所以,接下来要学习的命令行参数与环境变量,讨论的核心就是:子进程 main 的参数继承自 bash,环境变量也同样继承自 bash。理解了这条继承链,再看 main 函数的参数 argc、argv 以及环境变量的具体规则,就会顺畅很多。

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

    一、命令行参数

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

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

    示例:

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

    参数含义:

    参数全称含义
    argc argument count 命令行参数 个数
    argv argument 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
    ./commendTest 1 [0]=./code
    ./commendTest a 2 [0]=./code, [1]=a
    ./commendTest a b c d 5 [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
    # 空!啥都没有了,复原了

    答案分两层:

  • 环境变量的来源:来自 Linux 的配置文件——在我们启动 Linux / 登录的时候,系统自动帮我们配置;
  • 声明周期(寿命):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 登录时发生了什么(完整流程)

  • 你通过 SSH/Xshell 登录;
  • 系统校验身份,启动一个 bash 进程;
  • bash 启动时,读取配置文件;
  • 配置文件是 shell 脚本,里面会设置 PATH、HOME、umask 等;
  • bash 执行这些脚本,把变量载入自己的进程上下文(构建环境变量表);
  • 之后你执行的命令,都是 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=value export 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 继承、都存在内存里;想持久化,就写进配置文件。

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux进程篇】Linux学习第十七弹——Linux 命令行参数与环境变量详解:从 Bash 到 exec,搞懂进程如何传参与继承
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址