命令行参数

命令行参数是用户在命令行界面执行可执行程序 / 系统命令时,紧跟在程序名之后输入的字符串序列。

C语言程序想要接收命令行参数,必须使用 main 函数的完整标准原型:

int main(int argc, char *argv[])
参数名 全称 含义
argc argument count 命令行参数的总个数(整数)
argv argument vector 命令行参数数组(字符串数组)

规则

  • argv[0] 固定为程序名 / 程序路径
  • argv[1] ~ argv[argc-1]:用户输入的真正参数
  • argv[argc]:固定为 NULL(数组结束标记)
  • argc 最小值 = 1

示例:

#include <stdio.h>    
#include <string.h>    
    
int main(int argc, char *argv[])    
{    

    for(int i = 0; i < argc; i++)    
    {    
        printf("argv[%d]: %s\n", i, argv[i]);    
    }                                                                                     
    return 0;    
}

总结:进程拥有一张argv表(命令行参数表),用来支持实现选项功能。

  • 内核加载程序时,会将命令行参数拷贝到进程内存,为进程自动创建 argv 表;
  • 每个进程都有独立的 argv 表,互不干扰、互不共享;
  • 程序通过 main(argc, argv) 直接访问这张表。

环境变量

基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
  • 格式固定:大写变量名=字符串值
  • 本质:进程的全局配置表

核心特性

  • 系统中每一个进程都有自己独立的环境变量表,进程之间环境变量相互隔离。
  • 父子进程默认继承:父进程通过 fork() 创建子进程时,子进程会完整复制父进程的环境变量表
  • 子进程修改自己的环境变量 不会影响父进程,父进程修改也不会影响已经创建好的子进程

核心作用

  1. 提供命令搜索路径(核心 PATH),让系统不用写全路径就能执行命令
  2. 保存用户基本信息:家目录、用户名、当前工作目录
  3. 配置系统运行环境:语言编码、终端类型、动态库路径
  4. 为应用程序提供全局运行配置,无需修改代码即可改变程序行为

常见的环境变量

  1. PATH:命令搜索路径,多个路径用 : 分隔
  2. HOME:当前用户家目录
  3. USER:当前登录用户名
  4. PWD:当前所在工作目录
  5. OLDPWD上一次所在目录,cd - 就是读取它
  6. SHELL:当前默认命令行解释器(一般 /bin/bash
  7. HISTSIZE:内存中最多能保存多少条历史命令记录
  8. LD_LIBRARY_PATH:动态链接库搜索路径,程序运行时找依赖.so 库

Linux 环境变量 常用操作命令

  1. 查看环境变量
env          # 查看所有环境变量

echo $PATH   # 查看单个环境变量
echo $HOME
  1. 临时添加环境变量(只对当前这个终端窗口有效,关闭终端 → 变量立刻消失)
# 定义新环境变量
export 变量名="变量值"

# 最常用:给 PATH 追加路径(**千万不要直接覆盖,要追加!**)
export PATH=$PATH:新路径
  1. 取消环境变量
unset 变量名
unset TEST
  1. 永久生效(加载配置文件)
    修改完 /etc/profile(系统全局永久添加) 或 ~/.bashrc(当前用户永久添加)后执行:
source ~/.bashrc

环境变量 底层存储结构

在这里插入图片描述

  • 每个进程内部都有一张环境变量表,底层是字符串数组:char *environ[];
  • 数组中每一项都是 NAME=VALUE 格式字符串
  • 环境表末尾以 NULL 标记结束
  • 和命令行参数表 argv[] 并列,都是进程启动时由内核传入

补充:C 语言 main 完整原型:

int main(int argc, char *argv[], char *env[]);

第三个参数就是当前进程的环境变量表。

代码获取 Linux 环境变量

1.通过 main 函数第三个参数
示例:

#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;
}

2.通过标准库函数 getenv()(推荐使用)

  • 需包含头文件:#include <stdlib.h>
  • 函数原型:char *getenv(const char *name);
  • 功能:根据变量名,精准获取对应环境变量的值

示例:

#include <stdio.h>    
#include <stdlib.h>    
    
int main(int argc, char *argv[], char *env[])    
{    
    (void)argc;    
    (void)argv;    
    (void)env;    
    
    char *value = getenv("PATH");    
    if(value == NULL)    
        return 1;    
    
    printf("PATH->%s\n", value);
    return 0;
}

3.通过全局变量 environ
直接访问进程全局环境变量表

  • 需包含头文件:#include <unistd.h>
  • 全局变量声明:extern char **environ;
  • 本质:environ 是一个指针,直接指向进程的环境表数组

示例:

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <string.h>                                                                                                                
    
extern char **environ;    
    
int main(int argc, char *argv[])    
{    
    (void)argc;    
    (void)argv;    
    
    for(int i = 0; environ[i]; i++)    
    {    
        printf("environ[%d]-> %s\n", i, environ[i]);    
    }
    return 0;
}

补充:set 显示当前 Shell 里的所有变量,包含:环境变量export 导出的,子进程可继承)、Shell 局部变量(没加 export,仅当前 Shell 有效)、Shell 自定义函数

Shell 本地变量

Shell 本地变量(也叫局部变量、自定义 Shell 变量):在 当前 Shell 终端 中直接定义、不使用 export 导出 的变量。

语法变量名=变量值

核心特性

  • 只对当前这一个终端 Shell 有效
  • 通过 fork 创建的子进程、子 Shell 无法获取本地变量
  • 关闭当前终端、退出 Shell,本地变量直接消失

核心功能

  • 终端临时存数据:在命令行临时记录路径、文件名、数字、字符串
  • Shell 脚本内部做中间变量
    等等

进程地址空间

虚拟地址

来段代码感受一下:

#include <stdio.h>                                                                                                                 
#include <unistd.h>    
    
int gval = 100;    
    
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
        while(1)    
        {    
            printf("子:gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());    
            sleep(1);    
            gval++;    
        }    
    }    
    else     
    {    
        while(1)    
        {    
            printf("父:gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());    
            sleep(1);    
        }    
    }    
    
    return 0;    
}

运行结果:
在这里插入图片描述
先看运行现象

  • 父子进程打印的 &gval 地址完全相同
  • 子进程的 gval 会不断自增,父进程的 gval 永远是 100
  • 地址一样,值却不一样,违背常识,但完全符合 Linux 设计

核心答案

  • 地址一样:我们打印的是 虚拟地址不是物理地址。父子进程拥有独立但布局完全相同的虚拟地址空间,所以虚拟地址一致
  • 值不一样fork 采用 写时拷贝 机制,子进程修改变量时,操作系统会重新分配物理内存,父子进程最终映射到不同的物理地址,数据相互隔离。
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

进程地址空间布局

在这里插入图片描述
用户空间各区域详解(从低到高)

区域 存储内容
正文代码段(.text段) 程序编译后的机器指令(可执行代码)、只读的字符串常量(如"hello world"
初始化数据段(.data段) 已初始化、且值不为 0 的全局变量和静态变量
未初始化数据段(.bss段) 未初始化、或初始化为 0 的全局变量和静态变量
堆(Heap) 程序运行中通过malloc/calloc/new动态申请的内存
共享区(mmap 映射区) 动态链接库(.so文件)、共享内存、文件映射(mmap系统调用)
栈(Stack) 局部变量、函数参数、函数返回地址、栈帧(函数调用的上下文)
命令行参数与环境变量区 进程的命令行参数(argv[])和环境变量表(env[]
  • 堆向上、栈向下生长,中间的共享区作为弹性缓冲,避免堆和栈扩展时直接碰撞,同时为动态库 / 文件映射预留空间,最大化利用地址空间。
  • 每个进程的虚拟地址空间布局(代码段、数据段、堆、栈的地址范围)完全相同

一个进程,一个虚拟地址空间

在这里插入图片描述
32 位系统虚拟地址空间整体划分
32 位寻址总范围:2 ^ 32 = 4GB

固定分成两大区域:

  1. 用户空间0x00000000 ~ 0xBFFFFFFF3GB
  • 每个进程私有
  • 存放程序代码、全局变量、栈、堆、环境变量等
  1. 内核空间0xC0000000 ~ 0xFFFFFFFF1GB
  • 所有进程共享同一份
  • 存储内容:操作系统内核代码、页表、驱动程序、内核栈、内核数据结构(如task_struct
  • 用户态进程不能直接访问

一个进程,一套页表

在这里插入图片描述

  • 页表 是操作系统给进程准备的 「虚拟地址 → 物理地址」的映射对照表
  • 物理地址内存条硬件本身真实的地址编号,是内存单元的唯一硬件编号。
  • 虚拟地址操作系统为每个进程虚构、分配的逻辑地址

虚拟地址 与 物理地址的映射

  1. 进程在自己的进程地址空间里发出虚拟地址
  2. CPU 的 MMU 硬件,查当前进程的页表
  3. 把虚拟地址翻译成物理地址
  4. 访问真实内存条

上面父子进程代码的最终解释
在这里插入图片描述
现象:虚拟地址相同,变量值不同
根本原因:

  • 虚拟地址 &gval 一样
  • 父子各有独立进程地址空间、各有一套独立页表
  • 初始页表映射同一物理页
  • 子进程修改 gval++ → 触发写时拷贝 COW ,内核修改子进程自己的页表,映射到新物理页。
  • 父进程页表不变,仍指向原物理页
    同虚拟地址 + 两套不同页表 → 映射不同物理内存 → 值不一样

虚拟内存管理

  • 虚拟地址空间(进程地址空间)在内核中的本质,就是一个数据结构:struct mm_struct
  • struct mm_structLinux 内核专门用于描述、管理一个进程完整虚拟地址空间 的核心数据结构
  • 每一个进程都会有自己独立的 mm_struct

「归属关系」(内核底层绑定)
内核用 task_struct 描述进程,里面有一个指针:

struct task_struct {
    // ... 进程PID、状态、优先级等
    struct mm_struct *mm;  // 指向进程专属的虚拟地址空间管理结构体
};

简化版 struct mm_struct 源码

struct mm_struct {
    // 1. 虚拟地址空间的各个区域(代码段、数据段、堆、栈、映射区)
    struct vm_area_struct *mmap;    // 虚拟内存区域链表:管理代码段、数据段、堆、栈、动态库等所有虚拟内存区域
    struct rb_root mm_rb;           // 红黑树:快速查找虚拟地址属于哪个区域

    // 2. 页表(最核心!虚拟地址 → 物理地址的映射表)
    pgd_t *pgd;                     // 页全局目录(一套页表的起始地址)

    // 3. 虚拟地址空间的边界(32位:0 ~ 0xBFFFFFFF)
    unsigned long start_code, end_code;   // 代码段起止虚拟地址
    unsigned long start_data, end_data;   // 数据段起止虚拟地址
    unsigned long start_brk, brk;         // 堆区起止虚拟地址
    unsigned long start_stack;            // 栈区起始虚拟地址

    // 4. 其他:统计、权限、共享标记...
};
  • vm_area_struct *mmap + mm_rb 管理进程虚拟地址空间里的所有分段:代码段、数据段、BSS、堆、栈、动态库映射区。
    → 内核靠它知道:哪个虚拟地址属于哪个区域
  • pgd_t *pgd(页表指针)指向进程专属页表的起始地址
  • 各段虚拟地址边界(start_code /brk/start_stack) 记录32 位 4GB 虚拟空间的固定布局:代码段在哪、数据段在哪、堆从哪开始、栈从哪开始。

task_structmm_struct,进程地址空间的分布情况:
在这里插入图片描述

vm_area_struct(VMA)

vm_area_struct(VMA)是 Linux 内核描述进程一段连续虚拟内存区域的结构体

层级从属关系

task_struct 
        ↓
mm_struct  整个虚拟地址空间
        ↓
多个 vm_area_struct(VMA)
每一个 VMA 对应一段:代码段 / 数据段 / 堆 / 栈 / 动态库
  • 一个进程只有 1 个 mm_struct,一个 mm_struct 里有 若干个 vm_area_struct
  • 进程的代码段、数据段、堆、栈、动态库,每一段都是一个独立 VMA

精简版内核源码

struct vm_area_struct {
    // 1. 该虚拟区域的 起始、结束 虚拟地址
    unsigned long vm_start;
    unsigned long vm_end;
    // 2. 链表节点:把所有 VMA 串成双向链表
    struct vm_area_struct *vm_next, *vm_prev;
    // 3. 红黑树节点:用于快速查找
    struct rb_node vm_rb;
    // 4. 内存权限:可读、可写、可执行、私有/共享
    unsigned long vm_flags;
    // 5. 所属的虚拟地址空间(归属哪个 mm_struct)
    struct mm_struct *vm_mm;
    // 6. 文件映射偏移(映射文件、动态库时用)
    unsigned long vm_pgoff;
};

对上图再进行更细致的描述:
在这里插入图片描述
在这里插入图片描述

mm_struct 如何管理所有 VMA
struct mm_struct 里有两个成员:

struct mm_struct {
    struct vm_area_struct *mmap;   // VMA 双向链表头
    struct rb_root mm_rb;          // VMA 红黑树根
};

两种管理方式:
1.双向链表:从头到尾遍历所有内存区域
2.红黑树:根据虚拟地址快速查找所属区域、检查权限

为什么需要虚拟地址空间

  • 因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的
  • 地址空间和页表是OS创建并维护的!是不是也就意味着,凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问!!也顺便保护了物理内存中的所有的合法数据,包括各个进程以及内核的相关有效数据!
  • 因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载!物理内存的分配和进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐