【Linux】进程地址空间、环境变量:从理论到实践(三)
Zfox_ 2024-10-10 12:07:06 阅读 74
🌈 个人主页:Zfox_
🔥 系列专栏:Linux
目录
🚀 前言一:🔥 环境变量 🥝 基本概念🥝 常见环境变量🥝 查看环境变量方法
二:🔥 测试 🥝 PATH🥝 修改PATH
三:🔥 与环境变量相关的命令 四:🔥 获取环境变量 通过代码如何获取环境变量
五:🔥 进程地址空间 🥝 定义与本质🥝 虚拟地址与页表🥝 进程描述符mm_struct🥝 struct_task_struct,struct_mm_struct和页表的关系
六:🔥 共勉
🚀 前言
🐲 接着上一篇博客我们继续往下学习,点击跳转上一篇博客 【Linux】进程优先级、调度、命令行参数:从理论到实践(二)
一:🔥 环境变量
🥝 基本概念
<code>环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数。如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
🥝 常见环境变量
PATH
: 指定命令的搜索路径,PATH
中存放的环境变量是为了在执行命令的时候,可以在PATH中找到对应的路径,这样就可以不用写出命令绝对路径了。HOME
: 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)。SHELL
: 当前 Shell
,它的值通常是 /bin/bash
。
🥝 查看环境变量方法
echo $NAME //NAME:你的环境变量名称
例如:查看PATH
环境变量:
echo $PATH
二:🔥 测试
🥝 PATH
💦 <code>PATH 的原理解释
PATH
:存放的环境变量是为了在执行命令的时候,可以在PATH中找到对应的路径,这样就可以不用写出命令绝对路径了。
举一个例子:ls的执行过程
💦 ls
命令的最常用的一个命令,但是其实 ls
也只不过是在系统中的一个封装的可执行程序而已,可以使用 which
命令查看 ls
的路径,可以看到 ls
的路径为/usr/bin/ls
。
🍊 所以我们可以这样使用ls命令:
/usr/bin/ls # 查看当前目录下的文件
但是平时我们却通常是直接使用ls命令的,这是为什么呢?这就是因为在 PATH
路径下有 /usr/bin
。
因为在使用ls命令的时候,系统就会首先去PATH中的环境变量从左向右地寻找ls的工作路径。如果发现了ls的工作路径,这时直接使用ls命令就不会报错了
🥝 修改PATH
🍊 通常我们自己在写完一个代码后,形成了一个可执行文件,需要通过 ./ 这样的方式才可以运行,这是因为环境变量 PATH 中没有当前可执行程序的工作目录,所以我们只能通过 ./ 这样的方式,自己手动的通过相对路径的方式运行可执行程序。
假设我们在/home/lisi/test目录下,有一个hello的可执行程序。运行之后可以打印出hello world
💦 <code>如果想要让我们的可执行程序可以直接像ls命令那样直接运行,我们可以用两种方法:
在PATH中用别人的工作目录。
可以将hello这个可执行程序,拷贝一份放入PATH中已经存在的工作目录下(比如说是/usr/bin/,这样在运行可执行程序的时候,其实运行的是在 /usr/bin/ 下的hello。
sudo cp hello /usr/bin/
但是强烈地不推荐使用这种方法,因为这样就会污染了其他工作目录。不利于整个系统的发展。
自己在PATH创建一个新目录。
第二种方法就是将当前的工作目录添加到PATH中即可。
需要使用export命令,在PATH中添加新的工作目录。
export PATH=$PATH:/home/lisi/test/
在执行完上述的命令之后,就可以直接使用hello指令了。
💦 注意:这样操作的话,其实在下一次重新开使用Linux的时候,原来的环境变量就会被重新的覆盖,导致 hello 又不可以直接被使用了。如果想要永久的使得命令生效,就必须要修改~/.bash_profile
文件才可以。
vim ~/.bash_profile # 将创建工作目录的指令写在.bash_profile中
source .bash_profile # 使得.bash_profile中的内容生效
三:🔥 与环境变量相关的命令
命令 | 说明 |
---|---|
set | 显示当前 Shell 所有变量,<code>/etc/bashrcs shell变量,/etc/profile 环境变量,用户环境变量,自定义变量 |
env | 显示当前用户的环境变量 <code>~/.bashrcs及~/.profile |
export | 显示从 Shell 中导出成环境变量的变量,也能通过它将自定义变量导出为环境变量。但只是临时生效,shell关闭后,变量就会释放。 |
unset | 删除环境变量,执行 <code>unset PATH ,再执行 ls 将提示找不到 ls 命令 |
四:🔥 获取环境变量
✨ extern char ** environ
\colorbox{pink}{✨ extern char ** environ}
✨ extern char ** environ
💦
environ
是一个二级字符指针,相当于一个字符串数组,是程序运行的环境变量,当程序启动时,会复制,父进程的环境变量。程序在shell
中运行,父进程就是当前shell
。若当前shell
使用了export a=123
,程序运行后environ
也会存在a=123
。
✨ getenv、setenv、unsetenv函数
\colorbox{pink}{✨ getenv、setenv、unsetenv函数}
✨ getenv、setenv、unsetenv函数
getenv
、setenv
、unsetenv
三个函数存在 stdlib.h
中。
getenv
char *getenv (const char *__name)
根据环境变量名获取环境变量
setenv
int setenv (const char *__name, const char *__value, int __replace)
设置环境变量,replace=0表示若存在不进行替换,replace=1表示若存在也会进行替换。
unsetenv
int unsetenv (const char *__name)
根据环境变量名删除环境变量
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char * argv[])
{
char * lang = getenv("LANG"); // 获取本程序运行的语言环境
if (NULL == lang)
{
return -1;
}
puts(lang);
char * a = getenv("a");
if (a == NULL)
{
puts("不存在");
setenv("a", "345", 0);
}
else
{
puts(a);
setenv("a", "345", 1); // 第三个参数为1表示存在就替换, 为0表示,存在就算了
}
a = getenv("a");
puts(a);
unsetenv("a"); // 删除本程序的a的环境变量, 对父进程没有影响
a = getenv("a");
if (a == NULL)
{
puts("不存在");
}
return 0;
}
通过代码如何获取环境变量
💦 每个程序中的main函数中都要参数,分别为
int argc
、char* argv[]
、char* envp[]
💦 其中arge表示argv中有效数据的个数,而argv是存放指向命令参数的指针数组,envp是存放指向环境变量的指针数组。
用代码演示获取环境变量:
#include <stdio.h>
#include <unistd.h>
int main(int argc ,char* argv[],char* envp[])
{
int i=0;
while(envp[i])
{
printf("envp[%d]:%s\n",i,envp[i]);
i++;
}
return 0;
}
五:🔥 进程地址空间
🥝 定义与本质
🍁 <code>定义:进程地址空间是用来描述操作系统中的进程所占的空间。 由于进程的独立性,每个进程都认为自己独占系统内存资源,因此通过让每个进程都看到完整的地址空间来实现这种独立性。
🍁 本质
:进程地址空间本质上是虚拟地址空间,它通过虚拟地址与物理地址的映射来分配空间。 这些虚拟地址在进程运行时由操作系统通过页表等机制映射到实际的物理地址上。
来段代码感受一下
<code>#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 0;
}
else if (id == 0) { //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else { //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
💦 输出
parent[17440]: 0 : 0x601058
child[17441]: 0 : 0x601058
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.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;
}
💦 输出:
child[18300]: 100 : 0x601058
parent[18299]: 0 : 0x601058
🍊 我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
变量内容不一样, 所以父子进程输出的变量绝对不是同一个变量!
但地址值是一样的,说明,该地址绝对不是物理地址
!在Linux地址下,这种地址叫做 虚拟地址
。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
🍁 OS必须负责将 虚拟地址
转化成 物理地址
。
🥝 虚拟地址与页表
💦 不管中间发生了什么最终会出现两个不同的数值,说明在实际存储的空间中 g_val
一定是被存放在了不同的区域当中了,这就是下面要介绍的一个新的概念: 「虚拟地址空间」
。
🦁 实际上,平时我们站在学习语言的层面上看的地址空间都是虚拟的地址空间,也就是说这个空间并不是实际存在的,并不是实际的物理上的内存地址空间。为了防止用户破坏系统中的空间,真正存储变量的空间由操作系统统一管理。
🦁 所以上面 <code>&x 其实打印出来的是虚拟的地址空间,虚拟地址空间通过一系列的翻译转化可以通过
「页表」
映射到真正的物理地址空间,而子进程中的g_val=100
和父进程中的g_val
就存放在这两个真正的物理空间中。这就解释了为什么相同的虚拟地址空间可以有两个不同的数值。
🥝 进程描述符mm_struct
🦁 其实 「进程地址空间」
本质上也是一种在操作系统的一个内核数据结构,在Linux中进程地址空间称之为 struct mm_struct
(内存描述符)的结构体。Linux就是通过这个结构体来实现 「内存管理」
的。
🎯 每个进程只有一个mm_struct结构,在每个进程的task_struct结构体中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述。
struct mm_struct {
//...
unsingned long start_code,end_code,start_data,end_data; //代码段的开始start_code ,结束end_code,数据段的开始start_data,结束end_data
unsigned long start_brk,brk,start_stack; //start_brk和brk记录有关堆的信息,start_brk是用户虚拟地址空间初始化,brk是当前堆的结束地址,start_stack是栈的起始地址
unsigned long arg_start,arg_end,env_start,env_end; //参数段的开始arg_start,结束arg_end,环境段的开始env_start,结束env_end
// ...
};
🍁 上面这个
Linux
内核的源代码,可以看到struct mm_struct
中也是被划分成为了多个不同的区域的。这些虚拟地址通过页表和物理内存建立映射的联系。由于虚拟地址也是有0x00000000
到0xFFFFFFFF
线性增长的,所以虚拟地址也叫作「线性地址」
。
🦁 补充:
堆的向上增长和栈的向下增长是上都是在改变 truct mm_struct
中 end_brk
和 end_stack
的位置而已。
我们生成的可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。
🥝 struct_task_struct,struct_mm_struct和页表的关系
💦 在最开始介绍进程在创建的时候,我们了解到每当起一个进程的时候都实际上是在内核中创建了一个 struct task_struct
。
学到这里,我们又可以重新的认知进程的创建过程:创建与进程对应的进程控制块 struct task_struct,进程描述符 struct mm_struct 和对应的页表。而 struct task_struct 中有指向 struct mm_struct 的指针,所以可以找到 struct mm_struct。然后 struct mm_struct 中的内容通过页表映射到物理内存中。
父子进程都有自己的 task_struct 和 mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置。
一开始创建子进程的时候,子进程和父进程的代码和数据共享,即相同的虚拟地址会映射到相同的物理地址空间。
<code>当在子进程要修改父进程中的数据的时候,父进程中的数据会重新的拷贝一份,然后子进程再对数据进行修改。这样父子进程中的数据就独立了。
这种只有在多个进程中其中一个进程对数据进行修改的时候再进行拷贝的行为称之为 <code>「写时拷贝」。
🦁 对于写时拷贝,有两个问题:
为什么要进行写时拷贝?
🦁 进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
为什么不在创建子进程的时候就直接在子进程中拷贝一份父进程中的data和code?
🦁 子进程不一定会修改父进程中的code或者data,只有当需要修改的时候,拷贝父进程中的数据才会有意义,这种按需分配的方式,也是一种延时分配,可以高效的时候使用内存空间和运行的效率。
进程地址空间的作用
🦁 通过虚拟地址 + 页表的这种方式,可以使得用户不能接触到物理内存,这样就不会出现系统级别(访问物理内存)的越界问题了,因为虚拟内存的越界问题并不会影响到实际的物理内存。本质上说就是保护了内存。
🐯 为每一个进程提供了一致的地址空间,从而简化了内存管理。
🦊 更好的完成了进程的独立性以及合理使用内存空间,并将进程调度(task_struct管理)和内存管理(mm_struct管理)进行了解耦。
六:🔥 共勉
以上就是我对 【Linux】进程地址空间、环境变量:从理论到实践(三)
的理解,会立刻更新下一篇的,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。