写在前面
这个博客主要谈一下环境变量和程序地址空间,其中程序地址空间可能有点不好理解,但是这个可以帮助我们解决前面我们遗留的一些问题,以后我们几乎都要和程序地址空间打交道,很重要.当然,前面的环境变量也解决了我们的指令问题.
环境变量
在谈这个之前,我们先来看一个例子,引出这个话题.
#include<stdio.h>intmain(){printf("我仅仅是一个 main 函数\n");return0;}
首先我不疑惑结果,这里我就想问一件事,为何我们执行这个程序需要**./**,这一点才是我感觉到不一样的.当然我们平常是这样做的,可能觉得是理所当然,那么我是知道的Linux是用C语言写的,一一些指令的本质就是函数.这一对比就出来,我们的ls就不需要指定路径,为何我们自己写的程序就需要路径.
我们先来看看如果使用路径会怎样,这样我们执行不了.
这里面就涉及到环境变量的知道知识点;了,在Windows环境下,我们也是可以查看自己电脑的环境变量的.我们这里主要就是看看.
env 指令
env是查看系统环境变量的指令,可以帮助我们查看系统所有的环境变量.
[bit@Qkj 08_13]$ env
你会发现这些环境变量实在实在太多了,这里面我们挑几个比较常用的熟悉一下.
PATH
PATH 指定命令的搜索路径 说人话就是我们把一些指令的地址放在PATH种,这个环境变量是我们今天要重点谈的,里面有些可以解决上面我们的疑问.
我们先来看看如何查看PATH.
第一种方法是使用grep指令,不过这个指令得到的不太简洁.
[bit@Qkj 08_13]$ env|grepPATH
第二种就是通过echo\指令来获得,这个倒是很舒服,不过也有要注意的地方.
[bit@Qkj 08_13]$ echo$PATH# 注意 一定要加$
如果上面我们不加
或者
或者
或者和PATH分离都不会得到这个结果,他只会它们当作一个字符串直接打印出来.
PATH是什么
前面我们已经谈过的,PATH可以看做很多串路径的集合,其中一个:作为一个分隔符.
其中我们主要关注的是/usr/bin目录,这个目录放在我们Linux种几乎所有的指令.这里我截出来几个让大家看看.
[bit@Qkj 08_13]$ ls /usr/bin/
到这里我们就明白了,如果我们想要执行某一条指令,操作系统会去PATH里面保存的路径中寻找,找到了就执行,找不到就会出现警告,我们 之前学的which指令的本质也是去PATH保存的路径中寻找.
修改PATH
我们希望可以把自己写的程序可以直接运行,不用在指明什么路径。这里我们存在两种方法,这里我推荐第二种。
- 把可执行程序直接拷到 /usr/bin/目录
- 修改PATH
我们先来看第一种方法,这里我们需要超级用户权限
#include<stdio.h>intmain(){printf("我仅仅是一个 main 函数\n");return0;}
[root@Qkj 08_14]# cp mybin /usr/bin/
但是这种方法有一个弊端,就是你这个程序已经是不变的了,如果你要改变,还要再次拷贝才可,我们不建议这么用.
#include<stdio.h>intmain(){printf("我仅仅是一个 main 函数\n");printf("我仅仅是一个 main 函数\n");return0;}
第二种方法就比较简单了,我们可以直接修改PATH的值,这样的话就可以知道找到我们对应的程序了
如果我们要是直接修改PATH,你就会发现我们一些指令不能用了,因外OS在PATH路径中找不到我们用的指令.不过也不用担心,我们退出下用户再次进入就可以了,这也是因为OS会再次给PATH赋值.
所以一般情况下我们都是给PATH加上路径,很少删除路径的,这里我们也是加上路径.
[bit@Qkj 08_14]$ exportPATH=$PATH:/home/bit/104/2022/08_14 # export 后面再说
这样的话我们就可以直接运行自己的可执行程序了
#include<stdio.h>intmain(){printf("我仅仅是一个 main 函数\n");return0;}
设置环境变量
我们在想是不是自己可以设置环境变量,毕竟后面我们也可能使用到.Linux是允许我们自己设置的,不过在设置之前我们需要看一下本地变量
本地变量
我们经常拿本地变量和环境变量进行比对,这里我们先不说它们原理,只谈用法.设置本地变量的方法是很简单的.
[bit@Qkj 08_14]$ aaa=12345
上面的aaa就是一个本地变量,本地变量是不会放在环境便里面的,也就是我们env是查不出本地变量的.
设置环境变量
上面我们谈过了如何设置本地变量,这里需要看看环境变量是如何设置的,我们要用到上面的一个指令.export的作用就是设置一个新的环境变量 .
[bit@Qkj 08_14]$ exportbbbb=111222333
当然我们也可以把自己设置的本地变量导到环境变量里面
[bit@Qkj 08_14]$ export aaa
unset 指令
这是清除环境变量的指令.
[bit@Qkj 08_14]$ unset bbbb
set 指令
我们知道了env是不不能产看本地变量的,这里的set却可以查看它们两个.set显示本地定义的shell变量和环境变量.我们这里只演示查看本地变量的方法.
[bit@Qkj 08_14]$ set|grep aaa
常见的环境变量
下面我们看一下比较常见的一些环境变量,有助于理解Linux的指令.
HOSTNAME
产看用户的主机名称.
[bit@Qkj 08_14]$ echo$HOSTNAME
HOME
查看家目录,这里我们用root用户和普通用户做对比,你一看就会明白了为何cd ~会自动跳到自己的家目录.
[bit@Qkj 08_14]$ echo$HOME[root@Qkj 08_14]# echo $HOME
PWD
这个环境变量实时记录当前位置的绝对路径,和我们之前的pwd命令是有联系的,可以认为pwd就是取出了这个环境变量的值.
[bit@Qkj 08_14]$ echo$PWD
后面还有一点,这里我就不演示了,直接出截图吧.
通过代码如何获取环境变量
这个也算是我们今天的主要内容吧,不过他衍生出来的知识才是比较重要的.
main函数可以带参数吗
我们心里想这不是废话吗,我们写了这么长时间的main函数,是一次都没有带过参数,肯定也是不能带参数的.如果你要是这么认为,那么你的C语言只能算是掌握阶段,但是一些边边角角的知识还是没有掌握的,这里我告诉大家,main函数不仅能带参数,而且还能带两个(目前结论).我们测试一下你就知道了.
#include<stdio.h>intmain(int argc,char*argv[]){return0;}
我们先来看看这两个参数是什么,这样才有利于我们理解后面的知识.
#include<stdio.h>intmain(int argc,char*argv[]){int i =0;for(i=0;i<argc;i++){printf("argv[%d] : %s\n",i,argv[i]);}return0;}
这里面我来解释一下,这两个参数究竟是什么.
倒着里你就明白了这两个参数的含义,这里面的字符串可以看作用空格隔开的,其中./可执行程序也算是一个字符串.
指令的选项
这里我们就可以简单的明白一些东西了,我们前面说过大多数的指令都是函数,这里的指令的选项和我们现在所看到是多么的像.我们也可以简单的理解这些指令的函数也是这么实现的.
可以给无参的函数传参吗
这也是一个问题,我们在之前些=]写main函数时从来没有写过参数,这里打破了我们的认值.那么这还有一个打破认值的知识,我们可以给无参的函数传入参数吗,这里是可以的,反正我们函数不接受就可以了.
#include<stdio.h>voidfunc(){printf("func()\n");}intmain(int argc,char*argv[]){func(1,2,3);return0;}
通过代码如何获取环境变量
这里我们要谈一下如何通过代码来获取环境变量,这里一共有三种]方法.
- 命令行第三个参数
- 通过第三方变量environ获取
- 系统调用获取 getenv
main函数的第三个参数
是的,这里面我们更新一下自己的结论,main函数可以带入第三个参数,这第三个参数就是我们的环境变量.
#include<stdio.h>intmain(int argc,char*argv[],char*env[]){int i =0;for(i=0;env[i];i++){printf("env[%d] : %s\n",i,env[i]);}return0;}
从这里我们就可以得到一个结论,环境变量是被传入的进程里面的,这一点是十分重要的.
通过第三方变量environ获取
这个变量是一个全局变量,它是指向我们main函数第三个参数的指针.
我们来看一下他的作用
#include <stdio.h>#include <unistd.h>
int main(){
extern char** environ;
int i =0;
for(i=0;environ[i];i++){
printf("environ``[%d]: %s\n",i,environ[i]);}return0;}
系统调用获取
上面的两种方式获取的是全部,现在这个方法是指定哪个获取哪个.
#include<stdio.h>#include<stdlib.h>intmain(){printf("%s\n",getenv("PATH"));return0;}
再析环境变量 & 本地变量
我们前面只是认识了本地变量和环境变量,但是我们还不知道它们的具体区别,这里我们演示一下。
bash
前面我们已经谈过了,在Linux中,几乎所有指令对应的进程的父进程都是bash.
intmain(){while(1){printf("hello world pid : %d,ppid : %d\n",getpid(),getppid());sleep(2);}return0;}
这里我们就要如果我们kill的bash,你就会发现Linux不能用了,原因就是命令行启用的进程,父进程都是bash,既然父进程没了,那么子进程又如何创建呢,我这里是直接退出来来了,不过有点系统是指令不能执行.
环境变量通常具有全局属性
这里我们先下一个结论,环境变量具有全局属性,它可以被子进程给继承,那么也就能被子进程的子进程所继承。这一点我们演示一下就可以了.
我们来解释一下,环境变量和本地变量都是存储在bash进程中,这一点我们先暂时这样理解,那么main函数所在的进程也是bash的子进程,但是确可以进程环境变量.
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){while(1){printf("hello world:%s\n",getenv("pit_104"));sleep(2);}return0;}
本地变量不能被继承
这里我们就可以演示一下,本地变量是不能被子进程继承的,他只能存在bash中.
内建命令
我们这里就有一个问题了,你说本地变量不能被继承,还说了在Linux中几乎所有的进程的指令是bash的子进程,那么下面的指令为何可以执行.
[bit@Qkj 08_14]$ set|grep pit_104
要知道我们的set可是bash的子进程,你不是说子进程拿不到本地变量吗,这里为何又拿到了?这不是矛盾了吗?注意了,这里我要补充一个结论,在Linux中几乎所有的指令都是按照子进程的形式来完成的,但是仍存在一下指令是通过bash调用自己的函数来完成某些功能的,我们把这类命令称为内建命令.
地址空间
这个模块很难理解,主要分为两种,当然本质是一种,名字不同罢了.
- 程序地址空间
- 进程地址空间
程序地址空间
我们先来简单的,在C语言中我们是学过的C程序虚拟地址空间的,这里我们要详细的谈一下.我们在C语言中学过指针,也学过变量在内存中的存储,今天我们就要看看内存的存储的实际情况.
内存为下面几个模块,这张图叫做虚拟地址空间,我们用代码验证一下实际的存储是不是和我们想的一样,这里建议在Linux环境下验证,VS可能做了优化.这里注意一下,共享区这里不好验证,就不验证了.
#include<stdio.h>#include<stdlib.h>int g_val;int init_g_val =1;intmain(int argc,char* argv[],char* env[]){printf("code : %p\n",main);printf("init data : %p\n",&init_g_val);printf("uninit data : %p\n",&g_val);char* array =(char*)malloc(10);printf("heap area : %p\n",array);printf("stack area : %p\n",&array);printf("命令行参数 : %p\n",argv[0]);printf("环境变量 : %p\n",env[0]);free(array);return0;}
堆区 & 栈区
我们都知道堆区先上增长,栈区线下增长,这里演示一下就可以了.
#include<stdio.h>#include<stdlib.h>intmain(){char* arr1 =(char*)malloc(10);char* arr2 =(char*)malloc(10);char* arr3 =(char*)malloc(10);char* arr4 =(char*)malloc(10);printf("heap area : %p\n",arr1);printf("heap area : %p\n",arr2);printf("heap area : %p\n",arr3);printf("heap area : %p\n",arr4);printf("stack area : %p\n",&arr1);printf("stack area : %p\n",&arr2);printf("stack area : %p\n",&arr3);printf("stack area : %p\n",&arr4);free(arr1);free(arr2);free(arr3);free(arr4);return0;}
static修饰的局部变量
我们也知道static修饰的局部变量在生命周期被放大了,实际上这个变量是存储在全局变量区.
#include<stdio.h>#include<stdlib.h>int g_val;int init_g_val =1;intmain(){printf("code : %p\n",main);printf("init data : %p\n",&init_g_val);printf("uninit data : %p\n",&g_val);char* array =(char*)malloc(10);staticint a =10;staticint b;printf("init static : %p\n",&a);printf("static : %p\n",&b);printf("heap area : %p\n",array);printf("stack area : %p\n",&array);free(array);return0;}
进程地址空间
我感觉上面的程序地址空间应该叫做进程地址空间,这个概念是系统层次的概念.我们先来看一下下面的的代码,你就会明白我们为何这么说了.
#include<stdio.h>#include<unistd.h>intmain(){int val =10;pid_t id =fork();if(id ==0){// childwhile(1){printf("我是子进程 pid : %d,ppid : %d &val : %p\n",getpid(),getppid(),&val);sleep(1);}}else{while(1){printf("我是父进程 pid : %d,ppid : %d &val : %p\n",getpid(),getppid(),&val);sleep(1);}}return0;}
他们的地址一样我不感到惊讶,毕竟上面我们说过父子进程共用同一片代码和空间,这里地址也是应该相同的,但是下面你就会感到疑惑了.
这里我们修改一下子进程里面val的值,你就会发现一个难以理解的东西.
intmain(){int val =10;pid_t id =fork();if(id ==0){// childwhile(1){
val =20;printf("我是子进程 val = %d &val : %p\n",val ,&val);sleep(1);}}else{while(1){printf("我是父进程 val = %d &val : %p\n",val,&val);sleep(1);}}return0;}
程序地址空间是内存吗
从上面的代码我们就可以知道,这里好象出现了一种矛盾.我们在C语言中学过,指针指向的数据肯定是唯一确定的,我们在同一片空间怎么会出现两个不一样的数据,除非我们中程序地址空间是假的,不是真正的内存.实际上也是如此,我们得到的地址绝对不是真实的物理地址,它是虚拟地址,在Linux中还可以称为逻辑地址或者线性地址.程序地址空间可以看作是实际内存通过一种美化手端得到的.
这里我们就疑惑了吗,既然程序地址空间不是内存,那么我们就疑惑OS为何不让我们看到真实的内存.这是由于内存只是一个硬件,它可不会主动做某件事事,只能被动的被动的进行写入和读取.
mm_struct
在Linux中,我们每创建一个进程,首先第一点就是实例化一个task_struct,同时我们给这个进程创建一个进程地址空间,这个地址空间就是我们上面说的进程地址空间.
这里我们就可以知道,每一个进程都有一个自己的进程地址空间,既然每一个进程都有一个属于自己的地址空间,OS就会把他们管理起来,具体来说就是先描述后组织.这个的描述就是一个mm_struct,也就是地址空间就是mm_struct.
什么是地址空间
前面的准备工作做的差不多了,这里我们先来解释一下什么事地址空间.我们前面说过进程具有独立性,体现在在数据和代码等资源的独立.
这里我们举一个例子.假如有一个很有钱的富翁,有十个亿的存款.这个富翁有三个私生子,这三个孩子互相不认识,都认为自己是这个富翁的唯一子嗣.这就是体现出了独立.有一天,这个富翁叫过来大儿子,告诉他,儿子,你好好的学习,等我去世了,这十个亿都是你的,后面他又把二儿子和三儿子都独立的叫过来,说了相应的话.也就是说这个富翁给这三个儿子画了一张大饼,他们都认为自己可以得到这十个亿.一天,大儿子过来说要一千块钱要买东西,后面其余的孩子都找过富翁要过钱,富翁都是很爽快的给了,这就有进一步加深了每个孩子可以可以得到这个十个亿的信心.
这里的十个亿就是我们真实的物理内存,富翁就是操作系统,那么孩子就是进程,富翁给孩子画饼就像OS给进程画饼,让每个进程都以为自己可以得到所有的物理内存,其中这个大饼就是地址空间.地址空间是一个抽象的概念,让每一个进程都认为自己可以得到所有的物理内存,而且只要进程需要,我们就给进程空间,那么对于进程来说,他好象是拥有了所有的资源.
页表
到这里我们就可以看一看一个可执行程序是如何变化成一个进程的,且还需要看看什么是页表.
我们可以把页表理解成一个函数,其中虚拟地址作为参数,结果作为真实地址.
这里我们先来说一下过程,我们执行一个进程,第一件事实在真实的物理内存开辟出资源,然后进程出现task_strcut,其中task_strcut中有一个指针,这个指针指向的就是地址空间,然后物理内存和地址空间里面出现一个页表(注意,这可能不是真实的顺序,主要是为了好理解).这是一个粗糙的流程.
区域
在能够谈清楚上面的过程前,我们需要看一下什么是区域.假设在一个小学中,一个爱干净的女生和一个不修边幅的男生是同桌.一天女生实在是看不下去男生的邋遢了,就在桌子上划了一条三八线,告诉男生不要越界.
我的地址空间可是划分了很多区域,那么他们也是应该按照这个流程来的,我们的访问越界就像是男生不小心把胳膊伸在了女生区域.既然有区域,那么肯定会有一个start和一个end来表述这块区域.我们心里像的应该是是这样的.
我们去源码里面看看是不是确实存在这种类似的结构,这个确实存在的,他们变成了一个链表.
程序是如何变成进程的
到这里我们还有两个问题没有解决,只要解决这两个问题,我们今天的进程的原理差不分享了三分之二.
可执行程序存在指针吗
我们先来谈一下,到底我们的道德.exe文件里面在没有被运的时候究竟有没有指针.这里我直接给出答案,是的,存在的.要知道我们进程在运行之前是存在一个看一个链接步骤,这个步骤如果我们要调用库函数,肯定是要拿到函数的地址的,所以可执行程序是存在指针的.
可执行程序有区域吗
可执行区域是由区域的当然这里没有堆区和栈区,这是运行的时候才会有的,不过它存在其他的区域.我们吗,我们用指令看看就明白了.
[bit@Qkj 08_15]$ readelf -S mybin
现在我们也可以知道了,可执行程序里面也是存在区域的,而且这个区域的初始部分是从0开始,他们的是相对地址,也成为逻辑地址.
程序是如何变成进程的
现在我们就可以舒服的谈程序变成进程的过程了.首先我们在物理内存给这个可执行程序开辟好空间,这里也会发生一个地址的转变,要知道我们的可执行程序里面的地址是相对地址,这里到真实的地址需要加上某个值进行合理的转换才可以在内存中得到真实的地址.
这时候进程开始执行了,操作系统实例化一个task_struct,了这个对象里面有一个指针,指向进程空间.
这个时候我们需要把虚拟地址课真实的地址链接起来,这样就需要页表了,
写时拷贝
现在我们就可以解释一下为何一地址会出现两种不一样的值了,这里涉及到写时拷贝,我们需要把前面的代码拿出再说一下.
#include<stdio.h>#include<unistd.h>intmain(){int val =10;pid_t id =fork();if(id ==0){// childwhile(1){printf("我是子进程 pid : %d,ppid : %d &val : %p\n",getpid(),getppid(),&val);sleep(1);}}else{while(1){printf("我是父进程 pid : %d,ppid : %d &val : %p\n",getpid(),getppid(),&val);sleep(1);}}return0;}
我们这里父子进程拿到的val变量进程空间的地址的一样的,那么对应的物理内存是不是一样的呢.?这里面的结果是的,至于为何是这样的机制,我们后面在谈.
首先子进程肯定会创建task_struct,进程空间,页表,但是这是时候OS不会再次位子进程在物理内存中为对应的变量才开辟空间,这也是我们之前说的代码共享.这里还要提一嘴,子进程的task_struct,进程空间,页表大部分数据都是进程父进程的来的,所以开始是指向同一片空间的.
写时拷贝
但是下面的代码就不一样的.这时候OS就会从新给变量开辟空间,页表中虚拟地址空间的部分不变,真实部分地址指向发生变化.这就是我们看到用一片空间,却出现两个不同的值的原因.
intmain(){int val =10;pid_t id =fork();if(id ==0){// childwhile(1){
val =20;printf("我是子进程 val = %d &val : %p\n",val ,&val);sleep(1);}}else{while(1){printf("我是父进程 val = %d &val : %p\n",val,&val);sleep(1);}}return0;}
我们这里要总结以上,上面我们遇到的情就是写时拷贝,如果我们不修改子进程中对应的数据,代码等等,这时候两者共用同一片真实物理空间,一旦子进程中数据代码等发生了变化,这时候操作系统会会重新开辟空间,并且改变子进程的页表.
这里就可以解释fork为啥会有两个返回值了,记住咋在fork中,我们return了两次,对于每一个,也就是我们子进程会开辟出一块新的空间用来接受返回值.
写时拷贝的意义
这里我们需要谈谈为何我们选择这样的机制,我们不可以为子进程开辟新的空间吗?可以的,但是是不时会有的这样的情况,假设我们的子进程不会修改数据和代码,他只是观察一下这些东西,如果我们的父进程占据的空间很大,我们这个子进程作占据的空间就浪费了,这里还只是两个进程,后面我们遇到的进程个数会更多,到时候浪费的会更多.
地址空间意义
这里我们需要谈一下地址空间为何要这样的设计.这里面的有三个理由.
越界访问
假设我们是直接访问物理内存,我们假设一个场景.两个进程连在一起,其中进程一出现了越界,他不小心的把进程二的数据给改了,要是我们出现报错还好,要是没有报错等着整个系统出现bug吧.
统一标准
虚拟地址的出现可以让进程认为自己独占整个内存,这样的话我们可以统一标准,简化进程本身的实现.编译器可以跟好的提供效率.
版权归原作者 玄鸟轩墨 所有, 如有侵权,请联系我们删除。