
⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**
** 🔥热门专栏🔥**
文章目录
-
- 上期回顾
- [4. 命令行参数和环境变量](#4. 命令行参数和环境变量)
-
- [4-1 基本概念](#4-1 基本概念)
- [4-2 常见环境变量](#4-2 常见环境变量)
- [4-3 查看环境变量方法](#4-3 查看环境变量方法)
- [4-4 和环境变量相关的命令](#4-4 和环境变量相关的命令)
- [4-5 环境变量的组织方式](#4-5 环境变量的组织方式)
- [4-6 通过代码如何获取环境变量](#4-6 通过代码如何获取环境变量)
- [4-7 通过系统调用获取或设置环境变量](#4-7 通过系统调用获取或设置环境变量)
- [4-8 环境变量通常是具有全局属性的](#4-8 环境变量通常是具有全局属性的)
- [5. 程序地址空间](#5. 程序地址空间)
-
- [5-1 研究平台](#5-1 研究平台)
- [5-2 程序地址空间回顾](#5-2 程序地址空间回顾)
- [5-3 虚拟地址](#5-3 虚拟地址)
- [5-4 进程地址空间](#5-4 进程地址空间)
- [5-5 虚拟内存管理 - 第一讲](#5-5 虚拟内存管理 - 第一讲)
- [5-6 为什么要有虚拟地址空间](#5-6 为什么要有虚拟地址空间)
- 进程上下总结
- 下期预告
- 进程控制
- 结语
上期回顾
大家好,这里是此生弃int(财哥) ,欢迎来到我的博客。上一篇我们主要学习了冯诺依曼体系、操作系统、以及学习了进程,进程的概念和进程的状态、优先级、切换和调度 ,今天我们接着学习命令行参数和环境变量,最后来看看虚拟地址空间,这两部分内容可以帮助我们更好的理解进程和操作系统!
4. 命令行参数和环境变量
4-1 基本概念
- 环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
4-2 常见环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
- SHELL :当前 Shell,它的值通常是
/bin/bash。
4-3 查看环境变量方法
bash
# NAME: 你的环境变量名称
echo $NAME
测试 PATH
- 创建 hello.c 文件
c
#include <stdio.h>
int main()
{
printf("hello world!\n");
printf("这是一个我的个人程序。\n");
return 0;
}
- 对比
./hello执行和hello之间执行 - 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?
- 将我们的程序所在路径加入环境变量 PATH 中,程序所在路径
bash
export PATH=$PATH:hello
- 对比测试
- 还有什么方法可以不用带路径,直接就可以运行呢?
测试 HOME
- 用 root 和普通用户,分别执行
echo $HOME,对比差异 - 执行
cd ~; pwd,对应~和HOME的关系
4-4 和环境变量相关的命令
echo:显示某个环境变量值export:设置一个新的环境变量env:显示所有环境变量unset:清除环境变量set:显示本地定义的 shell 变量和环境变量
4-5 环境变量的组织方式
每个程序都会收到一张环境表 ,环境表是一个字符指针数组,每个指针指向一个以 '\0' 结尾的环境字符串
4-6 通过代码如何获取环境变量
方法一:命令行第三个参数
c
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
方法二:通过第三方变量 environ 获取
c
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
libc 中定义的全局变量 environ 指向环境变量表,environ 没有包含在任何头文件中,所以在使用时要
extern声明。
4-7 通过系统调用获取或设置环境变量
putenv,后面讲解getenv,本次讲解
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
常用 getenv 和 putenv 函数来访问特定的环境变量。
4-8 环境变量通常是具有全局属性的
- 环境变量通常具有全局属性,可以被子进程继承下去
5. 程序地址空间
5-1 研究平台
- kernel 2.6.32
- 32 位平台
5-2 程序地址空间回顾
我们在讲 C 语言的时候,老师给大家画过这样的空间布局图:

5-3 虚拟地址
虚拟地址,顾名思义,就是不是真实的物理地址,是逻辑地址!也就是,只是编了一些号,把一个数32位比特当做一个地址,规定多少到多少编号为什么地址(栈、堆、静态区等等),为什么我们要设计虚拟地址?原因很多,我这里将一个就是,不可能让程序去访问和修改真实的物理地址,因为要是谁都可以改,那程序a不是可以改程序b的值啊?并且万一你写的代码访问或者修改操作系统程序对应的地址内容,那不完蛋了!所以我们要设计虚拟地址,然后再让(你申请的)每一个虚拟地址与物理地址对应,
虚拟地址让每个程序都认为自己拥有4GB内存(32位)
为什么是4GB?

来段代码感受一下:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出结果:
bash
# 与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在 Linux 地址下,这种地址叫做 虚拟地址
- 我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS 统一管理
- OS 必须负责将虚拟地址转化成物理地址。
5-4 进程地址空间
所以之前说"程序的地址空间"是不准确的,准确的应该说成 进程地址空间,该如何理解呢?看图:

分页 & 虚拟地址空间
说明:
- 上面的图就足以说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
5-5 虚拟内存管理 - 第一讲
OS通过一个结构体来组织和描述虚拟地址,里面通常会存放每种地址(比如栈)的起始地址和结束地址等等信息 ,
描述 linux 下进程的地址空间的所有的信息的结构体是 mm_struct(内存描述符)。每个进程只有一个 mm_struct 结构,在每个进程的 task_struct 结构中,有一个指向该进程的 mm_struct 结构体指针。
c
struct task_struct
{
/*...*/
struct mm_struct *mm; //对于普通的用户进程来说该字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。
struct mm_struct *active_mm; // 该字段是内核线程使用的。当该进程是内核线程时,它的mm字段为NULL,表示没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是一样的,内核线程可以使用任意进程的地址空间。
/*...*/
}
可以说,mm_struct 结构是对整个用户空间的描述。每一个进程都会有自己独立的 mm_struct,这样每一个进程都会有自己独立的地址空间才能互不干扰。先来看看由 task_struct 到 mm_struct,进程的地址空间的分布情况:
定位 mm_struct 文件所在位置和 task_struct 所在路径是一样的,不过他们所在文件是不一样的,mm_struct 所在的文件是 mm_types.h
c
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
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;
/*...*/
}
那既然每一个进程都会有自己独立的 mm_struct,操作系统肯定是要将这么多进程的 mm_struct 组织起来的!虚拟空间的组织方式有两种:
- 当虚拟区较少时采取单链表,由 mmap 指针指向这个链表;
- 当虚拟区间多时采取红黑树 进行管理,由 mm_rb 指向这棵树。

linux 内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个 vm_area_struct 结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是 vm_area_struct 结构来连接各个 VMA,方便进程快速访问。
所以我们可以对上图在进行更细致的描述,如下图所示:

5-6 为什么要有虚拟地址空间
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。
那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存大小是 128M,现在同时运行两个程序 A 和 B,A 需占用内存 10M,B 需占用内存 110M。计算机在给程序分配内存时会采取这样的方法:先将内存中的前 10M 分配给程序 A,接着再从剩余的 118M 中划分出 110M 分配给程序 B。
这种分配方法可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略问题很多。
问题:
- 安全风险
- 每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够去读写系统相关内存区域,如果是一个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
- 地址不确定
- 众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每一次运行都是不确定的,比如:第一次执行 a.out 时候,内存当中一个进程都没有运行,所以搬移到内存地址是 0x00000000,但是第二次的时候,内存已经有 10 个进程在运行了,那执行 a.out 的时候,内存地址就不一定了
- 效率低下
- 如果直接使用物理内存的话,一个进程就是作为一个整体(内存块)操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区中,好腾出内存,但是如果是物理地址的话,就需要将整个进程一起拷走,这样,在内存和磁盘之间拷贝时间太长,效率较低。
存在这么多问题,有了虚拟地址空间和分页机制就能解决了吗?当然!
虚拟地址空间和分页机制带来的好处:
- 地址空间和页表是 OS 创建并维护的!是不是也就意味着,凡是想使用地址空间和页表进行映射,也一定要在 OS 的监管之下来进行访问!!也顺便保护了物理内存中的所有的合法数据,包括各个进程以及内核的相关有效数据!
- 因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载!物理内存的分配和进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合。
- 因为有地址空间的存在,所以我们在 C、C++ 语言上 new, malloc 空间的时候,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这是由操作系统自动完成,用户包括进程完全 0 感知!!
- 因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的。
进程上下总结
本节核心围绕进程这一操作系统核心概念展开,涵盖冯诺依曼体系、操作系统管理思想、进程描述与组织、进程状态与调度、环境变量以及虚拟地址空间等关键知识点。
主要知识点框架:
| 模块 | 核心要点 |
|---|---|
| 冯诺依曼体系 | 所有设备只能直接和内存打交道 |
| 操作系统 | "搞管理"的软件:先描述(struct),再组织(链表/红黑树) |
| 进程概念 | 进程 = task_struct(PCB)+ 程序代码和数据 |
| 进程状态 | R(运行)、S(可中断睡眠)、D(不可中断睡眠)、T(停止)、X(死亡)、Z(僵尸) |
| 特殊进程 | 僵尸进程(子进程退出,父进程未回收)、孤儿进程(被 1 号进程领养) |
| 进程优先级 | PRI(new) = PRI(old) + nice,nice 取值 -20~19 |
| 调度算法 | Linux 2.6 O(1) 调度:active/expired 队列 + bitmap |
| 环境变量 | PATH、HOME、SHELL 等;具有全局继承性 |
| 虚拟地址 | C/C++ 中看到的都是虚拟地址,OS 负责虚拟→物理的转换 |
关键结论:
- 管理的本质:操作系统对任何资源的管理都是"先描述,再组织"------用结构体描述,用数据结构组织。
- 进程独立性:通过 fork + 写时拷贝技术保证父子进程的独立性,是进程独立性的技术保证。
- 调度公平性:Linux 2.6 的 O(1) 调度算法通过 bitmap + 双队列实现了常数时间复杂度的进程调度。
- 虚拟地址的价值:虚拟地址空间 + 分页机制解决了直接使用物理内存的三大问题(安全、地址不确定、效率低),同时实现进程管理与内存管理的解耦合。
- 环境变量继承 :环境变量通过
export导出后可以被子进程获取,这是通过 fork 复制父进程环境表实现的。
下期预告
进程控制
结语
** 本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**
** 也欢迎订阅我的**
深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++
算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线
快速复习系列:知识梳理、查漏补缺,考前冲刺必备
Linux系列:从零带你系统学习Linux系统,深入理解计算机组成原理!
** 愿每一次敲下键盘,都比昨天更进一步!** ** 愿每一行代码落下,都让未来多一种可能!**