【Linux】进程详解:命令行参数、环境变量及地址空间
IsLand1314~ 2024-10-06 13:37:02 阅读 87
✨ 一生如牛不得闲,得闲已与山共眠 🌏
📃个人主页:island1314
🔥个人专栏:Linux—登神长阶
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
1. 前言 🚀
🌈之前在这篇文章中【Linux】进程管理:状态与优先级调度的深度分析就已经简述了 进程的部分相关内容,下面我们来进一步深入了解进程的命令行参数以及环境变量和进程地址空间。
2. 命令行参数 📚
在C / C++中,main函数有很多的变种,比如 我们也可以给它 带上参数
<code>main(),
int main(),
int main(int argc, char *argv[]),
int main(int argc, char *argv[], char *env[])。
上面的 argv 就是我们现在要讲的 命令行参数,env(环境变量) 后面说因此我们可以知道:main函数并不是第一个被调用的函数,而是startup函数
编译器如何知道main函数的参数个数?(答:条件编译)
1. argc(Argument Count)
定义:argc 是一个整数,表示从命令行传递给程序的参数数量,包括程序的名称。
值:
如果没有参数,argc 的值将为1,因为至少会有程序名本身。
如果有参数,例如./my_program arg1 arg2,argc的值将为3(1个程序名 + 2个参数)
2. argv(Argument Vector)
定义:argv 是一个字符指针数组(char *argv[]),每个元素是一个字符串,代表传递给程序的每个参数。
内容:
argv[0] 通常是程序的名称(包括路径)。
argv[1]是第一个参数。
argv[2]是二个参数,依此类推。
🌈 由上面可以知道:main函数参数其中两个参数为 int argc 和 char *argv[],其中 argv是指针数组,里面存的全是指针变量, argc 是 argv 数组的元素个数,那么 argv 数组究竟存着什么东西?
举个例子:
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
for(int i = 0 ; i < argc ; ++i)
{
printf("argv[%d]:%s\n",i ,argv[i]);
}
return 0;
}
🔥这里的结果就很明显了,bash 将我们命令行参数以空格为分隔符转化为一个个的子串,并且 argv里的每一个指针按照顺序指向不同的子串。
🔥说到字符串,我们无论实在 Linux 还是 Windows 或者其他系统,都有命令行提示符,他们是怎么构成的?我们输入的命令被转化成了一整个字符串,以空格作为分隔符,将整个字符串转化为一个一个的子串。
🔥所以这样也能获取到我们的命令行参数。现在我们知道了C语言 main 函数中两个参数是由bash 维护并创建和传参的。那么为什么要这样去做呢?
下面我们来看几段代码:
🍉 案例1:
<code>#include <stdio.h>
#include <string.h>
#include <unistd.h>
// code -pot1/-opt2/-opt3
int main(int argc, char *argv[])
{
if(argc != 2){
printf("Usage: code opt\n");
return 1;
}
if(strcmp(argv[1], "-opt1") == 0){
printf("功能1\n");
}
else if(strcmp(argv[1], "-opt2") == 0){
printf("功能2\n");
}
else if(strcmp(argv[1], "-opt3") == 0){
printf("功能3\n");
}
else{
printf("默认功能\n");
}
return 0;
}
上面是我们根据输入的命令行参数的选项来做不同功能的函数:
🍉 案例2:
<code>// 简单计数器
int main(int argc, char* argv[])
{
if (argc != 4) {
printf("Usage:\n\t%s -[add|sub|mul|div| mod] x y\n\n", argv[0]);
}
// 将字符转化为数字
int x = atoi(argv[2]);
int y = atoi(argv[3]);
if (strcmp(argv[1], "-add") == 0){
printf("%d + %d = %d\n", x, y, x + y);
}
else if (strcmp(argv[1], "-sub") == 0){
printf("%d - %d = %d\n", x, y, x - y);
}
else if (strcmp(argv[1], "-mul") == 0){
printf("%d * %d = %d\n", x, y, x * y);
}
else if (strcmp(argv[1], "-div") == 0){
printf("%d / %d = %d\n", x, y, x / y);
}
else if(strcmp(argv[1], "-mod") == 0){
printf("%d %% %d = %d\n", x, y, x % y);
}
else{
printf("unknown!\n");
}
return 0;
}
🍊 通过上面的代码我们就可以实现:可以通过不同的选项,让同一个程序执行它内部不同的功能。
🍎 这个功能是不是很像我们的指令?(比如:ls 指令)为什么我们指令可以根据不同的选项而做出不同的动作?原因就在于我们的选项传递到main函数中的 argc 和 argv当中,所以能够完成同一个指令根据不同选项做出对应的功能,所以,选项的本质就是命令行参数💢
命令行参数可以为指令、工具、软件提供功能选项支持(指令可以带不同的选项和命令行参数有关)
3. 环境变量 🖊
3.1 基本概念
还记得我们在上面演示的命令行参数开篇提到的 env,现在我们来讲一下相关内容。
🍊 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数🍊 环境变量(environment variables)通常具有某些特殊用途,还有在系统当中通常具有全局特性我们可以通过输入 env 来查看当前的环境变量表
环境变量有很多的应用场景,这里举一个例子:
我们通常编写的C/C++ 代码,在编译之后将每一个目标文件链接起来的时候,我们并不知道动静态库的位置,但是将所有的目标文件链接起来,生成可执行文件,这就时依靠这全局的环境变量在查找。
为啥要用到环境变量,我们来看个图片
3.2 查看环境变量的方法
可以使用 echo命令来查看环境变量,语法格式为:
<code>echo $NAME # NAME为环境变量名
3.3 常见的环境变量
3.3.1 PATH 🥝
PATH : 指定命令的搜索路径
然后就可以不带 ./ 直接运行test.exe文件
注意不能写做:PATH=新路径名称,否则之前的PATH将会全部清空
此环境变量是内存级别的,当用户如果不小心改错了重新登陆Xshell即可(系统文件中已经预存好了环境变量)
PATH 的理解:
PATH:中存放的环境变量是为了在执行命令的时候,可以在PATH中找到对应的路径,这样就可以不用写出命令绝对路径了。
举一个例子:ls 的执行过程
ls命令的最常用的一个命令,但是其实ls也只不过是在系统中的一个封装的可执行程序而已,可以使用which命令查看ls的路径,可以看到ls的路径为/usr/bin/ls。所以我们可以这样使用ls命令:
<code>/usr/bin/ls # 查看当前目录下的文件
但是平时我们却通常是直接使用ls命令的,这是为什么呢?这就是因为在PATH路径下有/usr/bin。
因为在使用ls命令的时候,系统就会首先去PATH中的环境变量从左向右地寻找ls的工作路径。如果发现了ls的工作路径,这时直接使用ls命令就不会报错了。
修改PATH 的方法:
通常我们自己在写完一个代码后,形成了一个可执行文件,需要通过./这样的方式才可以运行,这是因为环境变量PATH中没有当前可执行程序的工作目录,所以我们只能通过./这样的方式,自己手动的通过相对路径的方式运行可执行程序。
假设我们在/home/zhy/test目录下,有一个hello的可执行程序。运行之后可以打印出hello world
如果想让我们的可执行程序可以直接像ls命令那样直接运行,我们可以用两种方法:
方法一:在PATH中用别人的工作目录
可以将hello这个可执行程序,拷贝一份放入PATH中已经存在的工作目录下(比如说是/usr/bin/,这样在运行可执行程序的时候,其实运行的是在/usr/bin/下的hello。
<code>sudo cp hello /usr/bin/
但是强烈地不推荐使用这种方法,因为这样就会污染了其他工作目录。不利于整个系统的发展,就比如对 PATH 的 覆盖式写法。
方法二:自己在PATH创建一个新目录
将当前的工作目录添加到PATH中即可。
需要使用export命令,在PATH中添加新的工作目录。
export PATH=$PATH:/home/zhy/test/
在执行完上述的命令之后,就可以直接使用hello指令了。
注意:这样操作的话,其实在下一次重新开使用Linux的时候,原来的环境变量就会被重新的覆盖,导致hello又不可以直接被使用了。如果想要永久的使得命令生效,就必须要修改~/.bash_profile 配置文件才可以
vim ~/.bash_profile # 将创建工作目录的指令写在.bash_profile中
source .bash_profile # 使得.bash_profile中的内容生效
而它的实质其实是每次重新登陆都会读取系统自带的配置文件.bash_profile
,配置文件中的内容,为我们bash进程形成一张环境变量表信息!
比如:
讲到这,我们就也要来了解一下 环境变量 的 配置文件
命令行启动的进程都是shell/bash的子进程,子进程的命令行参数和环境变量是父进程bash给我们传递的
上面我们对 PATH 路径 进行覆盖式写入时,发现有很多的指令都用不了了,但当我们重新登录之后,又可以恢复正常!
原因:我们直接更改的是bash进程内部的环境变量信息!每一次重新登陆,都会给我们形成新的bash解释器并且新的bash解释器自动从读取形成自己的环境变量表信息
💖实质其实是每次重新登陆都会读取系统自带的配置文件.bash_profile,配置文件中的内容,为我们bash进程形成一张环境变量表信息!
3.3.2 HOME 🥝
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
任何一个用户都有自己的主工作目录,<code>HOME保存的就是当前用户的主工作目录。
用root和普通用户,分别执行 echo $HOME ,对比差异
3.3.3 SHELL 🥝
SHELL : 当前使用的shell版本,通常为/bin/bash
Linux中的shell有很多不同的版本,也就是有很多不同的命令行解释器。常见的有bash,sh,tsh等等。SHELL中保存的就是当前的命令行解释器的版本
3.4 和环境变量相关指令
在讲这个之前,我们需要先了解一下本地变量和环境变量
Linux中,我们可以在bash中直接定义环境变量
指令:变量名=内容
但在环境变量表里是无法获取到刚刚自定义的变量
实际上: 我们用户自己定义的环境变量就称为 本地变量
如果想要我们定义的环境变量我们该怎么办,这时我们需要一条新的指令
a.export
有了export我们就可以将我们自定义的环境变量添加到bash上下文的环境变量中
案例:
我们用export指令可以将自己写的环境变量添加到父进程bash的进程上下文中,但是如果我们重新登录后,之前导入的变量是否还会存在?
答案很明显,我们变量之会被添加到内存中,并不会改变配置文件,所以重新登录后并不会被保存因此如果我们想让我们的环境变量能够保存可以直接在配置文件中更改。
b. env:显示所有的环境变量
HISTSIZE:Xshell能记录的最大历史指令条数
USER: 当前用户
LD_LIBRARY_PATH:指定查找共享库(动态链接库)时除了默认路径之外的其他路径。
PATH:可执行程序的搜索路径
LS_COLORS ls:的配色方案
MAIL:MAIL是指当前用户的邮件存放目录。
PWD:当前所处的路径
LANG:用于定义系统的主语系环境
HISTCONTROL:可以控制历史的记录方式
HOME:用户的主目录(也称家目录)
SHLVL:记录了bash嵌套的层次,一般来说,我们启动第一个Shell时。 $SHLVL=1。如果在这个Shell中执行脚本,脚本中的 $SHLVL=2 。
OLDPWD:相比于pwd记录当前路径,OLDPWD 是记录上一次的最新路径
c. set:显示本地定义的shell变量和环境变量,和env中的环境变量相比,set中的环境变量只在本进程中有效。
d. unset:清除环境变量
3.5 环境变量的组织形式
🔥 每个程序都会收到一张环境表,环境表是一个字符指针数组,环境变量的存储方式就是利用的指针数组,一个数组中存放了很多的指针,每一个指针都指向一个环境变量的首地址,并且都指向一个以’\0’结尾的环境字符串,因此我们可以找到对应的环境变量,而且指针数组的最后一定存放了NULL 作为结尾。
3.6 获取环境变量的三种方法
(1)命令行第三参数
命令行上面已经说过,这里就不过多讲了
<code>#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
argc :代表调用 main()
函数进程的时候,传入的参数个数。argv :代表调用 main()
函数进程的时候,传入的参数。env:代表环境变量表,由此获取系统的环境变量。
(2)通过第三方变量environ获取
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
#include <iostream>
using namespace std;
int main()
{
extern char** environ;
for (int i = 0; environ[i]; i ++) {
cout << environ[i] << endl;
}
return 0;
}
(3)系统调用接口 getenv()
putenv()("环境变量=value")
getenv()("环境变量")
getenv函数可以根据所给的环境变量名,在环境变量表中进行搜索,并返回一个找到搜索的环境变量的指针
<code> 1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 printf("PATH: %s\n",getenv("PATH"));
7 return 0;
8 }
可以通过比较getenv(“USER”)来判断登陆用户是不是正确的,和权限建立起连接
🔥常用putenv()和getenv函数来访问特定的环境变量。
3.7 环境变量的全局性
环境变量通常是具有全局属性的
环境变量是系统提供的一组name=value形式的变量,不同的环境变量有不同的用户,通常具有全局属性,能够被所有进程获取。环境变量通常具有全局属性,可以被子进程继承下去
案例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ; // 声明
int main()
{
printf("%s\n", getenv("island"));
for(int i = 0; environ[i]; i++)
{
printf("[%d]: %s\n", i, environ[i]);
}
return 0;
}
//输出打印 当前 bash 进程的环境变量表
(1)验证子进程会继承父进程的环境变量
(2)验证子进程不会继承父进程的本地变量
虽然子进程不会继承父进程的本地变量,但是我们可以向子进程中导入父进程的本地变量
🔥 在了解完环境变量的全局性时,我们现在再来看看本地变量和环境变量的区别:
本地变量只在bash进程内部有效,不会被子进程继承下去环境变量通过让所有的子进程继承的方式,实现自身的全局性!
注意:
我们所运行的进程,都是子进程,bash本身在启动的时候会从操作系统的配置文件中读取环境变量信息,子进程会继承父进程交给我们的环境变量,我们定好的环境变量可以让所有的子进程继承下去,所以环境变量具有全局属性但是注意!环境变量也是数据,默认情况下是父子共享的,由于进程具有独立性,创建完子进程后,如果想对环境变量进行修改,是不能影响父进程的,因为会写实拷贝环境变量被继承通常有两种方式:1. 直接继承 2. main 函数传参
3.8 内建命令和常规命令
🥑在之前就提到过,bash中的指令可以直接使用,不用加./是因为存在环境变量PATH,但是PATH 覆盖式写入 或者 置空后,这些命令就会失效
那么此时就有个问题了,为啥有些指令可以使用,有些指令却无法使用呢?
此时就需要引出一个新的概念
Linux的命令分类:
常规命令:shell fork让子进程成执行的内建命令:shell命令行的一个函数,当然可以直接读取shell内部定义的本地变量!
🥑虽然让PATH挂掉了,但是并没有让shell挂掉,因此shell内部定义的变量我们依然可以正常使用
注意:echo也是一个内建命令
4. 地址空间 🔍
4.1 回顾程序地址空间
下面这张图是我们通常意义认为的<code>C/C++内控空间分布图:
<code>#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int gval = 100;
int ungval;
int main(int argc, char* argv[], char* envp[])
{
int val = 10;
int unval;
printf("code address: %p\n", main);// 正文代码区
char* str = "hello world";
printf("read only address: %p\n", str);// 只读常量区
printf("init address: 局部: %p, 全局: %p\n", &val, &gval);// 初始化数据区
printf("uninit address: 局部: %p, 全局: %p\n", &unval, &unval);// 未初始化数据区
int* p = (int*)malloc(4);
printf("heap address: %p\n", p);// 堆区
printf("stack address: %p\n", &p);// 栈区
for (int i = 0; i < argc; i++) {
printf("args address: %p\n", argv[i]); // 命令行参数区
}
for (int i = 0; envp[i]; i++) {
printf("env address: %p\n", envp[i]);
}
return 0;
}
插入一个小知识:
栈区中的数组和结构体
<code>int num[10] ......&a[0] &a[9]
struct s
{
int a; ......&s.a
int b; ......&s.b
int c; ......&s.c
}
注意:栈区是整体向下增长,局部想上使用的,就是地址最低处,依次往上放后面的元素
但是我们对 char* 的内容进行修改还能运行嘛?
<code>char *str = "hello world";
*str = 'S';
显然我们是不能更改的,一更改就就运行不了了
注意:其实是因为字符常量区与代码区很接近,而编译器在编译时,字符常量区就是被编译到代码区的,代码又不可被写入,所以字符常量区也不可被修改
综上:
栈区是整体向下增长,局部想上使用的,就是地址最低处,依次往上放后面的元素常量区的字符串不允许修改
但是这都是我们之前了解的知识,现在我们来重新了解地址,我们先来看这段代码:
<code>#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int gval = 100;
int main()
{
printf("我是一个进程, pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
gval++;
sleep(1);
}
}
else
{
while(1)
{
printf("父进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
sleep(1);
}
}
return 0;
}
代码的含义如下: 首先定义一个全局变量gval,然后fork()出一个子进程。在子进程中使gval++,并打印出子进程的PID,PPID,gval和&gval。而为了使子进程在父进程之前运行完,所以在父进程中先休眠1秒,接着再打印出父进程的PID,PPID,gval和&gval
预期结果: 子进程中的gval等于101,父进程中的gval也等于101,并且两个x的地址应该是相同的。
实际运行结果:
🍊 我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
变量内容不一样, 所以父子进程输出的变量绝对不是同一个变量!但地址值是一样的,说明,该地址绝对不是物理地址!在Linux地址下,平时用到的这种地址叫做 虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!而物理地址用户一概是看不到的,由OS统一管理,🍁 OS 必须负责将 虚拟地址 转化成 物理地址。
4.2 虚拟地址与页表
🧩不管中间发生了什么最终会出现两个不同的数值,说明在实际存储的空间中gval一定是被存放在了不同的区域当中了,这就是下面要介绍的一个新的概念:「虚拟地址空间」。
🌸实际上,平时我们站在学习语言的层面上看的地址空间都是虚拟的地址空间,也就是说这个空间并不是实际存在的,并不是实际的物理上的内存地址空间。为了防止用户破坏系统中的空间,真正存储变量的空间由操作系统统一管理
🌸所以上面&gval其实打印出来的是虚拟的地址空间,虚拟地址空间通过一系列的翻译转化可以通过「页表」映射到真正的物理地址空间,而子进程中的gval=101和父进程中的gval=100就存放在这两个真正的物理空间中。这就解释了为什么相同的虚拟地址空间可以有两个不同的数值
综上:当一个进程修改之后,它就不再指向原来那块物理空间,而是拥有一个新的物理空间!而页表左边的虚拟空间没有发生改变,所以相同的的地址为什么会有不同的值,是因为映射的物理空间不同💖
再插入一个关于页表的小知识:
还记得我们之前对 char* 的内容进行修改后,但是却无法运行
🍅那我现在想问一下,这个是什么原因呢?
其实吧这个就涉及到页表的一个读写权限 --rmx
此时我们就可以解释通字符常量区为什么不能修改:
字符常量区在经过页表映射时,访问权限字段只设置成只读的,所以在写入时,页表直接将我们拦住,不让我们访问,所以字符常量区不能修改,代码区也是如此!
🍅 所以页表可以进行安全评估,有效的进行进程访问内存的安全检查
🍐比如:当我们有个虚拟地址要被访问了,但是它并没有被分配空间,更不会有内容,那该则么办呢?
其实在这个时候操作系统会将你的这个访问暂停,然后进行一下操作:
操作系统会将你的可执行程序重新开辟空间把对应可执行程序需要执行的这个虚拟地址对应的代码加载到内存里把对应的虚拟地址填充到页表把标志位改为1,代表已经分配地址,且内容已经填充将暂停的代码继续访问
操作过程也称为 「缺页中断」
而我们操作系统在进行这些工作时,是在进行内存管理, 而进程管理和内存管理因为有了地址空间的存在 ,实现了在操作系统层面上的模块的解耦 💖
4.3 再谈进程地址空间
🍅其实在学习完上面进程的概念之后,我们应该把「程序地址空间」称之为「进程地址空间」
概念:进程地址空间是用来描述操作系统中的进程所占的空间。 由于进程的独立性,每个进程都认为自己独占系统内存资源,因此通过让每个进程都看到完整的地址空间来实现这种独立性。
实质:进程地址空间本质上是虚拟地址空间,它通过虚拟地址与物理地址的映射来分配空间。 这些虚拟地址在进程运行时由操作系统通过页表等机制映射到实际的物理地址上。
4.2.1 🍋🟩 进程描述符mm_struct
🦁 其实 「进程地址空间」 本质上也是一种在操作系统的一个内核数据结构,在Linux中进程地址空间称之为 struct mm_struct(内存描述符) 的结构体。Linux就是通过这个结构体来实现 「内存管理」 的。
🍁 每个进程只有一个mm_struct结构,在每个进程的task_struct结构体中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述
<code>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线性增长的,所以虚拟地址也叫作「线性地址」。
补充:
堆的向上增长和栈的向下增长是上都是在改变struct mm_struct中end_brk和end_stack的位置而已。
我们生成的可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。
4.2.2 🥬 struct_mm_struct,struct_task_struct和页表的关系
🤯 在最开始介绍进程在创建的时候,我们了解到每当起一个进程的时候都实际上是在内核中创建了一个 struct task_struct
学到这里,我们又可以重新的认知进程的创建过程:闯将与进程对应的进程控制块struct task_struct,进程描述符struct mm_struct和对应的页表。而struct task_struct中有指向struct mm_struct的指针,所以可以找到struct mm_struct。然后struct mm_struct中的内容通过页表映射到物理内存中
再回到之前的那一段的代码中:
<code>#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int gval = 100;
int main()
{
printf("我是一个进程, pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
gval++;
sleep(1);
}
}
else
{
while(1)
{
printf("父进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
sleep(1);
}
}
return 0;
}
父子进程都有自己的task_str和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置
一开始创建子进程的时候,子进程和父进程的代码和数据共享,即相同的虚拟地址会映射到相同的物理地址空间
当在子进程要修改父进程中的数据的时候,父进程中的数据会重新的拷贝一份,然后子进程再对数据进行修改。这样父子进程中的数据就独立了
这种只有在多个进程中其中一个进程对数据进行修改的时候再进行拷贝的行为称之为「写时拷贝」
🐸 对于写时拷贝,有两个问题:
(1)为什么要进行写时拷贝?
🐻 进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
(2)为什么不在创建子进程的时候就直接在子进程中拷贝一份父进程中的data和code?
🦁 子进程不一定会修改父进程中的code或者data,只有当需要修改的时候,拷贝父进程中的数据才会有意义,这种按需分配的方式,也是一种延时分配,可以高效的时候使用内存空间和运行的效率。
4.4 地址空间的作用
一、 让无序便有序
让进程以统一的视角看待内存在页表层映射时会将不同的数据类型进行划分使得映射到物理内存后是比较有序的一种状态!所以任意一个进程,可以通过地址空间+页表可以将乱序的内存数据,变成有序,分门别类的规划好,并且可以使得用户不能接触到物理内存,这样就不会出现系统级别(访问物理内存)的越界问题了,因为虚拟内存的越界问题并不会影响到实际的物理内存。本质上说就是保护了内存。
二、存在虚拟地址空间,可以有效的进行进程访问内存的安全检查
三、将进程调度(task_struct管理)和内存管理(mm_struct管理)进行了解耦
四、保证进程的独立性以及合理使用内存空间
通过页表让进程虽然虚拟地址一样但是映射到不同的物理内存处,从而实现进程的独立性
5. 小结 📖
💢Linux命令行参数,环境变量,环境变量的学习重在理解,细节比较多,而且有很多新概念,所以认真,细心的学习环境变量是很重要的,地址空间让进程管理和内存管理互不干涉,起到了很大作用。结束进程地址空间,
💖💞💖【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。