目录
环境变量
基本概念
常见环境变量
查看环境变量方法
测试PATH[重点]
测试HOME
和环境变量相关的命令
环境变量的组织方式
通过代码获取环境变量
通过系统调用获取或设置环境变量
环境变量通常是具有全局属性的[重点]
程序地址空间
研究背景
程序地址空间回顾
程序地址空间新认识
进程地址空间
程序——>进程
g_val的地址相同值不同解释
fork为什么两个返回值
虚拟空间存在的意义
环境变量
基本概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量
- PATH : 指定命令的搜索路径 [重点]
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)[重点]
- SHELL : 当前Shell,它的值通常是/bin/bash
- HOSTNAME:当前主机的机器名
- HISTSIZE:系统一次允许记录使用过的命令的最多条数
查看环境变量方法
- echo $NAME //NAME:你的环境变量名称[重点]
比如查看PATH环境变量,使用以上命令:
测试PATH[重点]
- 创建myprocess.c文件
#include <stdio.h> int main() { printf("hello world!\n"); return 0; }
- 对比./myprocess执行和之间myprocess执行
发现后者报错:
- 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?
因为其他的指令的二进制文件路径被添加到了PATH环境变量中,而我们的没有
- 将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:hello程序所在路径
- 还有什么方法可以不用带路径,直接就可以运行呢?
直接将我们的文件拷贝到指令集的二进制文件目录(/usr/bin)下
测试HOME
- 用root和普通用户,分别执行 echo $HOME,对比差异
- 执行 cd ~; pwd,对应 ~ 和 HOME 的关系
cd ~即访问的是HOME的变量值
和环境变量相关的命令
- echo: 显示某个环境变量值[重点]
比如我们想查看PATH环境变量,如下图:
- export: 设置一个新的环境变量[重点]
通过上述实验发现确实可以,当如果想要把我们自己的路径添加到PATH中时,如下图:
发现我们是修改了PATH环境变量,并不是添加,此时PATH中环境变量全没了
由于没有修改系统中的配置文件只命令行上修改是临时性的,只要重新登录终端就恢复了,如下图:
增加环境变量的正确操作应该如下图:
我们发现在原来的基础增加了新的PATH环境变量,而我们使用的命令是export PATH=$PATH:环境变量值这个命令的意思是$PATH是把PATH中的所有环境变量提取出来放到PATH中,:后面是新增加一个环境变量
- env: 显示所有环境变量[重点]
- unset: 清除环境变量
清除环境变量方式如下图:
- set: 显示本地定义的shell变量和环境变量
使用set | less命令能看到这些全是环境变量和shell本地变量
可以看到我们定义的本地shell变量和环境变量都可以查看到
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
通过代码获取环境变量
- 命令行第三个参数[重点]
#include <stdio.h> int main(int argc, char *argv[], char *env[]) { for(int i = 0; env[i]; i++){ printf("%s\n", env[i]); } return 0; }
main函数的第三个参数接受的就是环境变量,打印env即打印的是环境变量
- 通过第三方变量environ获取[重点]
#include <stdio.h> int main(int argc, char *argv[]) { extern char **environ; for(int i = 0; environ[i]; i++){ printf("%s\n", environ[i]); } return 0; }
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
通过系统调用获取或设置环境变量
- putenv
- getenv
#include <stdio.h> #include <stdlib.h> int main() { printf("%s\n", getenv("PATH")); return 0; }
常用getenv和putenv函数来访问特定的环境变量。
例如使用getenv获取PATH环境变量:
环境变量通常是具有全局属性的[重点]
- 环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h> #include <stdlib.h> int main() { char * env = getenv("MYENV"); if(env){ printf("%s\n", env); } return 0; }
首先定义一个bash本地变量MYENV,发现没有结果,说明该环境变量根本不存在,导出环境变量 export MYENV="hello world",再次运行程序后会发现就会有hello world。
解释:MYENV被定义且没有使用export时,此时MYENV是bash本地变量,我们使用getenv时返回的是null,而使用export导出MYENV后就返回的是hello world,说明MYENV变成了环境变量。
由于我们能访问到本来是在bash中被定义的本地变量MYENV,而MYENV变成了环境变量后我们作为bash的子进程的程序也能访问到MYENV,说明了MYENV具有了全局属性,可以被子进程继承下去
额外的:既然子进程无法访问作为父进程bash中的本地变量,但是却可以使用echo输出bash中的本地变量。虽然Linux大部分命令都是通过子进程的方式执行的,但是还有一部分命令不通过子进程的方式执行,而是由bash自己执行(调用自己对应的函数来完成特定的功能,我们把这种命令叫做自建命令)
程序地址空间
研究背景
- kernel 2.6.32
- 32位平台
程序地址空间回顾
学习C语言的时候,接触过如下空间布局图:
使用下面的代码验证一下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int ug_val;
int g_val = 100;
int main(int argc, char* argv[], char* env[])
{
printf("main addr:%p\n", main);
printf("init addr:%p\n", &g_val);
printf("uninit addr:%p\n", &ug_val);
char* m1 = (char*)malloc(100);
char* m2 = (char*)malloc(100);
char* m3 = (char*)malloc(100);
char* m4 = (char*)malloc(100);
static int n = 100;//全局数据区
printf("static addr:%p\n", &n);
printf("heap addr:%p\n", m1);
printf("heap addr:%p\n", m2);
printf("heap addr:%p\n", m3);
printf("heap addr:%p\n", m4);
printf("stack addr:%p\n", &m1);
printf("stack addr:%p\n", &m2);
printf("stack addr:%p\n", &m3);
printf("stack addr:%p\n", &m4);
for(int i = 0; i < argc; ++i)
printf("argv addr:%p\n", argv[i]);
for(int i = 0; env[i]; ++i)
printf("env addr:%p\n", env[i]);
return 0;
}
然后运行该程序:
根据输出结果仔细对比上面的图发现确实如此,栈是向下增长的,堆是向上增长的,相对而生
程序地址空间新认识
根据程序地址空间,认识地址空间的存在,有如下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
int flag = 0;
while(1)
{
printf("I am son: pid:%d, ppid:%d, g_val=%d, g_val's address:%p\n", getpid(), getppid(), g_val, &g_val);
flag++;
sleep(1);
if(flag == 5)
{
g_val = 200;
printf("warning:the number has been changed!\n");
}
}
}
else
{
while(1)
{
printf("I am father: pid:%d, ppid:%d, g_val=%d, g_val's address:%p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
运行该程序,在flag等于5之前:
父进程和子进程的g_val值一样,地址也一样,这里很好理解,因为子进程是以父进程为模板创建的。
当flag等于5以后,出现了一个无法理解的现象:
父进程和子进程中的g_val地址一样,但是值却不一样! 能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量!
- 地址值是一样的,说明该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址(也叫逻辑地址或者线性地址)
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由操作系统统一管理!
操作系统必须负责将虚拟地址转化成物理地址 。
所以说“程序地址空间”的说法是不准确的,准确的应该说成进程地址空间
进程地址空间
每一个进程都会有一个自己的进程地址空间。
地址空间:操作系统通过软件的方式,给进程提供一个软件视角,认为自己会独占系统的所有资源(主要体现在内存上)。
操作系统通过先描述再组织的方式管理进程地址空间,也就是说进程地址空间其实是内核的一个数据结构(struct mm_struct)
我们发现在内核源码确实存在struct mm_struct,操作系统将虚拟地址分为了很多区域,如栈区,堆区等,每个区域都有一个start和end来划分区域,只不过是使用链表的方式划分的,而最后会经过页表的方式映射到物理内存上
程序——>进程
当一个程序写完并编译后未被加载,程序内部有无地址?
有,从链接中可以体现出来,链接会将我们的程序和库文件产生联系,而产生联系的方式就是需要使用地址
当一个程序写完并编译后未被加载,程序内部有无区域?
有,使用readelf -S命令查看编译好的二进制文件:
发现文件中确实有address(地址)和offset(偏移量)
我们所熟知的是只有当程序加载到内存中才有地址,但是这里并未加载到内存中怎么会有地址?
说明编译好文件中的地址并非物理地址,而是虚拟地址,当加载到内存中的时候经过相关转化的处理即可,所以在编译程序的时候就已经编址好了,等加载到内存中经过页表的处理映射到物理内存就行了
注:虚拟地址空间不仅操作系统会考虑,编译器也会考虑!
g_val的地址相同值不同解释
这里就能解释上面的父进程和子进程中的g_val地址一样,但是值却不一样,如下图(只划分出了数据,未划分出代码的存储,和数据存储的情况一致):
当数据未被修改时,由于子进程是以父进程为模板创建的,因此两个进程虚拟地址一致,并且映射到了同一个物理地址上,也就是flag=5之前的现象
但是,当我们进行写入时,无论是父进程还是子进程,由于进程具有独立性,进程之间互不影响,因此就会发生写时拷贝,操作系统重新给子进程开辟新的空间并将g_val的值拷贝到开辟的新空间中,两个进程不在使用同一块物理空间,如下图:
(写时拷贝:父进程/子进程尝试修改对应的数据时,操作系统会给修改的一方重新开辟一块空间并将原始数据拷贝到新空间当中)
通过页表,将父子进程的数据就可以通过写时拷贝的方式进行分离,做到父子进程具有独立性!
所以就出现了地址一样但却值不一样的现象,本质上就是虚拟地址一样但是物理地址不一样
fork为什么两个返回值
使用fork时我们知道它有两个返回值,而我们还是使用同一个变量(pid_t id)去接收的,怎么做到的?
pid_t id是属于父进程栈空间定义的变量,fork内部的return被执行两次,而return是通过寄存器将返回值写入到接收返回值的变量中
当id=fork()时,先返回的进程就要先发送写时拷贝,所以同一个变量有不同的返回值,本质上就虚拟地址一致但是物理地址不一样
虚拟空间存在的意义
为什么存在虚拟地址?
- 保护内存
直接让进程访问物理内存是不安全的,虚拟地址的存在让访问内存添加了一层软硬件层,能对转化过程进行审核,拦截非法访问。以及防止越界后进程之间互相影响或者访问到操作系统的进程导致进程崩溃
- Linux内存管理
按需提供物理内存,并非一次性就给申请的全部空间,使用到多少空间给多少空间,通过地址空间进行功能模块的解耦,提高Linux操作系统的运行效率
- 让进程或者程序可以以统一的视角看待内存
方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现