一、命令行参数:main 函数的三个参数
我们平时写的 int main() 只是简写,完整形态是:
c
int main(int argc, char *argv[], char *env[])
argc:参数个数,包含程序名本身,至少为 1,用于实现各种选项功能argv:字符指针数组,argv[0]是程序名,argv[argc]固定为NULL,命令行参数表env:环境变量表,第三部分再展开
例子:
cpp
#include<stdio.h>
int main(int argc, char *argv[], char *env[])
{
for(int i=0; i<argc; ++i)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
执行 ./testenv -a -b -c,则 argc 为 5,argv[0] 是 "./testenv",argv[1] 到 argv[3] 依次是 -a、-b、-c。数组以 NULL 收尾

1.1 命令行参数的意义
一个程序编译一次,靠不同参数跑出不同行为,这就是参数存在的理由。ls -l、cp -r 都是同一个可执行文件在不同 argv 下的不同分支。写带选项的小工具,第一步就是解析 argv。
二、环境变量:进程的隐形配置
环境变量是操作系统中用来指定运行环境的一些参数,通常具有全局特性。
典型证据:
- 输入
ls这样的命令时无需写完整路径->有环境变量PATH补全 - 编译链接时我们并不知道动态库、静态库在哪,但链接照样成功,原因就是有环境变量帮助编译器定位
2.1 三个常见的环境变量
PATH:命令的搜索路径;HOME:用户主工作目录,登录后的默认目录;SHELL:当前 Shell,通常是/bin/bash
查看:

环境变量的创建流程 :用户登录->bash进程创建启动->读取bash配置文件->构建环境变量
bash配置文件:

2.2 常用命令
echo $NAME:查看某个环境变量export 变量名="值":新增环境变量;env:显示所有环境变量;set:显示本地Shell 变量与环境变量unset MYENV:清除环境变量
使用示例:

这里有个坑:只写变量="值"不加 export,它只是一个 Shell 本地变量,子进程根本读不到;只有 export 之后才会进入环境表被继承
2.3 PATH 测试:为什么 ls 不用带路径
自己编译的 testenv 必须写 ./testenv 才能跑,系统命令敲名字就行,差别就在 PATH。
把程序目录(绝对路径!不要出现.)用export追加进去,再直接敲 testenv 就能执行了:

结论:PATH 本质是一串用冒号分隔的目录列表,Shell 按顺序去找同名可执行文件
2.4 其他测试
HOME

USER

其他:

2.5 代码里获取环境变量
c
// 1. main 第三个参数
int main(int argc, char *argv[], char *env[])
// 2. libc 提供的全局变量(要用 extern 声明,无对应头文件)
extern char **environ; //char**,指向char* argv
// 3. 系统调用级封装函数
char *getenv(const char *name);
getenv("PATH") 返回字符串指针,取不到则返回 NULL。环境表本身是一个字符指针数组,每个指针指向一个以 \0 结尾的 "KEY=value" 字符串,数组同样以 NULL 结尾------和 argv 的结构完全一致。
2.6 全局属性:会被子进程继承
直接运行 getenv("MYENV") 没结果,export MYENV="hello world" 后再运行就有了:环境变量会由父进程传给子进程。想在登录时自动生效,写进 ~/.bash_profile 或 ~/.bashrc 即可。
例子:

三、程序地址空间:你看到的地址都是虚拟的
3.1 先验证地址分布
打印各类变量地址(kernel 2.6.32,32 位平台):
code addr: 0x40055d
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x1791010 ← 依次递增
heap addr: 0x1791030
test static addr: 0x601038
stack addr: 0x7ffd0f9a4368
stack addr: 0x7ffd0f9a4360 ← 依次递减
read only string addr: 0x400800
argv[0]: 0x7ffd0f9a4811
env[0]: 0x7ffd0f9a4819
两条规律一眼可辨:栈地址从高到低、堆地址从低到高,也就是栈向下增长、堆向上增长;argv 与 env 的地址比栈变量还高,说明它们位于用户栈更上方。
3.2 fork 之后...?
把 g_val 初始化为 0,父进程睡 3 秒,子进程先改再打印:
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
地址完全相同,内容却不同。可以推出两条结论:
- 这两处变量绝对不是同一个;
- 这个地址也绝对不是物理地址。
它就是虚拟地址。C/C++ 里看到的所有地址都是虚拟地址,物理地址用户一概看不到,由操作系统统一管理并完成转换。
例子:通过修改全局变量g_val看父子进程差异:

修改前后图示:

3.3 谁在描述地址空间
描述进程地址空间全部信息的结构体是 mm_struct(内存描述符),每个进程各有一份,task_struct 里通过 struct mm_struct *mm 指向它(内核线程的 mm 为 NULL,借用 active_mm)。
c
struct mm_struct {
struct vm_area_struct *mmap; // 虚拟区间链表
struct rb_root mm_rb; // 红黑树
unsigned long task_size;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
};
一个独立的虚拟内存区域用 vm_area_struct(VMA)表示:区域少时用 mmap 串成单链表,区域多时用 mm_rb 组织成红黑树,兼顾遍历方便与查找快速。那组 start_code/end_code、brk、start_stack、arg_start/env_start 就是各段虚拟地址的边界,正好对上了 3.1 的打印结果。
具体过程/结构:

补充知识 :

3.4 为什么必须有虚拟地址空间
换个问法:如果程序直接操作物理内存会怎样?,三个问题很直接:
- 任何进程都能读写系统内存区域
- 程序每次被搬到内存的位置都不一样,地址无法确定
- 内存不够时要把整个进程拷到交换分区,效率低
有了地址空间与页表映射,就解决了:
保护物理内存- 进程管理与内存管理
解耦合,物理内存可任意位置加载数据 - 页表:实现地址无序(物理)->
有序(虚拟)
四、总结
- 命令行参数:
argc含程序名,argv以NULL结尾,一个程序靠它跑出多种行为; - 环境变量:
PATH/HOME/SHELL是常客,只有export过的才会被exec传给子进程,代码里用env[]、environ或getenv读取; - 程序地址空间:栈向下、堆向上,C/C++ 里的地址全是虚拟地址,
mm_struct+vm_area_struct描述布局,页表完成映射,换来安全、确定与解耦
感谢阅读,我们下篇见。