文章目录
- [Linux 进程篇 (四)](#Linux 进程篇 (四))
-
- [1. 环境变量](#1. 环境变量)
-
- [1.1 概念介绍](#1.1 概念介绍)
- [1.2 命令行参数](#1.2 命令行参数)
- [1.2 环境变量](#1.2 环境变量)
- [1.3 认识更多环境变量](#1.3 认识更多环境变量)
- [1.4 操作环境变量的方法](#1.4 操作环境变量的方法)
- [1.5 理解环境变量](#1.5 理解环境变量)
- [2. 进程地址空间](#2. 进程地址空间)
Linux 进程篇 (四)
1. 环境变量
为什么要在进程篇里面讲环境变量呢?对后面做铺垫用的。
1.1 概念介绍
环境变量一般是指操作系统中用来指定操作系统运行的一些参数。这个一般是bash直接使用,用户间接得通过bash使用,方便。
比方说,我们在编译运行这个C/C++程序的时候,在链接的时候,我们并没有指明,或者是我们不知道动静态库在哪里,但是我们依然可以链接成功。原因就是因为这个动静态库的路径写在环境变量里面,编译器可以直接查找得到。
环境变量通常有一些特殊的用途,在系统中有全局特性。
windows下面也有环境变量,在使用vscode配置gcc编译器的时候,编译器路径就是写道环境变量里面去的,还有,配置java的时候,也是要把jdk安装的路径写到环境变量里面的一个叫什么javahome的环境变量里面。

这就是windows下的环境变量。
1.2 命令行参数
有了上面对环境变量的一个初步认识以后,我们再来聊聊命令行参数。
我们在运行一个可执行程序的时候,是可以加上一些命令选项的,这些就是命令行参数。
有这样一个问题,我们写的C/C++程序里面,main函数有没有参数。我们平时一般不写,但其实是有的,或许大家见过这种写法:
c
int main(int argc, char *argv[])
{
.....
}
这两个就是main函数的参数,argv就是命令行参数列表,argc就是命令行参数的个数。
那么下一个问题就来了,main函数之前我们说是程序的入口,那么谁给它传参呢?其实,main 函数是我们程序的入口,而不是整个程序的入口,在我们进行编译的时候,编译器会对我们的程序的头和尾巴进行一些处理,加上一些东西。所以,程序真正启动起来,main函数是要被其它函数调用的 Windows下有一个叫CRT-start的函数是调用 main函数的,linux也有是_start函数来调用main函数。
所以,我们在运行程序的时候,加上的命令行参数,就会被传参给main函数,放到argv[]里面,这个指针数组的最后一位自动就是空指针,把这些参数个main函数以后,main进行相关的操作,验证一下:


如图,这样,我们就证明了,我们传进去的命令行参数,会被传参给这个 main函数。argv[]的最后一位被设置成NULL
而且,我们的第一个./code其实也是命令行参数。
但是,有没有想过,我们传进去的abcd,是谁或者说怎么被切分出来的?答案很简单,就是 bash 干的。我们自己输入的命令,父进程都是bash,之前也提到过,输入其实就是输入给bash,bash做切分,然后传给子进程,也就是我们的code可执行程序。
bash如何切分和维护呢? bash 里面其实有一张表, 命令行参数表,它把切分后的命令行参数,放到这个表里面,就是argv[].

main函数的命令行参数,就是实现程序不同子功能的方法,这也是指令选项的实现原理。比如说我们常用的ls -a -l等等,
之前提到过linux指令都是C语言写的嘛。
其实, main函数还有第三个参数,是什么呢?环境变量表,这里先抛出来
c
int main(int argc, char* argv[], char* env[])
{
.....
}
1.2 环境变量
举个例子,有没有发现我们在运行我们自己写的程序的时候,要加上一个./, 比如说 ./code, 但是我们之前说过,linux里面的命令也是C语言写的程序,为什么我们使用的时候就不需要加路径?
运行一个程序,必须先找到他,这也是我们要加./的原因,那么系统怎么找到指令的路径的呢?
就是通过环境变量:PATH,谁来找这个路径呢? 还是bash, bash通过PATH这个环境变量来找到指令的路径。

如图,可以看到打印一下 PATH 的内容就可以看到里面有很多的路径,路径与路径之间用:隔开。
而我们知道这个ls在这个user/bin目录下,我们也可以在里面找到。
系统里面,找命令的路径,默认就去PATH里面找。如果,我们把我们自己的code文件放到user/bin目录下,我们在运行的时候同样也不用加上路径了。
但是,不推荐把自己写的东西放到系统路径下,因为没经过测试,有很多不稳定性,说不定直接爆破系统了。
我们把所有的环境变量调出来看看,用到的就是env这个指令:

那么这些都是环境变量。
我们也不难看出,环境变量的格式就是 名称=内容, 中间没有空格。
那么我们如何理解环境变量呢?从存储的角度来看,类似于我们之前的命令行参数, 在bash里面同样存在着一张环境变量表,结尾依然是NULL结尾。 有一个默认的二级指针:
c
char** environ
指向的就是这个表表头,也就是第一个元素的地址。

所以,一个bash里面有两张表,一张环境变量表,一张命令行参数表。
环境变量是从哪里来的呢? 是从系统的相关配置文件中来的。

可以看到,在家目录下,有两个文文件一个叫 .bashrc 一个叫 .profile (有的系统里面叫.bash_profile)着两个就是环境变量的配置文件,在系统启动给用户分配bash的时候, 环境变量表就通过这两个配置文件在bash里面了。
我们打开这两个文件可以看到,.profile是指向了.bashrc, .bashrc里面指向 etc/bashrc


这就是etc目录下,可以看到有一个bash.bashrc。
所以,如果今天十个用户登录这个Linux系统,那么系统就会分配10个bash。
1.3 认识更多环境变量
还记得我们在 cd 的时候 cd ~ 就可以回家目录,为什么?


因为有环境变量 HOME ,用这个指令的时候这个 ~ 就回被替换成这个HOME的值。
那当我们使用 cd - 就可以返回上一级目录,以及使用 pwd 就可以打印当前目录,为什么?


当我们使用操作系统的时候,操作系统怎么知道我们是谁的?

还有,比如说,这个,

logname 就代表我们登录的时候是哪个用户登录的,
包括还有这些画框框的,

hostname是主机名, shell 是 bash, histsize 是保留的历史命令最大值等等。
1.4 操作环境变量的方法
之前我们用到过,查看环境变量:
1.env 查看所有环境变量;
2.echo $XXXX 查看单个环境变量。
那么,我们如果想导入环境变量怎么做的呢? export + 环境变量=...


一会儿还会再提到。
如果取消环境变量 unset + 环境变量名


如果,我们想通过代码获取环境变量呢?
还记得我们之前提到过, main函数的第三个参数吗?
c
int main(int argc, char* argv[], char* env[])
第三个参数就是环境变量表,同样是以NULL结尾。那么这个环境变量表是谁的?谁调这个main函数是谁的, 也就是说环境变量表是父进程的。
我们之前提到过,linus里面,调main函数的是_start,所以,_start里面应该是:

我们main函数里面三个参数可以不传,或者传两个,传三个,其实是会自动识别的,所以我们使用的时候无感。我们传进去也是字符,编译器会自己进行词法和语法分析,然后自己选择怎么传参。
由上述例子看出,环境变量可以被子进程继承!继承给孙子,曾孙当然也没问题。
之前我们提到过,我们运行的可执行程序,父进程都是bash,所以,大家都是继承的bash的环境变量,也就是这样。

包括之前提到的export,自己导的环境变量,可以被自己的子进程进程。
所以,我们可以说,环境变量具有全局特性。
这里插播一下,当我们在写C/C++程序的时候,有些变量定义出来没有使用,编译器可以回告警,这是我们使用一下它就好了,比如强转一下,但是什么都不做
那么除了main函数传参,从父进程那里拿到,还有没有别的办法呢?当然是有的getenv(), 是C语言封装的一个函数,

我们传入一个环境变量名,它给我返回一个对应的环境变量的内容的起始地址,也就是那个字符串。拿不到就返回NULL.
当然,我们也可以根据这个getenv,写出一个只有我能正常执行的程序。"USER"。
子进程就可以根据bash传下来的环境变量,毕竟我是谁只有bash知道。
所以,为什么父进程要继承环境变量给子进程呢?就像上面的例子,使子进程可以进程个性化的操作。
这个环境变量,默认使共享的,当然,如果子进程想改,会发生写诗拷贝。
还有第三个方法:
c
char** envrion
这个指针,在程序编译的时候,指针就会指向环境变量表的开头。

这个变量使全局的,使用的时候只需声明一下,extern一下就好。 包含在头文件 <unistd.h> 里面。
有了这个,就可以像数组一样访问环境变量了。
说实话,不是很推荐第一个和第三个,因为是直接获得一个列表。
1.5 理解环境变量
环境变量具有全局特性,这个之前也说过,原因也有,就是为了让子进程能够进行个性化的操作,当然也可以使用类似flag来让父进程达到控制子进程的效果。算是一种进程之间的交流。进程间参数传递就是利用的环境变量有全局性。
补充两个概念,刚才我们一直在谈环境变量,但其实,我们的shell还支持本地变量定义,
什么是本地变量?

我们看到,我们可以这样直接在shell命令行里面定义变量。这个变量就叫本地变量。如果我们想查看它,env 查不到,
我们可以set一下,这样出来的是环境变量和本地变量。

本地变量不可以被子进程继承,就是父进程里面自己用的。而我们的父进程最多的就是 bash , bash要这个本地变量干什么呢?其实为了让这个 bash 成为一个语言, 当然 bash 里面还要再做词法语法分析,这个我们不管。
还记得我们之前写的shell脚本吗?就是bash来解释的。我们之前用的一行一行的效果称作是交互式。
还有一点就是,这个本地变量其实是有很多用途的,比如说:

这就是我们的命令行格式,输出的时候按这个格式。
这个 > 是什么呢?续行的时候用的,

还有一个补充概念是什么呢?就是我们之前提到过的 export.

可以看到,这个export也可以导入本地变量。
但是,我们有没有想过,我们再导入环境变量的时候,也导入到了bash里面,从我们之前的例子来看,但是之前我们不是提到过,进程之间具有独立性吗?为什么子进程改了父进程的东西了。
其实,这个export是一种特殊的命令, 叫内建命令, build-in command。 一般再执行这种命令的时候,不需要bash创建子进程,而是bash亲自去执行。 怎么执行那就是bash调用自己的函数,或者系统调用之类的来完成。
2. 进程地址空间
我们之前在学习语言的时候,经常听到什么栈区,堆区,静态区之类的。
可以划分成这样的布局图,

从低地址一直到高地址,可以这样划分。我们之前把这个叫程序地址空间。
我们可以先来看看,这些空间的布局:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
看看结果:
c
$ ./a.out
code addr: 0x40055d
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x1791010
heap addr: 0x1791030
heap addr: 0x1791050
heap addr: 0x1791070
test static addr: 0x601038
stack addr: 0x7ffd0f9a4368
stack addr: 0x7ffd0f9a4360
stack addr: 0x7ffd0f9a4358
stack addr: 0x7ffd0f9a4350
read only string addr: 0x400800
argv[0]: 0x7ffd0f9a4811
env[0]: 0x7ffd0f9a4819
env[1]: 0x7ffd0f9a482e
env[2]: 0x7ffd0f9a4845
env[3]: 0x7ffd0f9a4850
env[4]: 0x7ffd0f9a4860
env[5]: 0x7ffd0f9a486e
所以,我们看到的地址空间就是这样的。
可以看到一些特点,栈是由高地址向低地址使用,堆从低地址到高地址。堆和栈之间可以看到跨度很大,因为堆和栈之间有一大段镂空的地址。
再看这个,只读字符串,和代码段在一起。所以是硬编码到代码里面了。
全局变量初始化和未初始化的,还有static变量生命周期就去全局了,可以通过地址的接近程度来验证。
那么问题就来了?程序地址空间,是内存吗?不是内存。
简单思考一下,如果这个东西是内存,那那么多的进程该怎么放,不可能每个进程都符合这样的划分。
然后,我们来更正一下概念,
这个东西不叫程序地址空间,其实叫进程地址空间,也叫虚拟地址空间,其实是一个系统层面的概念,而是语言层面的概念。
那么我们怎么证明一下,这个东西不是物理地址呢?
c
#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;
}
可以看到,这段代码,子进程会对子进程对gval修改,但是父进程不会修改数据,结果是什么呢?父进程看不到子进程的修改

可以看到,这个 gval 的地址一样,但是,值不一样。
这里还有一个小坑就是 c99 不认识 pid_t
一个物理地址空间可以存两个完全不一样的值吗?当然不行。所以,取地址去到的肯定不是物理地址。
所以,这里肯定不是内存地址。其实是虚拟地址。C/C++里面,我们取地址取到的都是虚拟地址。是操作系统为了适应操作来虚拟出来的地址,这个地址一点不影响上层使用。 其实只要是进程,用到的地址都是虚拟地址。
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
可以看到,这段代码,子进程会对子进程对gval修改,但是父进程不会修改数据,结果是什么呢?父进程看不到子进程的修改
[外链图片转存中...(img-R89XHQqK-1776408804184)]
可以看到,这个 gval 的地址一样,但是,值不一样。
> 这里还有一个小坑就是 c99 不认识 pid_t
一个物理地址空间可以存两个完全不一样的值吗?当然不行。所以,取地址去到的肯定不是物理地址。
所以,这里肯定不是内存地址。其实是虚拟地址。C/C++里面,我们取地址取到的都是虚拟地址。是操作系统为了适应操作来虚拟出来的地址,这个地址一点不影响上层使用。 其实只要是进程,用到的地址都是虚拟地址。
