✨前言✨
📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2024年7月24日
📨 博主码云地址:渣渣C
📕参考书籍:C语言程序与设计 和 数据结构(C语言版)
📢编程练习:牛客网+力扣网
1 环境变量的概念
~~~~
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
!例如:在Windows环境下,我们如果要使用Python语言,就必须先安装Python解释器,并且我们还必须配置对应的环境变量!配置环境变量的目的就是为了让我们编译代码的时候,就可以去找到编译器所在的位置,从而可以进行代码的编译工作!或者是在我们编译C/C++代码的时候,进行动态库的链接时,它是如何去找的,也是配置了对应的环境变量!
1.1 环境变量的有关命令
echo $NAME ~~~~ //NAME就是环境变量的名字,用来查询环境变量的内容
~~~~
常见的环境变量的名字如:HOME,PATH,USER,PWD,如下所示,我们可以查询PATH这样的一个环境变量!
~~~~
我们除了使用echo命令来查询环境变量,还可以调用系统接口getenv()来打印我们环境变量的内容!通过man命令来查看接口是如何使用的!
~~~~
我们就可以通过如下的程序进行查询环境变量
1 #include <stdlib.h>
2 #include <stdio.h>
3 int main()
4 {
5 printf("PATH:%s\n",getenv("PATH")); //PATH可以替换成任意一个环境变量的名字
6 return 0;
7 }
env ~~~~ //查看全局的环境变量
set ~~~~ //显示本地shell局部变量和全局的环境变量
~~~~ 由于本地shell(就是当前开启的shell窗口)可能会设置一些当前shell要用到的一些局部变量,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量!所以一般set查询到的环境变量会比env查到的要更多!
export NAME=$NAME ~~~~ //将NAME设置成一个全局的环境变量!
NAME=$NAME ~~~~ //设置一个局部变量,例如test = 123
~~~~ 例如:export PATH=$PATH:要添加的路径,这个命令的意思就是将要添加的路径信息加入到对应的PATH环境变量!这样子做我们就可以像使用shell命令一样,前面不用加上./也可以直接执行对应的可执行程序了,如果没有 $PATH:,那么此时在本地shell中的路径就会被要添加的路径所覆盖(此时还原的方法就是关闭shell,重新打开就可以恢复成默认的路径了)
unset NAME ~~~~ //清除某个变量
~~~~
实际上对于环境变量,/etc/profile 是每个用户登录时都会运行的环境变量设置文件,当用户登录时,该文件被执行。对所有用户有效。所以通过读取这个文件,我们就可以知道,当前用户是谁,有那些环境变量!在Linux中,环境变量的配置文件如下所示:
~~~~
1️⃣/etc/profile (建议不修改这个文件 )是全局(公有)配置,不管是哪个用户,登录时都会读取该文件。
~~~~
2️⃣/etc/bashrc (一般在这个文件中添加系统级环境变量)是全局(公有)配置,bash shell执行时,不管是何种方式,都会读取此文件。可以设置系统提示符 PS1等。比如 PS1=\h:\W \u $ 那么提示符的格式就是:主机:当前目录 用户名$。
~~~~
3️⃣~/.bash_profile 或者 ~/.profile (一般在这个文件中添加用户级环境变量)是每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!
1.2 main函数的参数
~~~~ 事实上,我们的main函数是可以带参数的!如下所示:
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 int main(int argc,char* argv[])
5 {
6
7 if(argc!=2)
8 {
9 printf("你必须选择一个选项:-a or -b\n");
10 return 1;
11 }
12
13 if(strcmp(argv[1],"-a")==0)
14 {
15 printf("this is funtional one\n");
16 return 0;
17 }
18
19 if(strcmp(argv[1],"-b")==0)
20 {
21 printf("this is funtional two\n");
22 return 0;
23 }
24 }
argc代表的是参数的个数(命令算是第一个参数),argv存储的就是字符数组,就是把参数当做字符串存储进这个字符数组!
通过上述,我们就可以明白bash命令带参数的工作原理了,本质也是将带参数的main函数打包成可执行的程序进行使用!图解如下所示:
也就是说bash会根据我们所输入的命令参数,利用空格作为分隔符,记录有几个参数,然后将参数放到对应的argv字符数组对应的位置上去!通过带参的main函数我们就可以实现同样的命令使用不同的参数可以有不同的功能!事实上,main函数还可以带第三个参数,就是环境变量参数!
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 int main(int argc,char* argv[],char* env[])
5 {
6 for(int i = 0;env[i];i++)
7 {
8 printf("env[%d],%s\n",i,env[i]);
9 }
10 }
运行结果如下所示:
env就是环境变量参数的数组,它的原理和之前说过的main函数带参是一样的,系统会有给指针environ指向这个数组,这里环境变量太多,只取其中一部分拿来表示其原理图:
所以每个程序都会有这样一张环境参数表!从而使用我们的环境变量!但是在父进程的全局环境变量是可以被子进程所继承的!
2 进程地址空间
我们之前在C++中学习内存分布的时候,把内存分为了堆区与栈区,以及静态区,常量区,如下图所示:
我们可以利用C语言代码来进行相应的验证,也可以知道堆是向上增长的,栈是向下增长的,代码如下所示:
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <sys/types.h>
6
7 int gval = 100;
8 int ungval;
9 int main(int argc,char* argv[],char* env[])
10 {
11 const char* str = "hello world";
12 static int val = 20;
13
14 char* heap1 = (char*)malloc(sizeof(char));
15 char* heap2 = (char*)malloc(sizeof(char));
16
17 printf("code adr:%p\n",main);
18 printf("init val adr:%p\n",&gval);
19 printf("uninit val adr:%p\n",&ungval);
20 printf("heap1 adr:%p\n",heap1);
21 printf("heap2 adr:%p\n",heap2);
22 printf("stack1 adr:%p\n",&heap1);
23 printf("stack2 adr:%p\n",&heap2);
24 printf("static adr:%p\n",&val);
25 printf("const char str:%p\n",str);
26 printf("&argv adr:%p\n",&argv);
27 for(int i =0;argv[i];i++)
28 {
29 printf("argv[%d] adr:%p\n",i,argv[i]);
30 }
31 printf("&env adr:%p\n",&env);
32 for(int i =0;env[i];i++)
33 {
34 printf("env[%d] adr:%p\n",i,env[i]);
35 }
36 }
实际上这个空间我们将它称为进程地址空间,而不是真正意义上的内存物理空间,下面这段代码就可以证明我所说的这一结论!
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <sys/types.h>
6
7 int gval = 100;
8
9 int main()
10 {
11 pid_t id = fork();
12
13 if(id == 0)
14 {
15 gval = 10;
16 printf("child pid:%d,gval=%d,gval adr:%p\n",getpid(),gval,&gval);
17 }else if(id>0){
18 printf("parent pid:%d,gval=%d,gval adr:%p\n",getpid(),gval,&gval);
19 }
20 return 0;
运行结果如下所示:
~~~~ 通过上述代码,我们就可以知道,这一定不是物理内存空间,这是因为物理地址一样,值怎么可能等于10的同时又等于100呢?事实上,这就是我们的进程地址空间,也被称为虚拟物理地址!那么进程地址空间也是要被操作系统管理起来的,和之前进程的认识一样,也是先描述在组织,也是通过一种数据结构将我们的进程地址空间管理起来!在我们的PCB中就会有一个指针指向进程地址空间这个数据结构,同样的进程地址空间的数据结构采用如下的方式描述的(事实上Linux中的源码也是采用这种方式来进行描述的):
struct MM_struct
{
int heap_begin;
int heap_end;
int stack_begin;
int stack_end;
…………………………
}
实际上,物理进程地址与内存物理空间是通过一张页表联系起来的!两者之间是存在映射关系的!对于上述的现象,可以用下图来表示:
~~~~
结合我们之前学过的进程,我们可以知道,父子进程之间数据与代码是共享的,也就是说子进程会拷贝一份父进程的PCB中的部分内容以及页表内容,以及虚拟地址表(以上三种内容每个进程其实都有的),那么是如何保持进程之间的独立性的呢?就是如上图所示,当发生写时拷贝的时候(子进程修改了数据),我们就会拷贝一份父进程的内容,然后在拷贝的位置上对数据进行更改,也就是内存位置确实发生了变化,这样就会保证父进程与子进程之间是具有独立性的!但是页表部分也会更改内存地址,虚拟地址没有改变。所以也就会发生上述的现象了,如果没有写时拷贝发生(子进程或者父进程没有改动数据),那么此时父子进程指向的数据是在同一个物理内存!
2.1 地址进程空间存在的意义
~~~~
在进程地址空间中,我们需要明白的是,申请空间首先就是在虚拟地址空间上申请一个空间,而不会立即向内存申请空间,包括我们平时在写代码,进行编译的时候,也是向虚拟地址申请空间!只有我们需要用到内存的时候,页表首先发生缺页中断,去内存开辟空间然后将映射关系记录在页表,等再次请求的时候,就可以访问数据与代码了!这样子做有什么好处呢?这样就可以保证内存不会被浪费掉,并且提高new和malloc的申请速度!
~~~~
此外进程地址空间让内存从无序变得有序,让进程从统一的视角看待内存!并且将其分为进程管理与内存管理解耦合(就是各自管理,双方的依赖性就降低了),如下图所示:
地址空间加页表也可以为了内存安全,因为地址空间和页表是OS创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问也便保护了物理内存中的所有合法数据,包括各个进程以及内核的相关有效数据!
~~~~
本文主要学习了环境变量的概念以及如何使用环境变量,以及环境变量和命令行参数原理,本质就是带参数的main函数!也学习了什么才是进程地址空间,进程地址空间与内存空间的联系等!