📌 个人主页: 孙同学_
🔧 文章专栏: Liunx
💡 关注我,分享经验,助你少走弯路!
一.环境变量
1. 环境变量
1.1基本概念
- 环境变量
(environment variables)
一般是指在操作系统中用来指定操作系统运行环境的一些参数 - 如:我们在编写
C/C++
代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 - 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
- 命令行参数
我们会发现argv
是一个变长数组,会把我们输入的内容呈现出来。实际上argv
是一个指针数组。当我们在命令行中输入一个./code
以空格作为分隔符,其实我们输入的时一个长字符串,我们把它叫做命令行或者命令行命令,其实就是一个字符串。当我们执行c语言程序时,这个字符串就会被切分成以空格作为分隔符,切成好几份,所以它把第一个字符串的地址填在argv[0]
里面,依次类推。其中数组的有效元素个数就是argc
所以有人帮我们把命令行当中我们输入的字符串打散成这种以空格作为分隔符的上图这个样子,这个样子就叫做命令行参数 ,命令行参数依次变成一个字串,放到一个叫argv
的数组里,一共有argc
个有效元素,最后这个argv
把有效元素放完之后,必须以NULL
结尾。
指令选项实现原理:main
函数的命令行参数,是实现程序不同子功能的方法。
进程有一张argv
表,用来支持实现选项功能!
- 我们会发现当执行系统命令比如
ls
时不需要带./
,而执行我们自己的程序时就需要带./
,这是为什么呢?
- 要执行程序我们先得找到它,
./
表示在当前路径下,而系统命令不需要是因为存在环境变量,来帮助我们找到目标二进制文件 ls
是在/usr/bin/
路径下的,我们会发现当前路径下我们输入code
是不会运行的,而当我们把code
拷贝到/usr/bin
路径下就会执行了- 执行命令时系统为什么会在
/usr/bin
路径下去查呢?答案是系统中存在环境变量(PATH) ,来帮助系统找到目标二进制文件 - 环境变量(PATH):系统中搜索指令的默认搜索路径
1.2 常见环境变量
- PATH: 指定命令的搜索路径
- HOME: 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL: 当前
Shell
,它的值通常是/bin/bash
。
1.3 查看环境变量方法
env
:查看所有环境变量
环境变量的构成:名字+内容echo $NAME
: //NAME(你的环境变量名称)查看单个环境变量
🌵1.如何理解环境变量?存储的角度
bash
内部有两张表,一个是环境变量表,另一个是命令行参数表。
🌵2. 环境变量最开始从哪里来的呢?
- 系统相关的配置文件中来的
- 在每个用户的家目录里都会有
.bash_profile
和.bash_rc
这两个配置文件
1.4 和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的
shell
变量和环境变量
1.5 环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以'\0'
结尾的环境字符串
1.6 通过代码如何获取环境变量
- 方法一:main()函数
上面获取的环境变量是父进程(bash),环境变量可以被子进程继承 - 方法二:
getenv()
,它会根据环境变量的名字来获取指定环境变量的内容
✏️ 如果我们想写一个程序,只能我执行,其他人一律不执行,该如何设计呢?根据我们刚刚对环境变量的认识,现在只有一个人知道登陆用户是谁,那就是bash
.
➀我们写一个只有sp
能运行的程序
运行
可以正常运行
➁我们拿root
账号来运行一下
不能运行
所以这个程序只能由sp
运行
所以获取环境变量的第二种做法叫做getenv
,环境变量可以被子进程继承是因为我们可以把环境变量相关的信息让子进程继承下去,子进程就可以和环境变量来做个性化操作,比如定制一个只能自己执行的程序
- 方法三:使用全局变量
environ
我们可以看到它的参数类型为char **
,因为环境变量表是一个char *
的,所以char **environ
应该指向第一个元素
2. 环境变量的特性
2.1 环境变量具有全局特性
- 环境变量通常具有全局属性,可以被子进程继承下去
2.2 补充两个概念
bash
会记录两套变量:一个是环境变量,一个是本地变量
可以通过set
命令查到所有的本地变量,本地变量不会被子进程继承,只在bash
内部使用- 我们的环境变量是在谁的上下文里面呢?
bash
export
命令是一个内建命令built-in command
,不需要创建子进程,而让bash
自己亲自执行,或者系统调度完成。
二.程序地址空间
1. 程序地址空间回顾
我们在以前学习c/c++
的时候,就听说过c/c++
程序默认内存地址空间是代码区,字符常量区,初始化数据区,未初始化数据区,堆区,栈区,共享区等。
- 下面我们把整个程序的内存空间布局以代码的方式打出来
- 证明这个地址是虚拟地址
结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用
C/C++
语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS
统一管理
OS
必须负责将 虚拟地址 转化成 物理地址
2. 进程地址空间
一个进程一个虚拟地址空间,虚拟地址空间的宽度是一字节,32位下是2^32
个地址 * 1字节 = 4GB(0-3GB用户空间[拿到地址就能直接访问],3-4GB内核空间),64位下是2^64
个地址。
一个进程一套页表,页表是用来做虚拟地址和物理地址映射的。
一个int
类型有4个字节,但我们的虚拟进程空间的宽度位1个字节,如何处理呢?因为我们有类型,实际上我们在访问任何一个变量时,只要知道起始地址+偏移量就访问到了,系统访问的是最小的那个地址。
有父进程,也就会有子进程,子进程的很多东西都是拷贝父进程的,他把父进程task_struct
里面的属性给自己拷贝一份,把个别的属性自己一更改,一个进程,一个虚拟地址空间,一个进程,一套页表,所以我们的子进程也有自己的虚拟地址空间和页表
以前我们说过,子进程的PCB
和一些物理属性都是拷贝自父进程的,同样的,页表也是拷贝自父进程的,相当于发生了浅拷贝,所以子进程和父进程就有相同的虚拟地址空间,我们也就理解了为什么全局变量为什么默认地被父子进程共享,因为他们的虚拟地址空间到物理空间的映射关系是一样的,相当于它们指向同一块物理内存。变量如此,代码也是如此。
上面我们演示的子进程的gval++
,父进程的gval
不变是怎么回事呢?
原因是子进程的gval++
的时候,操作系统会在物理内存空间上重新开辟一块空间,把老变量gval
的内容拷贝到新空间,此时就得到了一个新的变量或者物理地址,然后操作会把这个新的物理空间地址给给子进程的页表,构建全新的映射关系,这种机制称为写实拷贝
- 上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
3. 虚拟内存管理 - 第一讲
描述linux
下每一个虚拟地址空间的所有信息的结构体是mm_struct
,每一个进程都只有一个mm_struct
结构体,每个进程的task_struct
结构体中都只一个指针指向mm_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;
我们可以对上图在进行更细致的描述,如下图所示 :
虚拟地址空间的意义:
- 将地址从无序变为有序
- 将虚拟地址转化为物理地址(
OS
查找页表),也可以对你的地址和操作进行合法判定,进而保护物理内存
📌:页表项里面除了虚拟地址和物理地址外,还有一个条目,这个条目里面包含着r,w,x
权限,实现对物理内存的保护
✏️ 再谈野指针问题
当这个指针指向的对应区域被释放了,即物理内存释放了,所以映射关系要去掉,当对一个已经释放了的内存访问时,页表中就不存在对应的虚拟物理映射关系,查页表时会失败,操作系统会知道,就把进程干掉了,所以有了野指针之后,进程有可能会崩溃。
✏️ char *str = "hello world" ; *s = 'H'; 这段代码能编过吗?答案使能编过
上面的char *str = "hello world" ;
我们都知道叫做字符串常量,我们用指针指向字符串常量时,我们在c
语言中已经学过,字符串常量不能被修改。字符串是被编译到字符常量区的,也就是和正文部分是编到一块的,所以它是只读 的,所以想把这个字符串常量修改成 *s = 'H';
时,查页表时就会发现是只读的,而要写时页表会转化失败,所以操作系统不让我们转。
✏️ 再谈为什么要有虚拟地址空间
让进程管理和内存管理进行一定程度的解耦合
🎯 澄清一些问题!
- 我们可以不加载代码和数据,只有
task_struct
,mm_struct
,页表,程序也能运行,因为存在缺页中断 - 创建进程,先有
task_struct
,mm_struct
等,还是先加载代码和数据?
答案是先要有内核数据结构,然后才加载代码和数据 - 如何理解进程挂起(阻塞挂起)??
先找到对应的进程,然后将页表清空,将物理内存里对应的代码和数据和换出到swap
分区里。只保留页表中的左半部分,而把右半部分换出。 - 堆区细节性话题:堆区有自己的开始与结束,我们平时用堆区时可能
malloc
了好多次,申请了不同的堆空间,而每个堆都有起始地址,而定义的堆空间上只有一个起始和结束,那么如何确定其他的地址开始和结束呢?
vm_area_struct
里面就有vm_start
和vm_end
,会记录下你所需要的vm_start
和vm_end
,一份堆区对应一个vm_area_struct
👍 如果对你有帮助,欢迎:
- 点赞 ⭐️
- 收藏 📌
- 关注 🔔