进程地址空间详解

进程地址空间


image-20211128195336713

我们在学习C语言期间,经常可以提及到这些区域,有一个问题:这里的地址空间是内存吗?答案是这里的地址空间并不是内存。这里的地址空间是进程地址空间,下面我们就讲解进程地址空间。

这段空间中自下而上,地址是增长的,栈是向地址减小方向增长(栈是先使用高地址),而堆是向地址增长方向增长(堆是先使用低地址),堆栈之间的共享区,主要用来加载动态库。

接下来我们来说明四个问题:

  1. 验证地址空间的基本排布
  2. 进程地址空间究竟是什么?
  3. 为什么要存在地址空间?
  4. 地址空间和物理内存之间的关系
验证地址空间的基本排布
#include<stdio.h>
#include<stdlib.h>
int g_unval;//未初始化
int g_val = 100;//初始化
int main(int argc,char *argv[],char *env[])
{
    printf("code addr:           %p\n",main);//代码区起始地址
    const char* p = "hello bit";//p是指针变量(栈区),p指向字符常量h(字符常量区)
    printf("read only :          %p\n",p);
    printf("global val:          %p\n",&g_val);
    printf("global uninit val:   %p\n",&g_unval);
    char *q = (char *)malloc(10);
    printf("heap addr:           %p\n",q);
    
    printf("stack addr:          %p\n",&p);//p先定义,先入栈
    printf("stack addr:          %p\n",&q);
    
    printf("args addr            %p\n",argv[0]);//命令行参数
    printf("args addr            %p\n",argv[argc-1]);
    
    printf("env addr:            %p\n",env[0]);//环境变量
    return 0;
}

image-20211128202507130

我们可以看到代码区的地址是最小的,这里就验证了地址空间的基本排布:p和q都是定义在栈区的,p先定义,先入栈,可以看到p的地址大于q,说明了栈是先使用高地址再使用低地址。

这里我们首先得出两点结论:

1、进程地址空间不是内存

2、进程地址空间,会在进程的整个生命周期内一直存在,直到进程退出

这也就解释了全局变量为什么会一直存在,原因是未初始化数据,初始化数据,这些区域是一直存在的

进程地址空间究竟是什么?

下面我们通过一个代码来看一个现象,我们定义了一个全局变量,fork创建一个子进程,让父进程和子进程完成自己的任务,在子进程中定义count来计数,当子进程的打印任务进行到第五次时,让子进程将这个全局变量改成100:

#include<stdio.h>
#include<unistd.h>
int g_val = 0;
int main()
{
    printf("begin.....%d\n",g_val);
    pid_t id = fork();
    if(id==0)
    {
        //child
        int count = 0;
        while(1)
        {
    		printf("child: pid: %d,ppid: %d, g_val:%d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
            sleep(1);
            count++;
            if(count == 5)
            {
                g_val = 100;
            }
            
        }
        
    }
    else if(id>0)
    {
        //father
        while(1)
        {
             printf("father: pod: %d,ppid: %d, g_val:%d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
            sleep(1);
        }
       	
    }
    else
    {
        //todo
    }
    return 0;
}

image-20211128204600905

代码共享,所以看到前五次打印的g_val的地址都是一样的,这我们不意外,等到了第六次时,我们发现父进程g_val依然是0,子进程的g_val变成了100,因为我们将它改了,这也不意外,因为前面说了,父子进程之间代码共享,而数据是各自私有一份的(写时拷贝),但是令人奇怪的是地址竟然是一样的!

如果我们看到的地址,是物理地址,这种情况可不可能呢?

这是绝对不可能的,如果可能,那么父子进程在同一个地址处读取数据怎么会是不同的值呢?所以,我们曾经所学到的所有的地址,绝对不是物理地址

其实这种地址本质就是一种虚拟地址!

任何我们学过的语言,里面的地址都绝对不会是物理地址,虚拟地址这种地址是由操作系统给我们提供的,操作系统如何给我们提供呢?既然这种地址是虚拟地址,那么一定有某种途径将虚拟地址转化为物理地址,因为数据和代码一定在物理内存上,因为冯诺依曼规定任何数据在启动时必须加载到物理内存,所以肯定需要将虚拟地址转化成物理地址,这里的转化工作由操作系统完成,所有的程序都必须运行起来,运行起来之后,该程序立即变成了进程,那么刚刚打印的虚拟地址大概率和进程有某种关系

我们在上学期间,经常可能会和同桌画三八线,比如一张课桌是100cm,我们用一把尺子来划分区域,女孩的区域是0,50,男孩的区域是50,100,那么我们再计算机当中怎么描述这个事情呢?我们可以这样定义:

struct area
{
	unsigned long start;
	unsigned long end;
};
struct area girl = {0,50};
struct area boy = {50,100};

此时我们就划分好了区域,这时,不管是男孩还是女孩,大脑里都有了这样的一个区域:

image-20211128210824535

当女孩觉得自己活动范围不够,想扩大自己的区域时,就可以调整自己认为的[start,end],划分三八线的过程,就是划分区域的过程,调整区域的过程,本质就是调整自己认为的[start,end]

其中我们将桌子认为是物理内存,男孩和女孩认为是每一个进程,而男孩和女孩本质上都认为自己有一把尺子(脑海里的尺子),这把尺子就是进程地址空间,男孩想放自己的书包、铅笔等物品时,男孩就在自己的进程地址空间再划分区域放自己的物品。

那么如何划分进程地址空间的区域呢?在Linux当中,进程地址空间本质上是一种数据结构,是多个区域的集合。

在Linux内核中,有这样一个结构体:struct mm_struct,在这个结构体去表示我们开始说的一个一个的区域呢?这样去表示:

struct mm_struct
{
    unsigned long code_start;//代码区
    unsigned long code_end;
    
    unsigned long init_start;//初始化区
    unsigned long init_end;
    
    unsigned long uninit_start;//未初始化区
    unsigned long uninit_end;
    
    unsigned long heap_start;//堆区
    unsigned long heap_end;
    
    unsigned long stack_start;//栈区
    unsigned long stack_end;
    //...等等
}

在上面的例子中,男孩脑海里有一把尺子,想着自己拥有桌子的一半,女孩脑海里也有一把尺子,想着自己也拥有桌子的一半,而此时我们改变了:男孩和女孩关系比较好,不进行什么划分三八线,男孩脑海里有一把尺子,想着自己拥有0-100cm的桌子,女孩脑海里有一把尺子,想着自己也拥有0-100cm的桌子,他们在放东西时,只要记住了尺子的刻度就可以了。

为了更深一步的理解进程地址空间,我们再来举一个例子:

比如有一个富豪,他拥有10个亿的身家,这个富豪有10个私生子,这10个私生子互相并不知道自己的存在,富豪对自己的每一个私生子都说孩子你好好学习,老爸现在有10个亿的家产,以后就全是你的,请问在这十个私生子的视觉来看,他们认为他们有多少的家产?当然是10亿,当每个私生子向这个富豪要钱时,只要能接受,富豪肯定都会给,不能接受,富豪可以直接拒绝,在这个例子中富豪给每个私生子脑海里建立了虚拟的10个亿,此时每个私生子都认为自己有10个亿,每个人要的钱都是不一样的。

在这个例子中:富豪称之为操作系统,私生子称之为进程,富豪给私生子画的10亿家产,当前私生子的地址空间,对比言之:操作系统默认会给每个进程构成一个地址空间的概念(32位下,地址空间是从000000…0000到FFFFFF…FFF)4GB的空间,每个进程都认为自己有4GB的空间,每个进程都可以向内存申请空间,只要能接受都会给你,不能接受操作系统会直接拒绝,但是并没有什么影响,进程依旧认为自己有4GB的空间。

再回到男孩和女孩的例子,我们的进程地址空间就相当于是那把尺子,而尺子是有刻度的,进程地址空间也是从全000的地址到全FFF的地址,可以在这上面进行区域划分:比如代码区:[code_start,code_end],比如代码区的地址区间是这个:[0x10000,0x20000],那么区间的每一个地址单位就称为虚拟地址

总结

进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,内核:struct mm_struct,这样的每个进程,都认为自己独占系统内存资源(每个私生子都认为自己独占10亿家产),地址空间区域划分本质:将线性地址空间划分成为一个一个的area,[start,end]。虚拟地址本质,在[start,end]之间的各个地址叫做虚拟地址

地址空间和物理内存之间的关系

我们写了三个程序,将这三个程序运行起来,生成了可执行程序,此时系统存在三个进程,我们有三个task_struct结构体,那么对应的三个进程都有各自的进程地址空间mm_struct,这三个task_struct里面各自会有一个指针指向对应的进程地址空间,我们知道可执行程序运行起来需要将代码和数据加载到内存当中,那么是怎么加载到内存当中的呢?进程将自己的代码和数据首先放在虚拟地址空间的对应的区域,在这其中会有一种表结构,叫做页表,页表的核心工作就是完成虚拟地址到物理地址之间的映射,最终我们的可执行程序的代码和数据可以加载到物理内存的任意位置,因为最终只需要建立代码和数据与物理内存之间的映射关系,就可以通过虚拟地址找到物理内存的对应地址

不同进程的虚拟地址可以完全一样吗?答案是可以完全一样,因为每个进程都有各自的页表,每个进程都是独立的进行通过各自页表中虚拟地址和物理内存的映射关系去找代码和数据

image-20211130175546310

那么不同进程的虚拟地址在页表中映射的物理地址可能会重吗?答案是不会的,如果会重操作系统就挂掉了,有一种可能性会重,但这是我们可以刻意为之,比如创建子进程,让父子进程代码共享:

image-20211130190604080

总结

虚拟地址和物理空间之间是通过页表完成的映射关系

为什么要存在地址空间?

为什么进程不直接访问物理内存呢?这样不行吗?为什么要存在地址空间呢?

  • 保护物理内存不受到任何进程内地址的直接访问,在虚拟地址到物理地址的转化过程中方便进行合法性校验

在早些时候是没有地址空间的:

image-20211130191609309

如果进程直接访问物理内存,那么看到的地址就是物理地址,而语言中有指针,如果指针越界了,一个进程的指针指向了另一个进程的代码和数据,那么进程的独立性,便无法保证,因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,如果里面的数据有账号密码就可以改密码,即使操作系统不让改,也可以读取。

后来就发展出来了虚拟地址空间,那么虚拟地址空间如何避免这样的问题呢?

由上面我们所了解的知识,一个进程有它的task_struct,有地址空间,有页表,页表当中有虚拟地址和物理内存的映射关系,有了页表的存在,虚拟地址到物理地址的一个转化,由操作系统来完成的,同时也可以帮系统进行合法性检测

我们在写代码的时候肯定了解过指针越界,我们知道地址空间有各个区域,那么指针越界一定会出现错误吗?

不一定,越界可能他还是在自己的合法区域。比如他本来指向的是栈区,越界后它依然指向栈区,编译器的检查机制认为这是合法的,当你指针本来指向数据区,结果指针后来指向了字符常量区,编译器就会根据mm_struct里面的start,end区间来判断你有没有越界,此时发现你越界了就会报错了,这是其中的一种检查,第二种检查为:页表因为将每个虚拟地址的区域映射到了物理内存,其实页表也有一种权限管理,当你对数据区进行映射时,数据区是可以读写的,相应的在页表中的映射关系中的权限就是可读可写,但是当你对代码区和字符常量区进行映射时,因为这两个区域是只读的,相应的在页表中的映射关系中的权限就是只读,如果你对这段区域进行了写,通过页表当中的权限管理,操作系统就直接就将这个进程干掉。

所以进程地址空间的存在也使得可以通过start和end以及页表的权限管理来判断指针是否合法访问

  • 将内存管理和进程管理进行解耦

操作系统有四种核心管理:

1、进程管理

2、内存管理

3、驱动管理

4、文件管理

这里我们主要讲的是进程管理和内存管理:

如果没有进程地址空间,进程直接访问物理内存,当进程退出时,内存管理需要尽快将该进程回收,在这个过程当中必须得保证内存管理得知道某个进程退出了,并且内存管理也得知道某个进程开始了,这样才能给他们及时的分配资源和回收资源,这就意味着内存管理和进程管理模块是强耦合的,也就是说内存管理和进程管理关系比较大,通过我们上面的理解,如果有了进程地址空间,当一个进程需要资源的时候,通过页表映射去要就可以了,内存管理就只需要知道哪些内存区域(配置)是无效的,哪些是有效的(被页表映射的就是有效的,没有被页表映射的就是无效的),当一个进程退出时,它的映射关系也就没了,此时没有了映射关系,物理内存这里就将该进程的数据设置为无效,所以第二个好处就是将内存管理和进程管理进行解耦,内存管理是怎么知道有效还是无效的呢?比如说在一块物理内存区域设置一个计数器count,当页表中有映射到这块区域时,count就++,当一个映射去掉时,就将count–,内存管理只需要检测这个count是不是0,如果为0,说明它是没人用的。

没有进程地址空间时,内存也可以和进程进行解耦,但是代码会设计的特别复杂,所以最终会有进程地址空间

内存管理是怎么将一些大型数据加载到物理内存的?

内存管理是通过延迟加载的方式加载到物理内存的,什么意思呢?比如说你有一个16GB的大型进程,内存管理首先会给你加载小一部分先供你使用,当你使用完时,会先将进程置为睡眠状态,然后再加载一部分,然后将进程再唤醒,进程再继续使用就可以了。对于用户来说,唯一感觉到的是我的游戏运行的慢了。

  • 让每个进程,以同样的方式(虚拟地址),看待代码和数据,明确程序运行的地址

    目标文件和可执行程序,本身就已经被划分成为了一个个的区域:

image-20211130202009002

磁盘上的可执行程序分区域的每个大小单位为4KB,每个这个大小的数据称为页帧,在物理内存中的每个大小单位也为4KB,每个这个大小的数据称为页框,那么为什么要分区域呢?因为方便生成可执行程序,在这之后其中有一个链接库的过程,如果可执行程序是乱的,那么这个链接过程非常困难,所以需要分好区域,由此进程地址空间才有了区域划分这样的概念,进程的地址空间连续化,也让顺序语句的执行成为了可能:当前语句的起始地址+当前代码的长度就等于下一条语句的地址。如果没有进程地址空间,因为物理内存空余的地方不一定是连续的空间,可能是零散的,那么将可执行程序的数据加载到内存当中时,那么这些数据就是零散的放在各个位置,而这些位置我们又是不确定的,此时很难找到代码和数据的位置了,进程地址空间的存在,进程地址空间又是进行区域划分的,通过页表的映射关系可以很好的找到物理内存,所以这也是存在地址空间的一个理由:让每个进程,以同样的方式(虚拟地址),明确程序运行的地址

最开始我们的四个问题已经全部讲解完,回到最初的那个问题:为什么父进程和子进程的数据不一样,这个我们不意外,因为数据是私有的,但是地址却也是相同的,这是什么原因呢?到达这里我想这个问题已经显而易见了:

image-20211130222152087

image-20211130222120405

此时g_val的虚拟地址没有变化,而子进程的g_val的虚拟地址对物理内存地址的映射已经发生了变化,指向的数据区的g_val已经变为了100。

进程和程序有什么区别??

提到进程需要知道这三个东西:task_struct,mm_struct,页表。进程是加载进内存的程序,由进程常见的数据结构(struct task_struct(控制块) && struct mm_struct(地址空间))和代码数据组成

运行队列、等待队列

task_struct中是包含了很多的进程链接信息的,本质是把进程PCB进行排队的过程

若干PCB在一个队列中进行等待使用CPU的资源,该对称为运行队列

若干PCB在一个队列中进行等待访问磁盘的资源,该对称为等待队列

  • 63
    点赞
  • 174
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
smss.exe  Session Manager 这个进程是不可以从任务管理器中关掉的。 这是一个会话管理子系统,负责启动用户会话。这个进程是通过系统进程初始化的并且对许多活动的, 包括已经正在运行的Winlogon,Win32(Csrss.exe)线程和设定的系统变量作出反映。在它启动这些进程后,它等待Winlogon或者Csrss结束。如果这些过程时正常的,系统就关掉了。如果发生了什么不可预料的事情,smss.exe就会让系统停止响应(就是挂起)。 spoolsv.exe 这个进程是不可以从任务管理器中关掉的。 缓冲(spooler)服务是管理缓冲池中的打印和传真作业。 service.exe 这个进程是不可以从任务管理器中关掉的。 大多数的系统核心模式进程是作为系统进程在运行。 包含很多系统服务 csrss.exe 子系统服务器进程 winlogon.exe 管理用户登录和推出的。而且winlogon在用户按下CTRL+ALT+DEL时就激活了,显示安全对话框。 winmgmt.exe win2000客户端管理的核心组件。当客户端应用程序连接或当管理程序需要他本身的服务时这个进程初始化 lsass.exe 这个进程是不可以从任务管理器中关掉的。 这是一个本地的安全授权服务,并且它会为使用winlogon服务的授权用户生成一个进程。这个进程是过使用授权的包,例如默认的msgina.dll来执行的。如果授权是成功的,lsass就会产生用户的进入令牌,令牌别使用启动初始的shell。其他的由用户初始化的进程会继承这个令牌的。 svchost.exe 包含很多系统服务 !!!->eventsystem,(SPOOLSV.EXE 将文件加载到内存中以便迟后打印等。)(附:Svchost.exe文件对那些从动态连接库中运行的服务来说是一个普通的主机进程名。Svhost.exe文件定位在系统的 %systemroot%\system32文件夹下。在启动的时候,Svchost.exe检查注册表中的位置来构建需要加载的服务列表。这就会使 多个Svchost.exe在同一时间运行。每个Svchost.exe的回话期间都包含一组服务, 以至于单独的服务必须依靠Svchost.exe怎样和在那里启动。这样就更加容易控制和查找错误. Svchost.exe 组是用下面的注册表值来识别。 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Svchost 每个在这个键下的值代表一个独立的Svchost组,并且当你正在看活动的进程时,它显示作为一个单独的 例子。每个键值都是REG_MULTI_SZ类型的值而且包括运行在Svchost组内的服务。每个Svchost组都包含一个 或多个从注册表值中选取的服务名,这个服务的参数值包含了一个ServiceDLL值。 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Service explorer.exe 资源管理器 (internat.exe 托盘区的拼音图标) 就像任务条,桌面等等。这个进程并不是像你想象的那样是作为一个重要的进程运行在windows中,你可以从任务管理器中停掉它,或者重新启动。 通常不会对系统产生什么负面影响。 taskmagr.exe 这个进程就是任务管理器。 System Idle Process 这个进程是不可以从任务管理器中关掉的。 这个进程是作为单线程运行在每个处理器上,并在系统不处理其他线程的时候分派处理器的时间。 mstask.exe 这个进程是不可以从任务管理器中关掉的。 这是一个任务调度服务,负责用户事先决定在某一时间运行的任务的运行。 internat.exe 这个进程是可以从任务管理器中关掉的。 internat.exe在启动的时候开始运行。它加载由用户指定的不同的输入点。输入点是从注册表的这个位置HKEY_USERS\.DEFAULT\Keyboard Layout\Preload 加载内容的。 internat.exe 加载“EN”图标进入系统的图标区,允许使用者可以很容易的转换不同的输入点。 当进程停掉的时候,图标就会消失,但是输入点仍然可以通过控制面板来改变。 附加的系统进程(这些进程不是必要的,你可以根据需要通过服务管理器来增加或减少) regsvc.exe 允许远程注册表操作。(系统服务)->remoteregister winmgmt.exe 提供系统管理信息(系统服务):netinfo.exe->msftpsvc,w3svc,iisadmn tlntsvr.exe->tlnrsvr tftpd.exe 实现 TFTP Internet 标准。该标准不要求用户名和密码。远程安装服务的一部分。(系统服务) termsrv.exe ->termservice dns.exe 应答对域名系统(DNS)名称的查询和更新请求。(系统服务) 以下全是系统服务,并且很少会用到,如果你暂时用不着,应该关掉(对安全有害 ) tcpsvcs.exe  提供在 PXE 可远程启动客户计算机上远程安装 Windows 2000 Professional的能力。(系统服务)->simptcp 支持以下 TCP/IP 服务:Character Generator, Daytime, Discard, Echo, 以及 Quote of the Day。(系统服务) ismserv.exe  允许在 Windows Advanced Server 站点间发送和接收消息。(系统服务) ups.exe   管理连接到计算机的不间断电源(UPS)。(系统服务) wins.exe   为注册和解析 NetBIOS 型名称的 TCP/IP 客户提供 NetBIOS 名称服务。(系统服务) llssrv.exe   License Logging Service(system service) ntfrs.exe   在多个服务器间维护文件目录内容的文件同步。(系统服务) RsSub.exe   控制用来远程储存数据的媒体。(系统服务) locator.exe   管理 RPC 名称服务数据库.->rpclocator(区 RpcSs) lserver.exe   注册客户端许可证。(系统服务) dfssvc.exe   管理分布于局域网或广域网的逻辑卷。(系统服务) clipsrv.exe   支持"剪贴簿查看器",以便可以从远程剪贴簿查阅剪贴页面。(系统服务) msdtc.exe   并列事务,是分布于两个以上的数据库,消息队列,文件系统,或其它事务保护资源管理器。(系统服务) faxsvc.exe   帮助您发送和接收传真。(系统服务) cisvc.exe   Indexing Service(system service)!!! dmadmin.exe   磁盘管理请求的系统管理服务。(系统服务) mnmsrvc.exe   允许有权限的用户使用 NetMeeting 远程访问 Windows 桌面。(系统服务) netdde.exe   提供动态数据交换 (DDE) 的网络传输和安全特性。(系统服务) smlogsvc.exe   配置性能日志和警报。(系统服务) rsvp.exe   为依赖质量服务(QoS)的程序和控制应用程序提供网络信号和本地通信控制安装功能。(系统服务) RsEng.exe   协调用来储存不常用数据的服务和管理工具。(系统服务) RsFsa.exe   管理远程储存的文件的操作。(系统服务) grovel.exe   扫描零备份存储(SIS)卷上的重复文件,并且将重复文件指向一个数据存储点,以节省磁盘空间。(系统服务) SCardSvr.exe   对插入在计算机智能卡阅读器中的智能卡进行管理和访问控制。(系统服务) snmp.exe   包含代理程序可以监视网络设备的活动并且向网络控制台工作站汇报。(系统服务) snmptrap.exe   接收由本地或远程 SNMP 代理程序产生的陷阱消息,然后将消息传递到运行在这台计算机上 SNMP 管理程序。(系统服务) UtilMan.exe   从一个窗口中启动和配置辅助工具。(系统服务) msiexec.exe   依据 .MSI 文件中包含的命令来安装、修复以及删除软件。(系统服务) dllhost.exe   win2000的话,一般是组件com调用的需要dllhost装入内存。所以dllhost.exe负责asp3.0组件装入内存。iis启动后。有一个大约20mb左右的dllhost。如果你的web应用程序不能释放内存。如关掉数据库连接,释放对象。这个dllhost会越来越大。还有一个dllhost是。web客户端的。大约5mb左右。用多层构架的概念来理解,就是一个是dllhost存根,一个是dllhost骨干。com远程访问缺一不可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小赵小赵福星高照~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值