Bootstrap

Linux笔记---进程:程序地址空间

1. 地址空间

程序地址空间是指程序在执行期间可以访问的内存范围。它由操作系统为每个进程分配,以确保进程之间不会相互干扰。地址空间包含了程序所需的所有内存区域,包括代码、已初始化和未初始化的数据、堆(heap)、栈(stack)等。

 2. 虚拟地址

什么是虚拟地址呢?我们在Linux笔记---进程:初识进程-CSDN博客中谈到过虚拟地址这一概念,现在再次回顾一下当时遇到的问题:

#include <stdio.h>
// fork函数包含在下面两个头文件中
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("我是一个进程,我的pid=%d,我即将创建子进程...\n", getpid());
    int id = fork();
    if(id == 0)
    {
        printf("我是一个子进程,我的pid=%d,我父进程的pid=%d,我得到的id=%d,&id=%p\n", getpid(), getppid(), id, &id);
    }
    else
    {
        printf("我是一个父进程,我的pid=%d,我子进程的pid=%d,我得到的id=%d,&id=%p\n", getpid(), id, id, &id);
    }
    return 0;
}

由于fork函数返回值不同,对id写入的内容不同,导致id发生了写时拷贝,可以看到父子进程的id值确实是不同的。但问题是,二者的地址竟然是完全相同的?

当时我们说,这是因为这里的地址其实是虚拟地址,其物理地址可能指向不同的空间。

我们先思考一个问题,在同一次程序运行的过程中,同一个变量的地址会发生变化吗?当然不会。

既然子进程继承了父进程的数据,那么出现这样的结果似乎完全是在意料之中的,但问题是,这是如何做到的呢?

2.1 虚拟地址空间

实际上,每个进程都会有一个属于自己的虚拟地址空间,我们在程序中看到的、使用的,全部都是虚拟地址。虚拟地址空间中的地址会通过页表映射到物理地址空间。

这使得每个程序都认为自己能够使用整个内存空间。

程序地址空间通过虚拟内存和地址映射技术实现了进程的内存隔离,保障了多任务操作系统的安全和可靠性。例如,在一个简单的程序中,通过父进程创建子进程,并使用一个全局变量来证明虽然父子进程共用一套代码,但是数据是分离开的。这是因为每个进程都有自己独立的地址空间,通过页表映射到不同的物理内存上。

页表是操作系统内核用来管理虚拟地址和物理地址之间映射的一个数据结构。它的核心作用是支持虚拟内存,使得每个进程可以在自己的独立虚拟地址空间中运行,增强了内存隔离和安全性。

2.2 mm_struct

mm_struct是Linux内核中的一个数据结构,用于描述进程的内存管理信息,包括进程的虚拟地址空间布局、页表信息、内存区域等。它是内核中内存管理的核心数据结构之一,每个进程都有一个对应的mm_struct结构,用于管理该进程的内存空间。

struct task_struct
{
    /*...*/
    //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,对内核线程来说这部分为NULL。
    struct mm_struct *mm; 
    // 该字段是内核线程使用的。
    // 当该进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,
    // 这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
    struct mm_struct *active_mm; 
    /*...*/
}

struct mm_struct
{
    /*...*/
    struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
    struct rb_root mm_rb; /* red_black树 */
    unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
    /*...*/

    // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
    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;
    /*...*/
}
  • 虚拟区间:指进程的虚拟地址空间中的一个连续区域,这个区域具有特定的属性,如访问权限、是否映射到物理内存、是否与文件关联等。虚拟区间通常用于描述进程的代码段、数据段、堆区、栈区以及内存映射区域等。
  • mmap:当虚拟区间较少时采用单链表进行管理,mmap指向这个链表。
  • mm_rb:当虚拟区间较多时采用红黑树进行管理,mm_rb指向这棵树。
struct vm_area_struct {
    unsigned long vm_start; //虚存区起始
    unsigned long vm_end; //虚存区结束
    struct vm_area_struct* vm_next, * vm_prev; //前后指针
    struct rb_node vm_rb; //红⿊树中的位置
    unsigned long rb_subtree_gap;
    struct mm_struct* vm_mm; //所属的 mm_struct
    pgprot_t vm_page_prot;
    unsigned long vm_flags; //标志位
    struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } shared;
    struct list_head anon_vma_chain;
    struct anon_vma* anon_vma;
    const struct vm_operations_struct* vm_ops; //vma对应的实际操作
    unsigned long vm_pgoff; //⽂件映射偏移量
    struct file* vm_file; //映射的⽂件
    void* vm_private_data; //私有数据
    atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
    struct vm_region* vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy* vm_policy; /* NUMA policy for the VMA */
#endif
    struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

3. 虚拟地址空间的优势

  • 地址空间扩展:使得程序在编写和运行时无需过于担心物理内存的实际容量限制。
  • 内存保护:允许操作系统为不同的程序(进程)设置不同的内存访问权限。
  • 内存共享:不同的进程可以通过虚拟内存机制共享某些内存区域。
  • 进程管理与内存管理解耦:将实际的物理内存和程序使用的内存进行了分离,就像上文中有页表的那张图,以页表为分割,左边进行进程管理,右边进行内存管理,二者互不干扰并通过页表连接起来。

地址空间拓展与解耦的理解

假设某台计算机总的内存大小是128M,现在同时运行两个程序A和B,A需占用内存10M,B需占用内存110。计算机在给程序分配内存时会采取这样的方法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B。

若此时,占用20M的程序C也加入进来,计算机就无法为其分配空间,进程也就无法启动。

但如果程序采用虚拟地址空间,C进程在创建时就可以暂时先不加载代码和数据,仅仅将其task_struct和页表(此时仅有虚拟地址)维护起来(挂起状态)。

当C进程获得CPU资源时再将其他进程挂起,而将C进程的代码和数据加载到内存中,此时再将虚拟地址映射到实际的物理地址。

这样,尽管程序所需占用的内存空间大于计算机的内存空间,也能保证这些进程同时被启动,因为进程管理与物理内存的管理是被分割的。

4. 总结

Linux虚拟地址空间是操作系统为每个进程提供的一组虚拟地址,这些地址在进程看来是连续的,但实际上它们会被映射到物理内存的不同位置。虚拟地址空间的目的是使每个进程都认为自己独占了整个内存,从而简化内存管理和提高安全性。通过页表和MMU的配合,操作系统能够有效地管理和保护内存资源,同时提供了灵活的内存分配和共享机制。

因为有地址空间的存在,所以我们在C、C++语言上new, malloc空间的时候,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这是由操作系统自动完成,用户包括进程完全0感知!

;