Linux环境变量与程序地址空间
文章目录
- Linux环境变量与程序地址空间
- 1.环境变量
- 1.1 环境变量概念与深入理解
- 1.2 代码获取环境变量的方法
- 1.3 系统调用获取和设置环境变量的方法
- 2.程序地址空间
- 2.1 程序地址空间图(准确来说是进程地址空间图)
- 2.2 程序地址空间的验证
- 2.3 进程地址空间概念
- 2.4 引入虚拟内存空间原因
- 2.5 关于进程地址空间技术的一些疑难问题解答
- 3.Linux内核的进程调度队列
- 3.1 Linux内核调度系统简图
- 3.2 对于Linux内核调度队列的理解
1.环境变量
1.1 环境变量概念与深入理解
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
比如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
Linux下的环境变量和Windows下的环境变量意思差不多,只是Windows使用了GUI,如下图:
常见环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash
查看环境变量方法:
- 方法一:
echo $PATH
- 方法二:
env|grep PATH
注:每个
:
分割一个环境路径
和环境变量相关的命令:
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
环境变量的组织方式:每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串==
1.2 代码获取环境变量的方法
方法一:打印命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
//我们给main函数传递的argc、argv[]参数,其实是传递的命令行中输入的程序名和选项!
//char *env[]存储的是环境变量的地址
int i = 0;
for(; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
//注:char *env[]就是下图的environ
方法二:通过第三方变量environ获取
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
//libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
extern char** environ;
for(int i=0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
1.3 系统调用获取和设置环境变量的方法
putenv:获取环境变量
setenv:设置环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
2.程序地址空间
2.1 程序地址空间图(准确来说是进程地址空间图)
这里主要提一下:"程序的地址空间"是不准确的,准确的应该说成进程地址空间。在进程地址空间概念小节会详细说明!
2.2 程序地址空间的验证
参考现象代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<malloc.h>
int g_val=100;
int g_unval;
int main(int argc,char* argv[],char* envp[])
{
printf("code addr:%p\n",main);
char* str = "hello world";
printf("read only addr:%p\n",str);
printf("init addr:%p\n",&g_val);
printf("uninit addr:%p\n",&g_unval);
int* p = malloc(10);
printf("heap addr:%p\n",p);
printf("stack addr:%p\n",&str);
printf("stack addr:%p\n",&p);
for(int i=0;i<argc;i++)
{
printf("args addr:%p\n",argv[i]);
}
int i=0;
while(envp[i])
{
printf("env addr:%p\n",envp[i]);
i++;
}
return 0;
}
大家可以观察一下,按照打印的顺序,地址都是由低到高依次打印的,这也就证实了上面那个图
我们将代码稍加改动下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<malloc.h>
#include<sys/types.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("child: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
printf("father: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
}
sleep(1);
return 0;
}
值是一样的,地址也是一样也很正常,没有什么问题
我们在修改以下代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<malloc.h>
#include<sys/types.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
g_val = 200;
printf("child: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
printf("father: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
}
sleep(1);
return 0;
}
从这里开始我们就发现问题了,明明子进程全局变量改成了200,父子进程的地址是一样的,为什么父进程没有受到影响呢?
我们可能认为是父进程先执行的,子进程后执行的,所以子进程改了没有影响父进程,那么好,接下来我就让子进程先结束
从上图,我们发现让子进程先结束也并没有改变结果呀!说明并不是父子程序执行顺序引起的问题!
那究竟是为什么呢?
首先让我们分析一下问题:
- 进程已经将全局变量改成200,为什么父进程没有受影响?
- 为什么子进程已经改了,父进程才来读数据,读到100已经够奇怪了,为什么它们的地址还是一样的?
推理分析过程:
经过上图的分析,我们总结一下:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明:该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
- 其实所谓的虚拟地址就是上面程序地址空间的那个图,它们经过一定的方式转换成物理地址,进行物理级别的访问
2.3 进程地址空间概念
所谓的进程地址空间:其实就是OS通过软件的方式,给进程提供一个软件视角,认为自己会独占系统的所有资源(内存)
- 这里就是通过父进程的进程地址空间通过页表映射,映射到物理内存中,当我们创建子进程的时候,本质上是系统内部多了一个进程,而且每一次申请一个新进程的时候,操作系统会为当前新进程创建一个属于该进程的地址空间,所以子进程也有一个属于自己的页表,因为在创建的时候是以父进程为模板,也就意味着父子进程使用的代码和数据都是一样的
- 所以我们的子进程对变量进行写入,应不应该影响父进程呢?
页表里取的变量都不一样,很明显是不影响!(这也就回答了上面的为什么修改不会互相影响)
所以在更改的时候,在物理内存空开辟一个4个字节的空间,将新的值200写进去,更改映射关系,不再指向父进程数据,而是指向新开辟的空间,所以它们的虚拟地址是一样的,但是打印出来的值是不一样的,这种写入时再发生内存重新申请的技术叫做写时拷贝
- 这样也就实现了在数据上实现了分离!
2.4 引入虚拟内存空间原因
- 通过上图对于虚拟内存空间的总结,我们可以知道引入虚拟内存空间的最主要原因是:
保护物理内存
- 虚拟内存空间技术相当于一种
保护模式
,保证进程不使用物理地址,而是使用虚拟地址,最后由操作系统将虚拟地址映射到物理内存地址上- 进程直接访问内存是不安全的,在访问内存添加了一层软硬件层,可以对转化过程进行审核,非法的操作直接拦截,通过地址空间,进行功能模块的解耦
举个例子:
- 假如我们写代码出现了野指针问题,那么这个野指针就会在虚拟地址空间到处乱指,但是无论它怎么乱指也影响不到物理内存,所以随便你乱串,操作系统都不怕,野指针影响的只是你的程序,对操作系统没一点影响!所以这就是起到了保护作用!
最终我们得出引入虚拟内存空间的原因:
- 保护内存
- 管理进程
- 让进程或者程序以一种统一的视角看待内存(简化进程本身的设计与实现)
2.5 关于进程地址空间技术的一些疑难问题解答
为什么数据要进行写时拷贝?
- 进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
为什么不在创建子进程的时候就进行数据的拷贝?
- 子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间
代码会不会进行写时拷贝?
- 90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝
为什么要有进程地址空间?
- 有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是保护内存
- 有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置
- 有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离
对于创建进程的现阶段理解:
- 一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建
3.Linux内核的进程调度队列
3.1 Linux内核调度系统简图
Linux内核调度系统:
Linux内核调度队列:
- 扩展:一个CPU只有一个runqueue(运行队列),如果有多个CPU就要考虑进程个数的负载均衡问题
3.2 对于Linux内核调度队列的理解
活动队列(如上图):
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
- 从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
过期队列(如上图):
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针与expired指针(如上图):
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!