ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解

Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解 上一篇我们讲完了 Linux O (1) 调度算法搞懂了进程如何被调度上 CPU 运行。但一个进程启动之后除了自己的代码、数据操作系统还会给它附带两份 “随身资料”命令行参数和环境变量。很多人天天敲命令gcc、g、sudo、su却很少思考为什么 gcc 可以自动找到系统的库文件为什么 shell 可以在 PATH 里面搜索命令为什么子进程可以拿到父进程的环境信息本篇我们从 main 函数讲起逐层拆解命令行参数、环境变量底层结构、4 种代码获取方式、继承原理同时讲环境变量的安全边界。前言命令行参数1.1 重新认识 main 函数很多 C/C 初学者以为main()就是程序的真正入口其实不是。1.1.1 main 函数不是程序的开始真正的程序入口是_start函数这个函数属于 C 运行时库crt0。当操作系统把程序加载进内存CPU 跳转到_start执行做程序运行前的初始化工作栈、标准 IO、全局变量初始化解析命令行参数、解析环境变量表调用我们写的main()函数main 返回后_start调用exit()完成进程退出回收main 只是我们写业务逻辑的入口不是整个程序的底层入口。1.1.2 main 函数也有参数我们平时写的 main 有两种写法// 写法1不接收参数intmain()// 写法2接收命令行参数argc argvintmain(intargc,char*argv[])argcargument count命令行参数的个数argvargument vector字符串指针数组保存每一个命令行字符串数组末尾以NULL标记结束。示例代码// arg_demo.c#includestdio.hintmain(intargc,char*argv[]){printf(argc %d\n,argc);for(inti0;iargc;i){printf(argv[%d] %s\n,i,argv[i]);}return0;}编译运行gcc arg_demo.c-oarg_demo ./arg_demo hello linux123输出argc4argv[0]./arg_demo argv[1]hello argv[2]linux argv[3]123注意argv[0]永远是程序自身的名字。1.2 命令行参数的切分与表结构shell 在敲回车执行命令的时候会做一件事把你输入的整行字符串按空格切分成一个个字符串组装成argv数组。内存布局argv[0]→./arg_demoargv[1]→helloargv[2]→linuxargv[3]→123argv[4]→ NULL这个数组存放在进程用户地址空间的栈区高地址和环境变量挨在一起。1.3 命令行参数的应用演示命令行参数最典型的用途程序不用重新编译启动的时候传参数改变行为。比如ls -lgcc -o test test.c-l、-o test test.c全部都是命令行参数。一、引入环境变量1.1 基本概念1.1.1 环境变量的概念环境变量就是操作系统提供给进程的一组特殊全局变量用来记录系统和用户的运行信息。就像人出门会随身带证件、通讯录进程启动的时候操作系统也会给它附带一整套环境变量。里面记录的信息包括当前是谁登录用户USER主机名字HOSTNAME命令去哪里找PATH动态库去哪里找LD_LIBRARY_PATH历史命令最多存多少条HISTSIZE重点环境变量最大的特性子进程会继承父进程的环境变量。这就解释了为什么gcc、g、sudo、su这些工具都可以读取环境变量。gcc/g 继承 shell 的环境变量读取LD_LIBRARY_PATH去寻找动态库读取PATH寻找编译工具链。su/sudo 继承 shell 的环境变量可以读取系统信息完成切换用户、查找命令等操作。1.1.2 常见环境变量表格变量名作用PATH命令搜索路径shell 在这里找可执行程序HOME当前用户家目录USER当前会话用户名LOGNAME登录用户名HOSTNAME主机名LD_LIBRARY_PATH动态链接库搜索路径HISTSIZEshell 历史命令保存条数1.1.3 怎么看环境变量# 打印全部环境变量envprintenv# 打印单个环境变量echo$PATHecho$HOME1.1.4 环境变量和本地变量的设置和导入shell 里面有两种变量shell 本地变量只在当前 shell 生效子进程看不到。环境变量通过export导出子进程可以继承。1.1.4.1 核心概念对比# 定义shell本地变量子进程不可见MY_VARhello world# 导出为环境变量子进程可以继承exportMY_VAR1.1.4.2 变量的操作指令export变量名值# 设置环境变量unset变量名# 删除变量⚠️注意直接写MY_VARxxx只是 shell 局部变量不加 export子进程拿不到这个变量。1.2 main 函数的第三个参数很多人不知道 main 函数还有第三个隐藏参数用来接收环境变量表intmain(intargc,char*argv[],char*envp[])envp是字符串指针数组每一个元素格式为KEYVALUE数组末尾以NULL结束。示例代码// env_main.c#includestdio.hintmain(intargc,char*argv[],char*envp[]){for(inti0;envp[i]!NULL;i){printf(%s\n,envp[i]);}return0;}编译运行就会打印出全部环境变量。注意这个第三个参数是 GCC 的扩展标准 C 不推荐直接使用标准推荐使用getenv()或者全局environ指针。1.3 为什么执行自己的程序必须加././myprogram为什么不能直接写myprogramshell 执行命令的时候会去PATH环境变量记录的目录里面挨个找可执行文件。当前目录.默认不在 PATH 里面所以 shell 找不到你的程序必须用./显式告诉 shell在当前目录找这个程序。如果你把.加入 PATH就可以直接写程序名运行但这存在安全风险不建议生产环境这么做。二、环境变量的底层表结构与溯源2.1 环境变量表的组织形式环境变量表本质就是char *envp[]指针数组envp[0]→PATH/usr/bin:/binenvp[1]→HOME/home/ubuntuenvp[2]→USERubuntuenvp[3]→ NULL每一条字符串格式固定为键值数组末尾用NULL标记结束。这块内存位于进程地址空间的用户栈高地址和命令行参数挨在一起。2.2 环境变量的内存级和磁盘级修改内存级修改临时shell 执行export只对当前 shell 会话生效关闭终端就消失。磁盘级修改永久修改配置文件登录 shell 的时候自动加载环境变量。用户级别~/.bashrc、~/.profile只对当前用户生效系统级别/etc/profile对所有用户生效修改完磁盘配置文件需要执行source ~/.bashrc让配置立刻生效。2.3 怎么在 windows 下修改环境变量Windows 环境变量逻辑和 Linux 类似此电脑 → 右键属性 → 高级系统设置 → 环境变量分为用户环境变量当前用户、系统环境变量全部用户修改后新开 cmd 窗口生效旧窗口不会自动更新。三、获取环境变量的三种核心代码手段注意main 第三个参数、getenv、environ、子进程继承一共 4 种方式。3.1 方法一通过 main 函数的第三个参数隐式获取就是上面写的char *envp[]直接遍历数组读取环境变量。缺点标准 C 不支持属于编译器扩展可移植性差。3.2 方法二通过系统调用 APIgetenv获取✅最推荐C 标准库提供getenv()输入环境变量 key返回对应的 value 字符串。// getenv_demo.c#includestdio.h#includestdlib.hintmain(){char*pathgetenv(PATH);if(path!NULL){printf(PATH %s\n,path);}char*usergetenv(USER);printf(USER %s\n,user);return0;}编译运行gcc getenv_demo.c-ogetenv_demo ./getenv_demo优点标准 C 库函数跨平台代码简洁。3.3 方法三通过全局第三方指针environ显式获取内核给我们提供一个全局的二级指针environ不需要 main 第三个参数直接使用。需要手动声明extern char **environ;// environ_demo.c#includestdio.hexternchar**environ;intmain(){for(inti0;environ[i]!NULL;i){printf(%s\n,environ[i]);}return0;}environ全局指针直接指向进程的环境变量表的起始地址。3.4 方法四子进程继承父进程获取环境变量3.4.1 核心原理当我们调用fork()创建子进程的时候子进程会拷贝父进程的完整地址空间环境变量表也完整复制一份。所以子进程天然拥有父进程全部环境变量。后续调用exec加载新程序默认保留环境变量这就是为什么gcc、sudo这些子进程可以拿到 shell 的环境变量。exec 也可以使用execle()传入自定义的环境变量表不继承父进程。3.4.2 示例代码// fork_env.c#includestdio.h#includeunistd.h#includestdlib.hintmain(){pid_tpidfork();if(pid0){// 子进程读取PATHprintf([子进程] PATH %s\n,getenv(PATH));}else{printf([父进程] PATH %s\n,getenv(PATH));}return0;}运行可以看到父子进程打印出一模一样的 PATH证明继承生效。四、其他环境变量4.1 所有环境变量罗列执行env命令就可以列出当前进程全部环境变量。4.1.1 USER 和 LOGNAME 的区别LOGNAME记录登录终端的原始用户。USER记录当前会话的用户。举例子用 ubuntu 账号登录终端然后执行sudo su root切换 root。此时LOGNAME仍然是 ubuntu而USER变成 root。4.1.2 历史命令与 HISTSIZEHISTSIZE这个环境变量控制 shell 历史命令最多保存多少条。echo$HISTSIZE修改这个值就可以调整 shell 历史命令的存储上限。4.2 环境绑定与权限控制用法环境变量可以用来给程序传递配置不需要修改源代码只需要设置环境变量就改变程序行为。但是也存在安全风险。4.2.1 核心逻辑与安全防范⚠️安全坑点如果程序是 root 权限运行环境变量来自普通用户恶意用户可以篡改环境变量实现攻击比如 PATH 劫持。所以高权限程序很多会主动清空或者过滤环境变量。4.2.1.1 示例代码// secure_env.c#includestdio.h#includestdlib.hintmain(){// 高权限程序不要无条件信任环境变量char*debuggetenv(DEBUG);if(debug!NULL){printf(DEBUG模式开启\n);}return0;}4.2.3 基于自定义环境变量的控制逻辑业务程序可以自定义环境变量作为配置开关。4.2.3.1 示例代码// custom_env.c#includestdio.h#includestdlib.hintmain(){char*modegetenv(RUN_MODE);if(modeNULL){printf(默认生产模式\n);}elseif(strcmp(mode,debug)0){printf(调试模式\n);}return0;}运行测试# 生产模式./custom_env# 调试模式exportRUN_MODEdebug ./custom_env不用改代码只设置环境变量就切换程序运行模式。全文总结命令行参数shell 把命令切割成字符串数组传递给 main 函数的argc/argv用来给程序传递启动参数。环境变量操作系统给进程的一组特殊全局变量保存系统、用户信息格式为KEYVALUE。shell 本地变量必须用export导出才会变成环境变量被子进程继承。4 种获取环境变量的方式main 第三个参数 envp可移植差getenv()标准库函数推荐全局environ指针fork 子进程继承父进程环境变量继承原理fork拷贝地址空间环境变量表复制exec默认保留环境变量。为什么 gcc/g、sudo 可以读取环境变量它们都是 shell 的子进程继承 shell 的环境变量。环境变量可以做配置控制但高权限程序要警惕环境变量带来的安全风险。
返回列表