[Linux系统] 命令行参数 | 环境变量 | 程序地址空间

一、命令行参数: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。

二、环境变量:进程的隐形配置

环境变量是操作系统中用来指定运行环境的一些参数,通常具有全局特性。

典型证据:

  1. 输入ls这样的命令时无需写完整路径->有环境变量PATH补全
  2. 编译链接时我们并不知道动态库、静态库在哪,但链接照样成功,原因就是有环境变量帮助编译器定位

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

地址完全相同,内容却不同。可以推出两条结论:

  1. 这两处变量绝对不是同一个;
  2. 这个地址也绝对不是物理地址。

它就是虚拟地址。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 为什么必须有虚拟地址空间

换个问法:如果程序直接操作物理内存会怎样?,三个问题很直接:

  1. 任何进程都能读写系统内存区域
  2. 程序每次被搬到内存的位置都不一样,地址无法确定
  3. 内存不够时要把整个进程拷到交换分区,效率低

有了地址空间与页表映射,就解决了:

  1. 保护物理内存
  2. 进程管理与内存管理解耦合,物理内存可任意位置加载数据
  3. 页表:实现地址无序(物理)->有序(虚拟)

四、总结

  • 命令行参数:argc 含程序名,argv 以 NULL 结尾,一个程序靠它跑出多种行为;
  • 环境变量:PATH/HOME/SHELL 是常客,只有 export 过的才会被 exec 传给子进程,代码里用 env[]、environ 或 getenv 读取;
  • 程序地址空间:栈向下、堆向上,C/C++ 里的地址全是虚拟地址,mm_struct + vm_area_struct 描述布局,页表完成映射,换来安全、确定与解耦

感谢阅读,我们下篇见。

相关推荐
雯宝1 小时前
devices_init / buses_init / classes_init / firmware_init / hypervisor_init 函数
linux
M78佐菲2 小时前
ARM学习笔记(12)
linux·arm开发·笔记·嵌入式硬件·学习
可涵不会debug2 小时前
C++ std::bind 实战教程:线程池、回调场景全覆盖
linux·开发语言·c++
青梅味猪大肠3 小时前
【操作系统-26】进程互斥软件实现-Peterson算法
linux·运维·服务器
wdfk_prog4 小时前
Wi-Fi Direct教程 01:在 Ubuntu 搭建双 mac80211_hwsim + wpa_supplicant 2.12
linux·运维·服务器·ubuntu·ros·wifi-direct
Vcaker4 小时前
Linux学习36-k8s集群升级及kube-vip高可用
linux·运维·学习
skywalk81635 小时前
Deepseek harness的4种模式 配置文件内容
linux·人工智能·ubuntu·deepseek·harness
沫璃染墨5 小时前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
YYRAN_ZZU5 小时前
GMSL开发加解串配置介绍
linux·嵌入式硬件