『 Linux 』进程地址空间概念

简介: 『 Linux 』进程地址空间概念



🫙 前言

在c/C++中存在一种内存的概念;

一般来说一个内存的空间分布包括栈区,堆区,代码段等等;

且内存是自底向上(由0x000000000xFFFFFFFF);

以该图为例:

该图即为常见的内存分布图;

  • 正文代码段
    正文代码段所存放的数据一般为函数体的二进制代码;
  • 已初始化数据区
    已初始化数据区所存放的数据是在程序中声明的,并且具有初始值的变量,这些变量需要占用存储器的空间;
  • 未初始化数据区
    未初始化数据区所存放的数据是没有进行初始化或者初始值为0的数据,这些数据在存储时不需要额外占用存储器的空间;

  • 堆空间一般为动态空间,即需要成需要手动分配释放;若是分配了堆区空间但使用过后未对堆空间进行手动释放则将会出现内存泄漏的问题;

  • 一般情况下栈所存放的数据基本上都为局部变量;
  • 命令行参数/环境变量
    命令行参数/环境变量,顾名思义该段空间用来存放OS给程序所传递的命令行参数与环境变量;
  • 内核空间
    在Linux操作系统当中,内存的分布一般为其中3G为用户空间,1G为内核空间;
以下操作均在CentOS7_x64环境下进行

存在一个程序 ( mytest ) :

int init = 10; 
  int uninit; 
int main(int argc,char *argv[],char *env[])
{
  char*ch1= new char[10]; 
  char*ch2= new char[10];
  char*ch3= new char[10];
  char*ch4= new char[10];
  char*ch5= new char[10];
  printf("init : %p\n",&init);//已初始化数据
  printf("uninit : %p\n",&uninit);//未初始化数据
  printf("text : %p\n",main);//正文代码段
  cout<<"--------------"<<endl;
    //堆区
  printf("heap1 : %p\n",ch1);
  printf("heap2 : %p\n",ch2);
  printf("heap3 : %p\n",ch3);
  printf("heap4 : %p\n",ch4);
  printf("heap5 : %p\n",ch5);
  cout<<"--------------"<<endl;
    //栈区
  printf("stack1 : %p\n",&ch1);
  printf("stack2 : %p\n",&ch2);
  printf("stack3 : %p\n",&ch3); 
  printf("stack4 : %p\n",&ch4);
  printf("stack5 : %p\n",&ch5);
  cout<<"--------------"<<endl;
    //命令行参数
  for(int i = 0;i<argc;++i){
    printf("argv[%d] : %p\n",i,argv[i]);
  }
  cout<<"--------------"<<endl;
    //环境变量
  for(int i = 0;env[i];++i){
    printf("env[%d] : %p\n",i,env[i]);
  }
  return 0;
}

从这段代码中可以打印出内存中不同数据的内存分布情况;

但实际上在OS层面中,这些所谓的内存并非物理内存;


🫙 进程地址空间是什么

在上文中说到,进程所访问的地址并不是物理地址;

存在一个程序(证明):

using namespace std;
int tmp = 100;
int main()
{
  pid_t id = fork();
  if(id == 0){
    int s = 5;
    while(1){
      cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;
      sleep(1);
      s--;
      if(!s) tmp = 200;
    }
  }
  else{
    while(1){
      cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;
    sleep(1);
    }
  }
  return 0;
}

在该程序中定义了一个全局变量,并使用fork()函数对该进程创建了一个子进程,同时分别在父子进程中打印该全局变量的值与地址;

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

当五秒过后,子进程修改了全局变量的值;

可在父进程当中的这个全局变量并未被更改,且父子进程中所显示的这个全局变量tmp地址相同;

然而实际上,一个程序在运行的过程中所使用的内存地址为虚拟地址(线性地址);

在过去的计算机中,进程对于内存的访问是以直接访问的形式,即运行程序时程序载入至内存当中称为进程,CPU根据进程中的代码数据对内存的各个地址(物理地址)进行操作;

但是由于访问的是物理内存地址,所以若是程序在内存当中误操作则会导致某些进程的崩溃;

这种操作是十分不安全的操作;

所以为了保证安全性同时也保证进程间的独立性,现在的OS当中,出现了进程地址空间的概念;

每个进程都存在一个称为进程地址空间的数据结构(mm_struct结构体);

在这个结构体当中以一种类似于区间的方式模拟出地址(在Linux2.6的版本中使用unsigned long类型实现);

/*释放线性区的调用方法*/
 void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
    unsigned long mmap_base;    /* base of mmap area ,内存映射区的基地址*/
    unsigned long task_size;    /* size of task vm space */
    unsigned long cached_hole_size;   /* if non-zero, the largest hole below free_area_cache */
    unsigned long free_area_cache;    /* first hole of size cached_hole_size or larger */
    pgd_t * pgd;                            /* 页表目录指针*/
    atomic_t mm_users;      /* How many users with user space?,共享进程的个数 */
    atomic_t mm_count;      /* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */
    int map_count;        /* number of VMAs ,线性区个数*/
    struct rw_semaphore mmap_sem;
    spinlock_t page_table_lock;   /* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/
    struct list_head mmlist;    /* List of maybe swapped mm's.  These are globally strung
                         * together off init_mm.mmlist, and are protected
                         * by mmlist_lock
                         */
    unsigned long hiwater_rss;  /* High-watermark of RSS usage,进程拥有的最大页表数目 */
    unsigned long hiwater_vm; /* High-water virtual memory usage ,进程线性区的最大页表数目*/
    unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    unsigned long start_code, end_code, start_data, end_data;     /*维护代码区和数据区的字段*/
    unsigned long start_brk, brk, start_stack;       /*维护堆区和栈区的字段*/
    unsigned long arg_start, arg_end, env_start, env_end;  /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/
    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

除此之外在进程地址空间这个结构体中有一个指针,这个指针所指向的位置即为页表;

所谓的页表就是一种映射关系,这种映射关系以一种key/value的模型将对应的物理地址与虚拟地址进行一种存储,在查找或访问时将访问至虚拟地址,通过该虚拟地址通过页表的key/value模型找到其对应的物理内存再进行访问;

在CPU中存在一个内存管理单元(MMU),这个内存管理单元是CPU中的一个模块,这个模块具体的作用为负责虚拟地址到物理地址的转换;

以该图为例,其中task_struct表示PCB结构体,即进程控制块;

mm_struct即为该进程的进程地址空间,mm_struct中的pgd即为页表;


🫙 写时拷贝

当多个进程或线程共享同一块内存时,内核会使用写时拷贝来优化内存的复制行为;

当有一个进程尝试修改共享内存页面时,Linux内核会触发写时拷贝机制;

它会为修改的进程创建一个新的私有副本,并将修改的内容写入新的副本中,而不是立即修改原始的共享页面;

以该例子为例:

using namespace std;
int tmp = 100;
int main()
{
  pid_t id = fork();
  if(id == 0){
    int s = 5;
    while(1){
      cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;
      sleep(1);
      s--;
      if(!s) tmp = 200;
    }
  }
  else{
    while(1){
      cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;
    sleep(1);
    }
  }
  return 0;
}

在该例子中程序运行的结果为:

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

两个进程中的变量的地址相同但其值不同的原因就是在于其所在的虚拟地址相同但页表中虚拟地址所映射的物理地址不同;

在这个程序当中,使用fork()函数创建了子进程,由于子进程是由父进程创建的,所以对应的子进程的PCB结构体继承于父进程,即当父进程创建出一个子进程时,该子进程将会对父进程的PCB结构体进行一次浅拷贝,所以父子进程所对应的代码资源是共享的;

在只读的情况下两个进程的页表所映射至的物理地址也许相同的,而当一个进程要修改该物理内存中的内容时,OS将会重新在物理内存中申请一块空间,同时修改该进程所对应的页表映射关系;


🫙 可执行程序中的虚拟地址

实际在可执行程序当中也存在着所谓的虚拟地址,在一般的教材当中也被称为"逻辑地址";

存在一个程序:

#include<iostream>
using namespace std;
int g_val = 100;
int main()
{
  cout<<&g_val<<endl;
  return 0;
}

这个程序运行之后可以打印出该程序中全局变量g_val的地址;

在Linux中存在一个命令可以打印出一个可执行程序中的逻辑地址(虚拟地址),即objdump;

语法:

objdump -x <executable_file>

在此处配合| grep打印出该可执行程序中的虚拟地址,即:

objdump -x mytest | grep g_val

使用该命令后运行该程序:

$ objdump -x mytest | grep g_val
00000000004007f7 l     F .text  0000000000000015              _GLOBAL__sub_I_g_val
000000000060105c g     O .data  0000000000000004              g_val
$ ./mytest 
0x60105c

在上面的程序当中,程序运行的结果(打印全局变量地址)与使用objdump所显示出磁盘中的全局变量g_val地址相同,由此可见其进程中的虚拟地址与本在磁盘中的虚拟地址相同;

实际上在计算机当中,本质上无论是磁盘中的虚拟地址(逻辑地址)还是在进程当中的虚拟地址都是相同的;

只不过是在进程与磁盘中的表现形式不同;

当程序编译链接完成时生成的可执行程序当中将会存在代码数据等,在这些代码数据当中存在着静态的虚拟地址,这些地址被称作逻辑地址;

当这个程序被执行后即被加载至内存当中成为进程时,进程将会去初始化自身的PCB结构体;相对应的PCB结构体内的各种数据结构也将要被进行维护与初始化;

磁盘中的虚拟地址(逻辑地址)将会初始化PCB结构体中对应的进程地址空间,使得进程地址空间中的虚拟地址与原本磁盘内的虚拟地址(逻辑地址)保持一致;


🫙 物理地址分布方式

在上面的图中可以发现:

在对进程地址空间进行初始化时,真正将虚拟地址与物理地址进行关联的时候,其物理地址并没有按照原本的虚拟地址原模原样的进行对应的初始化;

在对对应物理地址进行初始化时更像是以一种随机的方式;

为了物理内存的安全性,Linux中采用了一种地址空间随机化(ASLR)的一种内存攻击缓存技术;

当对应的进程地址空间的虚拟地址在初始化时通过页表映射至物理内存时将会采用这种方式;

使得对应进程的物理内存地址无法被预测,也保证了进程在运行时的安全性;

相关文章
|
17天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
51 4
linux进程管理万字详解!!!
|
8天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
48 8
|
5天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
17天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
52 4
|
18天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
19天前
|
消息中间件 存储 Linux
|
26天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
31 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
25 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
31 0
Linux c/c++之IPC进程间通信
|
1月前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
28 0
Linux c/c++进程间通信(1)
下一篇
无影云桌面