一、环境变量
1、基本概念
-
环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运⾏环境的⼀些参数
-
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
-
环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
1)命令行参数
- 命令行参数是程序运行时通过命令行传递给程序的额外信息,允许用户在启动程序时动态指定参数,增强程序的灵活性。在 C/C++ 中,命令行参数通过 main 函数的参数来接收。
示例1:
cpp
// code.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
for(int i=0; i<argc; i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}

- 这里的arcv是一个指针数组

示例2:
cpp
#include<stdio.h>
#include<string.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("Usage: %s [-a][-b][-c]\n", argv[0]);
return 1;
}
const char* arg=argv[1];
if(strcmp(arg,"-a")==0)
printf("这是功能1\n");
else if(strcmp(arg,"-b")==0)
printf("这是功能2\n");
else if(strcmp(arg,"-c")==0)
printf("这是功能3\n");
else
printf("Usage: %s [-a][-b][-c]\n", argv[0]);
return 0;
}
运行结果:

对比指令选项:

我们可以发现 main 的命令行参数,其实就是实现指令选项的原理。
2、常见环境变量
- PATH:用于指定可执行程序的搜索路径
- HOME:表示当前用户的主目录路径(即⽤户登陆到Linux系统中时,默认的⽬录)
- USER:存储当前登录用户的用户名
- SHELL:指定当前用户使用的命令行解释器(shell),它的值通常是/bin/bash。
- LANG:用于设置系统的语言和区域设置
- PWD:表示用户当前所在的目录
- OLDPWD:记录上一次的工作目录
3、查看环境变量
命令:echo $NAME,NAME是环境变量的名字。
举例:查看 PATH 环境变量
powershell
echo $PATH

测试环境变量 PATH
示例:
cpp
// hello.c
#include<stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
分别用 ./hello 执⾏和 hello 执⾏

我们发现如果程序不带路径,是无法执行的。
那么将我们的程序所在路径加⼊环境变量PATH当中
powershell
PATH=$PATH:/home/zsy/code/linux/test_11_9
此时再进行测试

发现在当前路径下的可执行程序就可以直接运行了!
4、环境变量相关命令
- echo:显⽰某个环境变量值
举例:echo $NAME

- export:设置⼀个新的环境变量
举例:export MYENV=1234

- env:显⽰所有环境变量
举例:env

- unset:清除环境变量
举例:unset MYENV

- set:显⽰本地定义的shell变量和环境变量
举例:set

5、环境变量的组织⽅式

每个程序都会收到⼀张环境表,环境表是⼀个字符指针数组,每个指针指向⼀个以 '\0' 结尾的环境字符串。
6、代码获取环境变量
- 命令⾏第三个参数 env
cpp
#include <stdio.h>
#include <unistd.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;
}
运行结果:

可以看到通过遍历命令行参数env,依次得到了当前的所有环境变量。
- 系统调用 getenv
举例:写一个程序,只能我执行,其他人一律不让执行!
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char* argv[], char* env[])
{
(void)argc;
(void)argv;
(void)env;
const char* who = getenv("USER");
if(who == NULL)
return 1;
if(strcmp(who,"zsy")==0)
{
printf("这是程序的正常执行逻辑\n");
}
else
{
printf("Only zsy!!!\n");
}
}
运行结果:

- 通过全局变量environ获取
cpp
#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]);
}
}
运行结果:

这里通过全局变量依次获取当前的环境变量。
- libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头⽂件中,所以在使⽤时要⽤ extern 声明。
7、环境变量具有全局属性
- 环境变量通常具有全局属性,可以被⼦进程继承下去
示例:
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* env=getenv("MYENV");
if(env)
{
printf("%s\n",env);
}
return 0;
}
运行结果:

直接查看MYENV,发现当前环境变量不存在。
导出环境变量:export MYENV="hello world"
再次执行程序:

发现有结果了,说明环境变量是可以被⼦进程继承下去的!
二、程序地址空间
1、程序地址空间
我们在学习C语言的时候或多或少都见过下面这张图片

我们可以对其进⾏各区域分布验证:
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char* argv[], char* env[])
{
const char* str = "helloworld";
printf("code addr: %p\n",main); // 正文代码
printf("init global addr: %p\n",&g_val); // 初始化数据
printf("uninit global addr: %p\n",&g_unval); // 未初始化数据
static int test = 10;
char* heap_mem = (char*)malloc(10);
char* heap_mem1 = (char*)malloc(10);
char* heap_mem2 = (char*)malloc(10);
char* heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n",heap_mem); // 堆
printf("heap addr: %p\n",heap_mem1);
printf("heap addr: %p\n",heap_mem2);
printf("heap addr: %p\n",heap_mem3);
printf("test static addr: %p\n", &test); // 静态变量
printf("stack addr: %p\n", &heap_mem); // 栈
printf("stack addr: %p\n",&heap_mem1);
printf("stack addr: %p\n",&heap_mem2);
printf("stack addr: %p\n",&heap_mem3);
printf("read only string addr: %p\n",str);
for(int i = 0; i<argc; i++)
{
printf("argv[%d]: %p\n",i ,argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n",i, env[i]);
}
return 0;
}
运行结果:

2、虚拟地址
示例:
cpp
#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
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;
}
运行结果:

我们发现,父子进程输出的变量值和地址是⼀模⼀样的,这其实很好理解,因为⼦进程以⽗进程为模版,且⽗⼦并没有对变量进⾏任何修改。
可是将代码稍加改动:
cpp
#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;
}
运行结果:

我们发现,⽗⼦进程的输出地址是⼀致的,但是变量内容不⼀样!
从中我们能得出以下结论:
-
变量内容不⼀样,说明⽗⼦进程输出的变量不是同⼀个变量
-
地址值是⼀样的,说明该地址不是物理地址
-
在Linux地址下,这种地址叫做 虚拟地址
-
我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!而物理地址,⽤户是看不到的,由OS进行统⼀管理。
-
OS必须负责将 虚拟地址 转化成 物理地址 。
3、进程地址空间
所以之前说程序的地址空间是不准确的,准确的应该说成进程地址空间,那该如何理解呢?看图:

- 从上⾯的图可以得出,同⼀个变量地址相同,其实是虚拟地址相同,内容不同是因为被映射到了不同的物理地址!
4、虚拟内存管理
描述 linux下进程地址空间所有信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。
cpp
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。
cpp
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,⽅便进程快速访问。
cpp
struct vm_area_struct {
unsigned long vm_start; // 虚存区起始
unsigned long vm_end; // 虚存区结束
struct vm_area_struct* vm_next, * vm_prev; // 前后指针
struct rb_node vm_rb; // 红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct* vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma* anon_vma;
const struct vm_operations_struct* vm_ops; // vma对应的实际操作
unsigned long vm_pgoff; // ⽂件映射偏移量
struct file* vm_file; // 映射的⽂件
void* vm_private_data; // 私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region* vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy* vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
所以我们可以对上图在进⾏更细致的描述,如下图所⽰:


5、为什么要有虚拟地址空间
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存⼤⼩是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感知!!
-
因为⻚表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射,在进程视⻆所有的内存分布都可以是有序的。