进程概念(2)
- 环境变量
- 程序地址空间
今天,我和大家一起相约系统编程的进程概念篇,一起了解环境变量对于运行中的程序的益处以及程序地址空间的分布。
环境变量
⭐概念:存放系统运行环境参数的变量(配置参数时通过变量修改,则比较方便简单)
⭐和环境变量相关的命令:
- env:查看系统中的环境变量
- echo $:打印一个指定变量的内容,后面加变量的名称
- set:查看所有变量,包括环境变量在内
- export:设置一个环境变量;export 变量名
- unset:删除一个变量; unset 变量名
⭐目的:让系统运行环境参数配置起来更加灵活 / 环境变量具有全局特性
⭐通过代码如何获取环境变量:
- char *getenv(const char * name)
-通过环境变量名称获取环境变量内容 ;
-该函数的头文件:#include<stdlib.h>;
-直接打印value,不会打印name。 - main(int argc,char *argv[],char *env[]);
-argv存放命令以及命令后面的一个个运行参数;
-argc存放运行参数的个数;
-env存放当前程序的所有环境变量(shell中存放的环境变量都有,当前进程理解为shell的一个子进程,子进程会继承父进程的环境变量)
注:env和argv这两个指针数组都是以 NULL结尾的 - extern char**environ;
-库中的变量,是一个二级指针,程序运行时会将库加载到内存中;
-在代码中先声明有这个变量,否则链接时会出错。
//getenv:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
//命令行的第三个参数
#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获取
#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;
}
⭐环境变量的组织方式:
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
😊为什么说子进程可以继承父进程的环境变量?
首先考虑进程的虚拟地址空间 ,命令行参数和环境变量在栈之上 ;子进程以父进程为模板,拷贝PCB、虚拟地址空间 ;所以会被继承下去 。
所以我们接着继续了解程序地址空间的分布。
程序地址空间
首先我们先看一段代码:
#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;
}
//与环境相关,观察现象即可
//child[3046]: 100 : 0x80497e8
//parent[3045]: 0 : 0x80497e8
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
但地址值是一样的,说明,该地址绝对不是物理地址!
在Linux地址下,这种地址叫做 虚拟地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
为什么不直接在物理内存上操作呐? 因为会导致内存利用率低,缺乏访问控制,所以对于每个程序有了各自的虚拟地址空间。
⭐虚拟地址空间:(内存描述符)是操作系统为进程所描述的一个假的地址空间;目的是为了让进程认为自己拥有一块连续的线性的完整的地址空间;但实际上一个进程使用的内存并非连续存储,而是通过页表映射了虚拟地址与物理地址之间的关系;让进程通过页表获取物理地址,进而实现数据的离散式存储。
⭐作用:
1.提高内存利用率:物理内存的离散存储。
2.保证了进程的独立性:每个进程都只能访问自己虚拟地址映射的物理内存。
3.页表可以进行内存访问控制:页表可以对每个虚拟地址进行权限标记。
⭐页表的作用:
1.映射虚拟地址和物理地址之间的关系
2.内存访问控制
⭐请问:操作系统给进程是怎样描述一个虚拟地址空间的?
mm_struct结构体(内存指针是该结构体类型),结构体内对于每一段的地址给出起初和终止位置。
⭐理解父子进程间的代码共享,数据独有:
😊创建一个子进程的流程:写实拷贝技术(提高进程的创建效率)
1.创建PCB
2.拷贝父进程PCB中的数据(拥有相同的虚拟地址空间,相同的页表,相同的内存指针……)
3.父子进程一开始映射同一块物理内存
4.等到物理内存修改的时候才为子进程重新开辟内存,拷贝数据过来(要求进程的独立性)
代码共享和数据独有:因为代码段是只读的,不会修改,因此会一直映射同一块物理内存;数据独有,是因为数据有自己的各自的虚拟地址空间,当发生改变,各自有各自的物理地址空间。
⭐内存管理:
页表如何映射虚拟地址和物理地址—MMU(虚拟内存地址和物理内存地址都是按照相同的管理组织方式)
- 分段式:对程序中的地址管理比较友好,但是内存利用率不高
- 分页式:提高内存的利用率
- 段页式:目前采用的方案
分页式内存管理:
地址构成:页号+页内偏移
给出虚拟地址对应求出物理地址:
—>虚拟地址/页面大小=页号
—>在页表项中根据页号找到对应的块号
—>块号*块大小+页内偏移(虚拟地址%页面大小)=物理地址
分段式内存管理:
采用分页内存管理有一个不可避免的问题:用户视角的内存和实际内存的分离。设想一段main函数代码,里面包含Sqrt函数的调用。按照编写者的理解,这段代码运行时,操作系统应该分配内存给:符号表(编译时使用),栈(存放局部变量与函数参数值),Sqrt代码段,主函数代码段等。这样,编写者就可以方便地指出:“函数sqrt内存模块的第五条指令”,来定位一个元素。因此逻辑地址由有序对构成:<segment-number,offset>(<段号s, 段内偏移d>)。段偏移d因该在0和段界限之间,如果合法,那么就与基地址相加而得到所需字节在物理内存中的地址。因此段表是一组基地址和界限寄存器对。
段页式内存管理:
在段页式系统中,为了便于实现地址变换,须配置一个段表寄存器,其中存放段表始址和段表长TL。进行地址变换时,首先利用段号S,将它与段表长TL进行比较。若S<TL,表示未越界,于是利用段表始址和段号来求出该段所对应的段表项在段表中的位置,从中得到该段的页表始址,并利用逻辑地址中的段内页号P来获得对应页的页表项位置,从中读出该页所在的物理块号b,再利用块号b和页内地址来构成物理地址。
😊OK,今天笔者先总结到这里。如有问题或者疑惑,欢迎各位小伙伴们指出。我们一起交流,一起进步!