Linux进程内存


进程地址空间

只要是学过C语言的,都听过这样一句话:局部变量在栈上开辟空间,malloc在堆上开辟空间。那个时候只知道,堆比栈更nb,却不知道堆和栈具体是个啥,但是在进程里,就必须看看进程的地址空间图:

这段空间中自下而上,地址是增长的,栈是向地址减小方向增长(栈是先使用高地址),而堆是向地址增长方向增长(堆是先使用低地址),堆栈之间的共享区,主要用来加载动态库。
一个进程被放在内存中,有PCB,数据段和代码段。
而代码区往上,我们都可以称其为数据段,存储着进程的上下文。
代码区,存储着进程运行的代码,便是代码段。

在PCB中,存储着不同区段的首尾地址,cpu在获取数据的时候,先通过PCB找到首尾地址,然后再读取首尾地址中间的数据:

而在实际的内存分区中,并没有栈区,堆区,代码区之分。这些只是为了方便我们观察内存的分布,而实际上可能会指向内存的任意一块。 

父子进程地址空间 

在多进程中说道,子进程会完全复制父进程的代码段和数据段。但是这个完全复制是什么意思?是只复制了一个个变量的值,还是直接复制了整个task_memory的地址,父子进程共享一个进程地址空间?
我们来试验一下:

int main()
{
    int a=20;
    
    int pid = fork();
    if(pid==0)
    {
        printf("这是子进程,a的地址为: %p \n",&a);
    }
    else
    {
        printf("这是父进程,a的地址为: %p \n",&a);
    }
    return 0;
}

可是,真的有这么简单吗?
我们将代码稍微改一下,让子进程先跑,然后把值修改一下,父进程再跑。按理来说,子进程在修改之后,该地址上的值会发生改变,最后父子进程都去读取该地址的值,输出被修改后的值,但是:

int main()
{
    int a=20;
    
    int pid = fork();
    if(pid==0)
    {
	    a=10;
        printf("这是子进程,a的地址为: %p \n",&a);
        printf("a的值为: %d \n",a);
    }
    else
    {
    	sleep(3);//为了防止输出串行,让子进程先跑
        printf("这是父进程,a的地址为: %p \n",&a);
        printf("a的值为: %d \n",a);
    }
    return 0;
}

所以我们可以得到一个结论:子进程复制的,绝对不是真正的内存空间。 

虚拟地址空间

实际上,我们所看到的地址,并非真正的物理地址。 

我们看到的地址,是系统为了方便我们观察,为我们提供的虚拟地址。栈空间是连续的,堆空间也是连续的,但是真实情况,真的会给这么多连续的空间吗?难道系统一定要去划分,哪些内存是堆区,哪些内存是栈区,哪些内存是代码区吗?当然不是,无论是从空间利用,还是从查找效率,这种分配方法都是得不偿失的。

所以,其实内存也是有着两个表现形式的:给我们程序员看的,叫虚拟内存;给系统看的,叫真实物理内存;而链接他们之间关系的,叫做页表。 

页表 

虚拟地址空间中的某一变量,在真实物理内存中,只是随机选取了一块未被使用的地址,然后存储了起来。对所有的变量,都是在真实物理空间中随机找一块位置存储;在虚拟地址空间中相邻的变量,在真实物理空间里没有任何逻辑或是空间上的关系。
并且,虚拟地址空间和真实物理内存是一一对应的。在虚拟内存空间中新定义了一个变量a,那么物理内存一定会开辟一个空间,用于存储变量a。我们在查找这个变量a的值的时候,也一定会通过虚拟的地址,查找到真实物理的地址,然后查看变量a的值,这个对应的变量关系,一定是要被保存下来的。 

而每一个变量,都有一个虚拟地址到真实物理内存的对应。一个进程所有变量的对应,全部列在一张表上,当我们要查找一个变量的值时,就先在虚拟内存上找到虚拟地址,然后通过表查找到真实物理内存上对应的地址,最后访问地址查找地址上的值。而这张表,我们就叫他为页表。 

打个比方,每年高考的时候,都会发座次表。一个班的人都会被放在一个表中,第一列是班级序号,第二列是身份证号,第三列是考场的具体位置。

  • 班级序号便是虚拟地址空间,把一个班即一个进程的所有学生数据都放在了一起。
  • 身份证号是班级和考场的对应,你不能在考场上填自己的班级序号,只能通过身份证号确认自己的身份,这便是页表的对应。
  • 而考场号便是自己在所有考生中的位置,考生有这么多,也不可能把整个班的人放在一个考场,都是随机安排,你和你的同桌,就算再班上序号是相邻的,并不代表考场上是相邻的。所有考场就是真实物理内存,而你的考场位置就是物理内存地址。
  • 当你要找你自己的考场位置时,首先要通过虚拟地址空间即班级序号,找到自己的虚拟地址;然后通过身份证的映射,通过虚拟地址对应找到考场的位置

所以子进程在复制数据的时候,复制的是什么?复制的是父进程所有的虚拟地址空间。但是,是不是页表就要单独开一个呢?也不是,因为这太浪费空间了,子进程在复制虚拟地址空间的同时,也复制了父进程的页表。
在没有值被修改的时候,父子进程页表是相同的,指向了同一块物理内存来节省空间。但是一旦某个值没修改了,比如a由20变为了10,那就把a的页表映射单独修改:首先在物理内存中再找一块区间,然后将a的虚拟地址和新的物理内存地址相对应,最后修改新的物理内存。

总结一下:子进程在复制父进程的数据时,同时复制了虚拟地址空间和页表。当有变量进行数值修改的时候,单独为子进程开辟一块真实物理空间,然后单独修改变量到物理地址的映射,其他的映射不变。 


环境变量 

有没有考虑过这样两个问题:

  1. 在许多样例中,主函数的形式都长这个样子:
    int main(int argc,char *argv[],char *env[]);
     但是实际中,我们却没有去写这三个参数,为什么?
  2. 为什么只需要输入ls,系统便知道要干什么,而我们编译好的程序则需要带上./才能运行?

还是分别来看看,其中的原因:

主函数参数 

argc : arguments counter 参数计数器
argv : arguments vector    参数数组
env  : environment             环境 

非主函数传参,直接调用就可以了。但是主函数传参,怎么调用主函数?在一个程序内部,是不可能调用主函数的;而在程序外部,也就是进程之间,通过创建一个新的进程,就等于调用了主函数;而创建进程时输入的参数,就是给主函数传参。 

打个比方:
我们在命令行中输入:ls -a -l
此时便创建了一个名为ls的进程。在创建ls进程的时候,我们命令行输入的,便是给进程传递的参数,

  • 首先,argc表示参数计数器,即计算有多少参数。和C语言中一样,不同字符串用空格隔开,于是就有了ls,-a,-l三个字符串,即三个参数,argc等于3
  • 然后,argv表示参数数组,用来保存传入的参数。比如传入了ls,-a,-l三个参数,于是argv中保存了三个参数:argv[0]="ls",argv[1]="-a",argv[2]="-l"。
  • 但是,是不是意味着argv中只有三个参数呢?其实也不是。因为argv中最后还有一个空指针作为结尾,代表着字符串数组终止了,便于判断参数的个数。所以最终传入的参数: 

所以,ls内部可以理解为: 

int main(int argc,char* argv[])
{
	if(argv[0]!="ls")
		exit(1);

	for(int i=1;i<argc;i++)
	{
		work(argv[i]);//执行对应-a,-l的功能
	}
	return 0;
}

所以,所有命令行参数说白了就是进程,和我们写的程序跑起来没什么区别。我们想让进程去实现类似于命令行传参的功能,也只需要分析argv中的数据便可以了。
但是,这又有了第二个问题:既然两个都是程序,凭什么系统的只需要输入ls就可以,而我们写出来的需要在前面加上./? 

环境 

这就不得不讲主函数的第三个参数:env。env是环境变量,就算我们不主动去传参,系统也会自动传参,举个例子:

  • 我身处中国,和亲人聊天,问我们最高城在哪里,无论谁都可以脱口而出:理塘
  • 我身处中国,和外国友人聊天,问我们最高城在哪里,他可能会思考一下,然后说出:理塘
  • 我身处外国,和老外聊天,问我们最高城在哪里,这个时候他就会问,你是哪个省的?

同一个问题,却给出了不同的答案,为什么?因为环境不同。 

如果我们身处中国,而且是一个中国人,我们思考答案的范围也肯定在中国。而如果我们身处外国,而对方是个外国人,他就无法理解指的是哪个地方的最高城,不知道范围也就无法给出具体的答案。

而系统的环境也是如此。我们身处于某个目录,而操作系统看到的目录,则是成百上千的目录和文件。我们能看到的,是目录下的几个文件;而操作系统看到的,则是所有文件夹中的文件。我们只说文件名而不说范围,一是操作系统不可能每次都去成百上千个文件里找效率极低,二是就算找到了,也会有许多同名文件,并不知道你所指的是哪一个。所以,我们写出来的程序,我们要指明运行的文件,必须要在前面加上./,即指定范围是当前文件夹。 

那为什么ls类似的系统命令,就不需要指明范围呢? 

因为这些命令是系统的,也就是在系统的环境中,如果没有指明范围,系统就会在默认的路径文件夹中去寻找这些文件。
这也就像当其他人询问我们问题时,如果没有给定范围,我们就会在自己的认知领域中去搜索答案;而如果给定了范围,则会在特定的领域去搜索资料。 

环境变量不只这一个用途。系统的环境变量是一张表:

也就是传入主函数的env字符串数组。当系统创建一个新进程的时候,就会把这张表也交给子进程,以user举例:

假如这个文件的拥有者是科比,而今天打开的人是奥尼尔。程序怎么知道打开文件的人是谁?很简单,科比的账户进行操作,环境变量表里的user是Kobe,而奥尼尔的账户进行操作,环境变量表里的user是Shake。当打开文件时,会自动传入env环境变量表,而进程看到env中的user不是Kobe而是Shake,就将其拒之门外,简单用代码表示一下: 

//cat进程的简单模拟
int main(int argc,char* argv[],char* env[])
{
	if(env[3]=="Kobe")
	{
		look();//如果user是user,则可以查看
	}
	else
	{
		exit(1);//否则就不能查看
		printf("你不是科比!");
	}
}


进程替换 

什么是进程替换? 

 

简单来说,就是一个进程在运行,但是中途突然让你去完成另一个进程的任务,于是在不删减进程的条件下,把该进程变成另一个进程,就叫做进程替换。 

进程替换的原理 

一个进程的虚拟地址空间,可以分为数据段和代码段。而区分不同进程的任务,也是数据段和代码段的不同。所以,替换一个进程,自然要从数据段和代码段下手。
打个比方,当你在一家公司干了三年,做的是营销;突然中途给你调离岗位,让你去做后勤,这个时候你会怎么做? 

  1.  首先,你会去查看公司的工作手册,看看新工作是要做些什么,应该咋做
  2. 然后,你会重新规划你的工作流程图,去适应你的新任务
  3. 接着,你会清除掉岗位上的所有资料,为新工作腾出空间来放新资料
  4. 最后,你会递交你的离职申请书,nmd怎么彼时这么多

同样,如果是一个进程切换到了新的任务,进程会怎么做?

  1. 首先,进程会找到新任务的代码,了解新任务应该怎么运行
  2. 然后,进程会将代码段的旧代码替换为新代码,从新代码第一行开始运行
  3. 接着,进程会清理掉数据段的所有旧数据
  4. 最后,进程会时不时给你报错一下,来证明他还活着 

但是,进程被替换了,他的pid等一系列进程基本数据还是不会变的。就像你换了工作,你的身份证号,你的工号,你的长相和你的钱包,会有太大变化吗? 

进程替换函数 

有六种exec开头的函数,而exec即execute change,可执行程序替换 

后面出现的字母,其实也是单词的组合:

  • l,list,参数采用列表
  • v,vector,参数采用数组
  • p,path,采用系统环境变量
  • e,env,使用传递的环境变量 

函数的使用,就和我们命令行中使用一样,打个比方: 

不同函数的区别,可以看以下样例代码: 

#include <unistd.h>

int main()
{
char *const argv[] = {"ps", "-ef", NULL};

char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

execl("/bin/ps", "ps", "-ef", NULL);

// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);

// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);

execv("/bin/ps", argv);

// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);

// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);

}


 

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-17 21:20:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-17 21:20:02       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-17 21:20:02       58 阅读
  4. Python语言-面向对象

    2024-07-17 21:20:02       69 阅读

热门阅读

  1. [C++11] 模板函数的默认模板参数

    2024-07-17 21:20:02       17 阅读
  2. python-Web

    2024-07-17 21:20:02       21 阅读
  3. 企业和个人在网络安全方面需承担哪些责任?

    2024-07-17 21:20:02       18 阅读
  4. mysql高版本(8.0+)group_by报错的处理方法

    2024-07-17 21:20:02       18 阅读
  5. arm64机器指令转换为汇编指令

    2024-07-17 21:20:02       21 阅读
  6. 【Python Cookbook】S03E07 处理无穷大以及NaN

    2024-07-17 21:20:02       18 阅读
  7. 构建新纪元:Gradle中Kotlin插件的配置全指南

    2024-07-17 21:20:02       22 阅读
  8. 软设之命令模式

    2024-07-17 21:20:02       21 阅读
  9. Linux系统中调试蓝牙的常用命令

    2024-07-17 21:20:02       19 阅读
  10. C++中调用Pytorch模型

    2024-07-17 21:20:02       17 阅读
  11. 若依自定义文件上传下载

    2024-07-17 21:20:02       17 阅读