1.环境变量
1.1基本概念
环境变量 (environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
下面这段代码就是将命令行参数传递给main函数的参数,然后进行模仿命令行的指令+选项,选项的本质也就是命令行参数。
cpp
int main(int argc, char *argv[])
{
if (argc != 2)
{
cout << "Usage: " << argv[0] << "-number[1-3]" << endl;
return -1;
}
if (strcmp("-1", argv[1]) == 0)
cout << "Function 1" << endl;
else if (strcmp("-2", argv[1]) == 0)
cout << "Function 2" << endl;
else if (strcmp("-3", argv[1]) == 0)
cout << "Function 3" << endl;
else if (strcmp("-4", argv[1]) == 0)
cout << "Function 4" << endl;
else if (strcmp("-5", argv[1]) == 0)
cout << "Function 5" << endl;
else
cout<<"unknown!"<<endl;
return 0;
}
1.2查看环境变量方法
echo $NAME //NAME: 你的环境变量名称
1.3常见环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )
SHELL : 当前 Shell, 它的值通常是 /bin/bash 。
这个全局的环境变量就是能够查看到指定命令的搜索路径,也就是说这个搜索路径是os的默认搜索路径,所以我们执行自己写的程序时,需要加./来告诉os我们的程序是在当前目录下,当然我们也可以修改PATH。
指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
1.4环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的 shell 变量和环境变量
1.5通过代码如何获取环境变量
命令行第三个参数
cpp
#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;
}
1.6环境变量通常是具有全局属性的
cpp
int main(int argc, char *argv[], char *env[])
{
cout << "I am process, pid: " << getpid() << ", ppid: " << getppid() << endl;
for (int i = 0; env[i]; i++)
{
printf("---------env[%d] -> %s\n", i, env[i]);
}
pid_t id = fork();
if (id == 0)
{
cout<<"----------------------------------------"<<endl;
cout << "I am child, pid: " << getpid() << ", ppid: " << getppid() << endl;
for (int i = 0; env[i]; i++)
{
printf("---------env[%d] -> %s\n", i, env[i]);
}
}
return 0;
}
通过上面这段代码已经结果可以发现环境变量是会被子进程继承下去的,再一次验证了环境变量具有全局属性,可以被所有子进程继承下去。
1.7通过系统调用获取或设置环境变量
getenv()这个函数就能够获取到环境变量,这是一个系统调用的接口。
cpp
int main()
{
const char* username=getenv("USER");
if(username)
cout<<"USER: "<<username<<endl;
else
cout<<"NONE"<<endl;
return 0;
}
2.地址空间
2.1地址空间分布
我们可以使用下面这段代码来验证一下地址空间的分布是否如上图所示。
cpp
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);
printf("init data addr: %p\n", &g_val);
printf("uninit data addr: %p\n", &g_unval);
char *heap = (char*)malloc(20);
char *heap1 = (char*)malloc(20);
char *heap2 = (char*)malloc(20);
char *heap3 = (char*)malloc(20);
static int c;
printf("heap addr: %p\n", heap);
printf("heap1 addr: %p\n", heap1);
printf("heap2 addr: %p\n", heap2);
printf("heap3 addr: %p\n", heap3);
printf("stack addr: %p\n", &heap);
printf("stack addr: %p\n", &heap1);
printf("stack addr: %p\n", &heap2);
printf("stack addr: %p\n", &heap3);
printf("c addr: %p, c: %d\n", &c, c);
}
可以发现地址空间的分布确实是如上图所示。
2.2进程地址空间
通过下面这段代码我们可以发现一个问题,就是在子进程改掉全局变量g_val之后,子进程和父进程的g_val发生了变化,这是正常的,因为进程间具有独立性 ,但是地址确是一样的,那么同一个地址可能存储两个不一样的值吗?这是不可能的,所以这个地址是虚拟地址。
cpp
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt=0;
while (true)
{
printf("I am child, g_val: %d, &g_val: %p\n", g_val, &g_val);
sleep(1);
cnt++;
if(cnt==5)
{
g_val=200;
printf("child change g_val:100->200\n");
}
}
}
else
{
while (true)
{
printf("I am father, g_val: %d, &g_val: %p\n", g_val, &g_val);
sleep(1);
}
}
return 0;
}
虚拟地址没有保存数据的能力,所以数据都存放在物理内存,因此需要通过虚拟内存的地址找到对应的物理内存的地址,所以os会维护一张页表,这张页表有着映射关系,也就是虚拟地址到物理地址的映射,类似于数组的下标与数据的关系。
那么每一个进程运行之后都有自己的进程地址空间,并且在os层面都要有**页表映射结构,**那么子进程在创建出来后会继承父进程的大部分数据,当然包括这张页表,所以我们在上面的测试中能够看到g_val这个变量在子进程和父进程的地址是一样的,因为连映射关系都是一样的。
那么当子进程修改了这个变量时,因为进程具有独立性,为了不影响到父进程,在修改之前os会在物理内存中开辟一段新的空间,将原数据拷贝一份到这个新的空间,这个过程就叫写时拷贝,然后子进程的页表映射关系也发生改变,所以我们能看到g_val这个变量在子进程和父进程的地址是一样的,因为是虚拟内存,但是值却不一样,因为通过页表映射的物理地址不一样。
2.3为什么要有进程地址空间
2.3.1无序到有序
一个程序要放到物理内存当中,那么是可以放在任意位置的,那么就可以通过页表让我们以更有序的视角看到虚拟内存,也就是让进程以统一的视角看待内存,所以任意一个进程,可以通过地址空间+页表将乱序的内存数据,变成有序。
2.3.2安全检查
其实页表旁边还有一个字段叫做访问权限字段,代表着这个虚拟地址到物理地址的访问权限,也就是r和w,在进行越界访问和修改数据时会进行拦截,比如:字符常量不可被修改,就是因为内存被设置了只读权限。这样就可以有效地进行地址访问内存时的安全检查。
2.3.4如何找到进程相应的页表
系统中有那么多的页表,那么如何找到这个进程所对应的页表呢?通过寄存器CR3就能找到这个页表的地址,然后通过页表的映射来找到物理地址 ,那么有一个小问题,那么这个页表的地址是虚拟地址还是物理地址呢?这个地址其实是物理地址,因为虚拟地址是为了给用户来使用的,而这个CR3是系统内部的,没必要用虚拟地址。
2.3.5 进程切换
当一个进程在cpu上运行的时候,CR3里面的内容本质是在该进程的进程上下文当中,也就是说页表的地址是在进程上下文中,那么当切换数据的时候,也要把数据的页表地址保存到进程的上下文中,所以每一个进程都要有自己独立的页表地址的起始数据。所以当进程切换时,要把PCB和进程的页表地址都要进行切换。那么进程的地址空间是在PCB中的,只需要切换PCB,就可以把页表,地址空间,数据全部切换了。
2.3.6 进程挂起
进程挂起在linux中的体现就是当进程正在运行,系统内存已经严重不足,这个进程代码和内存依旧要占空间,但是又不会被调度,那么OS就会把这个进程挂起,那么我们怎么知道这个进程挂起了呢?其实在页表中还有一个字段用来表明这个进程的物理地址是否在内存当中,并且是否有内容。
通过二进制来标记是否分配和是否有内容,如果都为0的话表示没有分配内存并且没有内容,表示这个进程被OS挂起了。
2.3.7 缺页中断
当OS需要访问一个虚拟地址为0x112233的程序时,通过页表查看到标记为是00,代表没有分配内存,并且没有内容,此时OS会强制暂停这次访问,等待在内存中重新开辟空间,将物理地址放入映射,然后修改标记位。那么这个过程就叫做缺页中断。
那么进程切换,进程挂起,缺页中断这些事情进程都是不知道的,都是内存管理,也就是进程地址空间在执行,这样就实现了OS层面上模块的解耦,也就是为什么要有进程地址空间和页表的理由。
今天的分享到这里就结束啦,感谢大家的阅读!