最近在学习Linux系统编程中环境变量与进程地址空间相关内容,把课堂笔记整理成这篇博客,方便自己复盘也分享给大家~
一、环境变量:操作系统的"全局配置参数"
环境变量是操作系统中用来指定运行环境的参数,就像程序运行时的"全局配置文件",决定了程序能在哪里找指令、用户的工作路径等核心信息。
- 命令行参数:程序的"动态开关"
int main(int argc, char* argv[]) 是我们接触C语言时就熟悉的入口,其中:
-
argc 是命令行参数的个数, argv 是存储参数的指针数组。
-
它的核心作用是让同一个程序通过不同参数实现不同功能,比如 ls -a 和 ls -l ,就是通过不同的命令行参数改变程序行为,底层会被bash解析成命令行参数表。
- 环境变量的核心作用
环境变量最典型的例子就是 PATH ,它是系统搜索指令的默认路径集合。当我们在终端输入 ls 时,系统会遍历 PATH 中的路径,找到对应的可执行文件再执行。
除此之外,常见的环境变量还有:
-
HOME :当前用户的主目录路径
-
SHELL :当前使用的Shell程序路径
-
USER :当前登录的用户名
- 环境变量的特性与操作
-
全局特性:环境变量是bash的全局变量,会被子进程继承。比如系统中如果有6个用户登陆,就会存在6个bash进程,每个bash的环境变量都会被子进程继承。
-
本地变量与全局变量:用 export 导出的环境变量是全局的,会被子进程继承;未导出的本地变量仅在当前bash内部有效,子进程无法访问。
-
常用操作命令:- echo 变量名 :查看指定环境变量的值,比如 echo HOME
-
env :查看所有环境变量
-
export 变量名=值 :设置全局环境变量
-
unset 变量名 :取消环境变量
-
代码中获取环境变量的两种方式
-
通过 main 函数的第三个参数 char* env[] :
这是一个存储所有环境变量的指针数组,以 NULL 结尾。不过这种方式不推荐,容易和其他参数混淆。c
#include <stdio.h>
int main(int argc, char* argv[], char* env[]) {
(void)argc; (void)argv;
for(int i=0; env[i]; i++) {
printf("env[%d]: %s\n", i, env[i]);
}
return 0;
}
- 通过 getenv 函数(推荐):
可以直接指定要获取的环境变量,更安全也更灵活,适合只需要特定环境变量的场景。c
#include <stdio.h>
#include <stdlib.h>
int main() {
char* path = getenv("PATH");
if(path == NULL) return 1;
printf("PATH: %s\n", path);
return 0;
}
- 通过全局变量 environ :
系统提供的全局变量 environ 指向环境变量表,使用时需要声明 extern char** environ; 。c
#include <stdio.h>
int main() {
extern char** environ;
for(int i=0; environ[i]; i++) {
printf("env[%d]: %s\n", i, environ[i]);
}
return 0;
}
二、进程地址空间:每个进程的"虚拟独立世界"
进程地址空间是操作系统给每个进程提供的虚拟地址空间,它不是真实的物理内存,而是一个逻辑上的地址范围,通过页表映射到真实的物理内存。
- 什么是进程地址空间?
对一个进程来说,它看到的内存是一个从低地址到高地址连续的虚拟地址空间,而真实的物理内存可能是离散的,通过页表完成虚拟地址到物理地址的映射。
-
32位系统下,进程地址空间的大小是4GB(2的32次方字节),其中低3GB是用户空间,高1GB是内核空间。
-
每个进程都认为自己独占了整个内存,这就是进程的"虚拟内存"机制。
- 进程地址空间的区域划分
进程地址空间从低地址到高地址,主要分为这些区域:
区域 作用
代码段(.text) 存储程序的机器指令,只读
数据段(.data) 存储已初始化的全局变量和静态变量
BSS段(.bss) 存储未初始化的全局变量和静态变量,程序运行时初始化为0
堆(Heap) 动态内存分配的区域,从低地址向高地址增长,比如 malloc 分配的内存就在这里
栈(Stack) 存储局部变量、函数参数、返回地址等,从高地址向低地址增长
共享库区域 存储动态链接库的代码和数据
内核空间 操作系统内核的地址空间,用户进程无法直接访问
- 虚拟地址 vs 物理地址
C/C++中使用的所有指针地址,都是虚拟地址,不是真实的物理内存地址。操作系统通过页表,将虚拟地址转换为物理地址,这样做有几个好处:
-
内存隔离:每个进程的虚拟地址空间相互独立,一个进程的错误不会影响其他进程。
-
内存保护:操作系统可以通过页表设置权限,比如代码段只读,防止进程修改指令。
-
内存管理:可以将不常用的内存数据交换到磁盘(swap分区),缓解物理内存不足的问题。
-
地址无关代码:程序不需要知道自己会被加载到哪个物理地址,方便操作系统动态加载和重定位。
-
进程地址空间的核心特性
-
进程独立性:每个进程都有自己独立的地址空间,修改一个进程的变量不会影响其他进程,即使变量的虚拟地址相同,映射的物理地址也不同。
-
写时拷贝(Copy-On-Write):当子进程被创建时,不会立即复制父进程的整个地址空间,而是和父进程共享同一份物理内存,只有当进程需要修改数据时,才会复制一份副本,这就是写时拷贝,也是子进程具有独立性的底层支撑。
三、学习中的关键思考与疑问
-
环境变量的来源:环境变量最开始从哪里来?答案是用户的相关配置文件,比如 ~/.bashrc 、 /etc/profile 等,用户登陆时bash会读取这些文件初始化环境变量。
-
本地变量与全局变量的区别:本地变量仅在当前bash进程中有效,不会被子进程继承;而用 export 导出的全局环境变量会被子进程继承,这也是为什么我们在子shell中设置的变量,父shell看不到的原因。
-
页表的作用:页表不仅完成虚拟地址到物理地址的映射,还能通过权限位控制内存的读写执行权限,比如代码段的只读保护,防止恶意修改指令。
-
地址空间的实现:进程地址空间本质上是操作系统通过 mm_struct 结构体管理的,里面包含了各个区域的起始地址、结束地址和权限信息,内核通过这个结构体来管理进程的内存。
四、学习总结
这次学习的环境变量和进程地址空间,是Linux系统编程的核心基础:
-
环境变量是进程运行的"配置中心",理解它的继承机制和操作方式,能帮我们更好地理解进程间的环境传递。
-
进程地址空间是操作系统给进程的"虚拟隔离舱",虚拟地址、页表映射、区域划分这些概念,是理解进程管理、内存管理的关键。
后续还要深入学习页表的具体实现、进程创建时的地址空间复制,以及动态链接库在地址空间中的加载机制,把这些知识点串起来~