《Linux系统编程》10.Linux进程-命令行参数,环境变量与程序地址空间
本文介绍了Linux系统中的命令行参数和环境变量机制。命令行参数通过main()函数的argv和argc传递,用于指定操作对象、控制命令行为、传递配置信息等。环境变量则通过PATH等变量存储系统配置,可通过main()的env参数、environ变量或getenv()函数获取。文章还深入分析了进程的虚拟地址空间原理,解释了父子进程变量地址相同但值不同的现象,这是由于写时拷贝机制导致虚拟地址映射到不

💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》
🌸Yupureki🌸的简介:

目录
1. 命令行参数
Linux 中的命令行参数(Command-Line Arguments)是用户在终端执行命令时,附加在命令名称之后的额外信息。它们的作用非常广泛
1.1 指定操作对象
命令行参数最常见的用途是指定命令要处理的文件、目录或其他资源。
例如:
-
ls /home:参数/home告诉ls命令列出该目录的内容。 -
rm file.txt:参数file.txt指定要删除的文件。
1.2 控制命令的行为(选项)
通过选项(通常以 - 或 -- 开头)可以修改命令的默认行为,实现更精细的控制。
例如:
-
ls -l:-l参数让ls以长格式显示文件详细信息。 -
grep -i "hello" file.txt:-i参数使搜索不区分大小写。
1.3 传递配置信息
许多命令支持通过参数传递配置值,例如指定端口号、超时时间、颜色主题等。
例如:
-
ping -c 4 google.com:-c 4指定只发送 4 个数据包。 -
ssh -p 2222 user@host:-p 2222指定连接端口为 2222。
1.4 提供输入数据
参数可以直接作为输入数据传递给命令,特别是脚本或处理文本的工具。
例如:
-
echo "Hello World":参数"Hello World"被echo直接输出。 -
./script.sh arg1 arg2:Shell 脚本中可以通过$1、$2等变量获取传入的参数值。
1.5 main函数参数
你是否想过,命令本身也是一个可执行程序的文件,我们既然能在这些命令后面加参数,那假设我们有自己编译的C/C++的程序,在后面带参数有什么用?这就是main函数的参数
int main(int argv,char* argc[])
main函数的参数默认第一个为命令行参数的个数,而后面的argc字符串数组则是根据空格分割的几个字符串
#include <stdio.h>
int main(int argv,char* argc[])
{
printf("argv:%d\n",argv);
for(int i = 0;i<argv;i++)
{
printf("argc[%d]:%s\n",i,argc[i]);
}
return 0;
}

结论:
无论是我们自己的可执行程序,还是使用的命令(也是可执行程序),在命令行输入的参数都被传入到了对应程序的main函数的参数中!
2. 环境变量
如何理解环境变量?一个例子:
我们在命令行中输入ls,系统凭什么知道我们要执行ls命令,知道ls命令的源可执行程序的位置并执行?->因为系统存在默认路径,会在这个默认路径上拼接ls,然后执行这个路径下的ls程序,如果不存在就会报错
这个默认路径是操作系统早就存在的,是环境变量
2.1 查看环境变量
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- unset:清除环境变量
- set:显示本地定义的shell变量和环境变量
env命令:

echo命令:
我们使用echo $PATH查看环境变量中PATH的默认值
![]()
这就是命令的默认搜索路径,我们输入ls命令会默认在这些目录中查找
![]()
2.2 理解环境变量
从存储的角度看,一个用户都有其对应的bash,每个bash都能生成一张环境变量表

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境
字符串
常见的环境变量
| 变量名 | 含义 | 示例值 |
|---|---|---|
PATH |
可执行程序的搜索路径 | /usr/bin:/bin:/usr/local/bin |
HOME |
当前用户的主目录 | /home/username |
USER |
当前用户名 | username |
SHELL |
当前使用的 Shell 程序路径 | /bin/bash |
LANG |
系统语言和字符编码设置 | zh_CN.UTF-8 |
PWD |
当前工作目录 | /home/username/projects |
2.3 程序获取环境变量
2.3.1 main函数的第三个参数
main函数的第三个参数为环境变量表,程序执行时系统会自动传入
int main(int argv,char* argc[],char* env[])

2.3.2 第三方变量environ
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
#include <stdio.h>
extern char **environ;
int main()
{
for(int i = 0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
}

2.3.3 getenv函数
getenv可以获取环境变量中的某一个变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
}
![]()
2.3.4 环境变量的特性
- 环境变量具有全局性,所有程序都可以访问
- bash记录本地变量和环境变量,本地变量只有自己的程序能访问

上面的i为本地变量,只有bash自己保存了,其余的进程都没有。并且本地变量无法被子进程继承
2.4 修改环境变量
2.4.1 临时修改环境变量
临时修改环境变量只有在当前shell有效,如果切换到其他账户或者退出服务器,那么就会重置
方法一:使用 export 命令(适用于 Bash、Zsh 等)
export 变量名=值

方法二:直接在命令前临时赋值(仅对该命令有效)
变量名=值 命令

2.4.2 永久修改环境变量
永久修改需要将设置命令写入 Shell 的配置文件中,这样每次启动 Shell 时会自动加载。不同 Shell 使用的配置文件不同,以最常用的 Bash 为例:
用户级别(仅当前用户)
-
~/.bashrc:每次打开新的终端(交互式非登录 Shell)时执行。
-
~/.bash_profile 或 ~/.profile:登录 Shell 时执行(例如通过 SSH 登录或终端模拟器启动登录 Shell)。
通常建议将环境变量设置放在 ~/.bashrc 中,并在 ~/.bash_profile 中显式引入 ~/.bashrc(许多发行版已自动配置好)。
vim ~/.bashrc

系统级别(所有用户)
-
/etc/environment:系统级别的环境变量配置文件,使用
变量=值格式(不需要 export)。 -
/etc/profile、/etc/bash.bashrc 或 /etc/profile.d/*.sh**:系统级别的初始化脚本,会影响所有用户
vim /etc/environment

3. 程序地址空间
在我们学习语言的时候,时常看见这张内存分布图

3.1 虚拟地址空间
我们都说子进程继承父进程的一切信息
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
else if (id == 0)
{ // child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ // parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

看起来也确实是这样的
现在我们让子进程修改变量的值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
else if (id == 0)
{ // child
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ // parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

我们发现地址一样的变量,子进程修改了变量的值,打印的也是修改后的值。而父进程打印的竟然是修改前的值!!!
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。但地址值是一样的,说明,该地址绝对不是物理地址!
在Linux地址下,这种地址叫做虚拟地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一
管理
OS必须负责将虚拟地址转化成物理地址
为什么需要虚拟地址空间?
如果没有虚拟化,程序直接操作物理内存,会面临三大问题:
物理内存限制:程序必须被加载到连续的物理地址,且总大小不能超过实际内存容量。
内存隔离与安全:任何程序都可以访问任意物理地址,恶意或出错的程序可能破坏其他进程甚至内核的数据。
多进程难以管理:同时运行多个程序时,分配和释放物理内存变得极其复杂,且容易产生碎片。
虚拟地址空间就是为了解决这些问题而设计的抽象层。
3.2 虚拟地址空间的工作原理
-
虚拟地址:每个进程看到的地址(例如 0x400000)都是虚拟的,进程以为自己在独享整个内存空间。
-
物理地址:内存芯片上的真实地址。
-
MMU(内存管理单元):CPU 中的硬件组件,负责将虚拟地址实时转换为物理地址。
-
页表:操作系统为每个进程维护一张映射表,记录了虚拟页到物理页框的对应关系。
每个进程都有一个页表,这个页表可以当作一个哈希表,保存着虚拟地址向着物理地址的映射表
当进程要访问虚拟地址时,系统会顺着页表找到其物理地址,然后在物理地址上进行访问
而一开始,子进程确实继承了父进程的页表,其映射关系是完全一样的

当子进程或者父进程要进行对变量的修改时,对应变量的虚拟地址不变,但是物理地址会发生改变,使得两个进程访问的变量实际上并不一样,这就是写时拷贝

更具体一点,每个task_struct都有一个mm_struct结构体,正是表示进程地址空间的结构体,这个mm_struct再根据页表查询物理地址

上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映
射到了不同的物理地址!
3.3 虚拟地址与进程地址空间
我们回顾定义
什么是虚拟地址?
-
定义:虚拟地址是CPU在执行指令时用来访问内存的地址。它不是直接对应物理内存中的位置,而是一个逻辑地址,需要通过内存管理单元(MMU)转换成物理地址后才能访问真正的内存。
-
特点:
-
每个进程都有自己的虚拟地址空间,进程看到的地址都是从0开始的连续虚拟地址。
-
虚拟地址的位数决定了进程能访问的最大地址范围(例如64位系统可达2^48)。
-
虚拟地址本身只是一个数字,它能否使用取决于它是否被映射到某个物理内存或文件。
-
什么是进程地址空间?
-
定义:进程地址空间是操作系统为每个进程抽象出的独立内存视图,它描述了进程可以使用的全部虚拟地址范围以及这些地址的访问权限(读、写、执行)和映射对象(物理内存、文件、设备等)。
-
组成:通常包括代码段、数据段、堆、内存映射区、栈等区域,每个区域称为一个虚拟内存区域(VMA),由内核的
mm_struct结构体统一管理。 -
特点:
-
独立性:每个进程拥有独立的地址空间,互不干扰。
-
稀疏性:地址空间中只有部分区域被实际映射(已分配物理页或文件映射),未映射的部分访问会触发段错误。
-
动态性:地址空间的布局和映射内容随程序的运行(如
malloc、mmap、execve)而动态变化。
-
简单来说,每一个进程都有其进程地址空间,即mm_struct,所有的进程都认为系统给自己的物理内存是全部的,自己在独吞内存!但实际上系统不可能全给,而是偷偷以一张页表,其中包含虚拟地址映射真实的物理内存地址的表,骗过了进程。让进程看见的是虚拟地址,以为是真实的物理地址,但实际上根本就不是,甚至骗了我们
3.4 虚拟内存管理
回顾内存分布图

mm_struct还管理着进程地址空间中每一个划分区域的大小
如何管理?
内存大小是固定的,我们只需要划分好每个区域的开始地址和结束地址即可


那既然每一个进程都会有自己独立的mm_struct,那么操作系统如何管理?->先描述,再组织
- 当虚拟区间较少时采取单链表,由mmap指针指向这个链表;
- 当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树。
3.5 vm_area_struct
实际上mm_struct结构体内还包含着vm_area_struct,vm_area_struct才是真正管理内存地址的
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚
拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进程快速访问。

3.6 为什么要有虚拟地址空间?
简单来说,引入虚拟地址空间(Virtual Address Space)是现代操作系统(包括Linux)能够成为稳定、安全、多任务系统的基石。
如果没有虚拟地址空间,所有的程序将直接操作物理内存。想象一下,如果每个程序都能直接读写内存中的任意位置,计算机世界将会一片混乱。
不使用的风险:
- 安全风险:每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够去读写系统相关内存区域,如果是一个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
- 地址不确定:众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每一次运行都是不确定的,比如:第一次执行a.out时候,内存当中一个进程都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经有10个进程在运行了,那执行a.out的时候,内存地址就不一定了
- 效率低下:如果直接使用物理内存的话,个进程就是作为一个整体(内存块)操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区中,好腾出内存,但是如果是物理地址的话,就需要将整个进程一起拷走,这样,在内存和磁盘之间拷贝时间太长,效率较低。
存在这么多问题,有了虚拟地址空间和分页机制就能解决了吗?当然!
- 地址空间和页表是OS创建并维护的!是不是也就意味着,凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问!!也顺便保护了物理内存中的所有的合法数据,包括各个进程以及内核的相关有效数据!
- 因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载!物理内存的分配和进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合。
- 因为有地址空间的存在,所以我们在C、C++语言上new,malloc空间的时候,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这是由操作系统自动完成,用户包括进程完全0感知!!
- 因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的。
更多推荐


所有评论(0)