(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),来帮助系统找到目标二进制文件Shell,它的值通常是/bin/bash。env:查看所有环境变量

环境变量的构成:名字+内容
echo $NAME : //NAME(你的环境变量名称)查看单个环境变量🌵1.如何理解环境变量?存储的角度
bash内部有两张表,一个是环境变量表,另一个是命令行参数表。

🌵2. 环境变量最开始从哪里来的呢?
.bash_profile和.bash_rc这两个配置文件

shell变量和环境变量
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串


上面获取的环境变量是父进程(bash),环境变量可以被子进程继承
getenv(),它会根据环境变量的名字来获取指定环境变量的内容


✏️ 如果我们想写一个程序,只能我执行,其他人一律不执行,该如何设计呢?根据我们刚刚对环境变量的认识,现在只有一个人知道登陆用户是谁,那就是bash.
➀我们写一个只有sp能运行的程序

运行

可以正常运行
➁我们拿root账号来运行一下

不能运行
所以这个程序只能由sp运行
所以获取环境变量的第二种做法叫做getenv ,环境变量可以被子进程继承是因为我们可以把环境变量相关的信息让子进程继承下去,子进程就可以和环境变量来做个性化操作,比如定制一个只能自己执行的程序
environ

我们可以看到它的参数类型为char **,因为环境变量表是一个char * 的,所以char **environ应该指向第一个元素



bash会记录两套变量:一个是环境变量,一个是本地变量
可以通过set命令查到所有的本地变量,本地变量不会被子进程继承,只在bash内部使用
bash
export命令是一个内建命令built-in command,不需要创建子进程,而让bash自己亲自执行,或者系统调度完成。我们在以前学习c/c++的时候,就听说过c/c++程序默认内存地址空间是代码区,字符常量区,初始化数据区,未初始化数据区,堆区,栈区,共享区等。





结论:
C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理OS必须负责将 虚拟地址 转化成 物理地址
一个进程一个虚拟地址空间,虚拟地址空间的宽度是一字节,32位下是2^32个地址 * 1字节 = 4GB(0-3GB用户空间[拿到地址就能直接访问],3-4GB内核空间),64位下是2^64个地址。
一个进程一套页表,页表是用来做虚拟地址和物理地址映射的。

一个int类型有4个字节,但我们的虚拟进程空间的宽度位1个字节,如何处理呢?因为我们有类型,实际上我们在访问任何一个变量时,只要知道起始地址+偏移量就访问到了,系统访问的是最小的那个地址。
有父进程,也就会有子进程,子进程的很多东西都是拷贝父进程的,他把父进程task_struct里面的属性给自己拷贝一份,把个别的属性自己一更改,一个进程,一个虚拟地址空间,一个进程,一套页表,所以我们的子进程也有自己的虚拟地址空间和页表

以前我们说过,子进程的PCB和一些物理属性都是拷贝自父进程的,同样的,页表也是拷贝自父进程的,相当于发生了浅拷贝,所以子进程和父进程就有相同的虚拟地址空间,我们也就理解了为什么全局变量为什么默认地被父子进程共享,因为他们的虚拟地址空间到物理空间的映射关系是一样的,相当于它们指向同一块物理内存。变量如此,代码也是如此。
上面我们演示的子进程的gval++,父进程的gval 不变是怎么回事呢?
原因是子进程的gval++的时候,操作系统会在物理内存空间上重新开辟一块空间,把老变量gval的内容拷贝到新空间,此时就得到了一个新的变量或者物理地址,然后操作会把这个新的物理空间地址给给子进程的页表,构建全新的映射关系,这种机制称为写实拷贝

描述linux下每一个虚拟地址空间的所有信息的结构体是mm_struct,每一个进程都只有一个mm_struct结构体,每个进程的task_struct结构体中都只一个指针指向mm_struct。
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。
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,方便进程快速访问。
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
