Linux深入理解内存管理29

Linux深入理解内存管理29(基于Linux6.6)---进程虚拟地址介绍

一、概述

Linux 操作系统的内存管理体系中,进程的内存管理是非常核心的概念之一。进程虚拟地址是现代操作系统内存管理的重要组成部分,通过虚拟内存机制,操作系统能够更有效地管理内存资源,提高系统的稳定性、安全性和效率。

在深入理解进程虚拟地址之前,需要先了解一些基础概念:

1. 虚拟内存(Virtual Memory)

虚拟内存是一种内存管理技术,它为每个进程提供一个独立的内存空间。这意味着每个进程看到的内存地址是虚拟地址,而不是物理内存的实际地址。操作系统通过硬件支持的内存管理单元(MMU,Memory Management Unit)将虚拟地址映射到物理内存地址。

虚拟内存的主要优势包括:

  • 隔离性:每个进程拥有自己独立的虚拟地址空间,避免了进程间的内存干扰,提高了系统的稳定性。
  • 扩展性:操作系统可以使用磁盘空间作为虚拟内存的一部分(如通过交换空间或交换文件),从而有效扩大可用内存。
  • 共享内存:不同的进程可以共享同一块物理内存区域,而它们的虚拟地址空间中仍然是独立的。

2. 虚拟地址空间的结构

在 Linux 操作系统中,每个进程的虚拟地址空间通常被划分成多个区域。虚拟地址空间结构对于内存管理至关重要,它决定了如何有效地划分和使用内存。大致上,一个进程的虚拟地址空间包括以下几个主要部分:

  • 用户空间(User Space)

    • 用户空间地址是进程用来执行其代码和存取数据的地方,通常占用虚拟地址的较低部分。
    • 这部分内存包括进程的代码段、数据段、堆、栈等。
  • 内核空间(Kernel Space)

    • 内核空间是操作系统内核用来运行的地方,通常位于虚拟地址的高端部分。
    • 进程无法直接访问内核空间,内核通过系统调用和中断机制与用户空间进行交互。
  • 虚拟地址范围

    • 在 32 位系统中,虚拟地址通常是 4GB 的空间,其中低 3GB(0~3GB)用于用户空间,高 1GB(3GB~4GB)用于内核空间。
    • 在 64 位系统中,虚拟地址的空间非常大(理论上可以达到 2^64),通常操作系统将地址分成更细的区域。

3. 虚拟地址空间的主要部分

进程的虚拟地址空间结构可以进一步细分为多个区域:

  1. 文本段(Text Segment)

    • 存储进程的执行代码(即程序的指令)。这个部分是只读的,防止进程修改自己的代码。
  2. 数据段(Data Segment)

    • 存储已初始化的全局变量和静态变量。
  3. BSS段(BSS Segment)

    • 存储未初始化的全局变量和静态变量。这部分的内存空间在程序运行时会被操作系统初始化为零。
  4. 堆(Heap)

    • 堆用于存储动态分配的内存,如通过 malloc()new 分配的内存区域。
    • 堆的大小是动态的,可以随着程序的需要而增长或缩小。
  5. 栈(Stack)

    • 栈用于存储局部变量、函数调用的返回地址等。
    • 栈的大小通常是固定的,每次函数调用都会在栈中分配一块新的内存区域,函数返回时相应的栈空间被释放。
  6. 内存映射区域(Memory-mapped Region)

    • 存储由 mmap() 系统调用映射的文件内容或设备内存。
    • 共享库和共享内存区域也会映射到这个区域。

4. 进程虚拟地址的映射

虚拟地址空间并不是直接与物理内存一一对应的,操作系统利用页表将虚拟地址映射到物理地址。这一过程由硬件的内存管理单元(MMU)完成。

  • 页表(Page Table):是操作系统用来存储虚拟地址和物理地址之间映射关系的数据结构。每个进程都有一个独立的页表。
  • 页表项(Page Table Entry, PTE):每一项存储了虚拟地址到物理地址的映射信息。
  • 页面(Page):内存被分割成固定大小的块,通常为 4KB(在 64 位系统中也可能为更大的页面)。这些块称为页面。
  • 段(Segment):在一些系统中,内存不仅通过页面来管理,还可能使用段来管理。段更大范围地划分内存,用来存储不同类型的数据(如代码段、数据段)。

5. 虚拟地址转换

虚拟地址转换的基本过程如下:

  1. 虚拟地址分为页号和页内偏移

    • 页号:用于查找页表项,页表项存储虚拟页到物理页的映射。
    • 页内偏移:表示在该页内的具体位置。
  2. 查找页表项

    • 根据虚拟地址中的页号查找对应的页表项,得到物理页号。
  3. 计算物理地址

    • 将物理页号与页内偏移结合起来,得到最终的物理地址。

6. 内核的虚拟地址空间的管理

  • 用户进程的虚拟地址空间是Linux的一个重要抽象,它向每个运行进程提供了同样的系统,每个应用程序都有自身的地址空间,与所有的应用程序分割开,不会干扰到其他进程内存的内容。
  • 在内核的虚拟地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此有一定的距离,内核需要一些数据结构来有效的管理这些分布的段。
  • 地址空间中只有极少的一部分与物理页直接关联,不经常使用的部分,仅当必要时与页帧关联
  • 内核无法信任用户进程,所以各个操作系统用户地址空间的操作伴随着各种检查,以确保程序的权限不会超出应有的限制,进而危及到系统的稳定性和安全性。
  • 用户空间的内存分配方法。

二、进程虚拟地址空间

理论上,64Bit地址支持访问空间是[0, 0xFFFF FFFF FFFF FFFF],而实际上现有的应用程序都不会用这么大的地址空间,而现在ARM64芯片上也不支持访问这么大的地址空间,现有的架构最大支持访问48bit的地址空间。而对于进程有用户态和内核态,同样进程地址空间包括用户地址空间和内核地址空间,用户态访问用户地址空间。对于各个进程的虚拟地址空间起始于地址0,延伸到TASK_SIZE - 1,其上是内核地址空间。

  • 在ARM32系统上,地址空间范围为4GB,总的地址空间通常按照3:1划分,各个用户空间进程可用的部分是3GB。
  • 在ARM64系统上,64位虚拟地址中,并不是所有位都用上,除了高16位用于区分内核空间和用户空间外,有效位的配置可以是:36, 39, 42, 47。这可决定Linux内核中地址空间的大小。比如以采用4KB的页,4级页表,虚拟地址为48位的系统为例(从ARMv8.2架构开始,支持虚拟地址和物理地址的大小最多为52位),其虚拟地址空间的范围为256TB ,按照1:1的比例划分,内核空间和用户空间各占128TB。

对于用户程序只能访问整个地址空间的下半部分,不能访问内核部分。同时无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是相同的。

2.1、进程地址空间的布局

一个进程通常由加载一个elf文件启动,而elf文件是由若干segments组成的,同样的,进程地址空间也由许多不同属性的segments组成。虚拟地址空间中包含了若干区域,其分布方式特定于体系结构,但所有的方法都有下列共同的特点,如下图所示:

text段:包含了当前运行进程的二进制代码,其起始地址在IA32体系中中通常为0x08048000,在IA64体系中通常为0x0000000000400000。

  • data段:包含程序显式初始化的全局变量和静态变量,即已初始化且初值不为0的全局变量(也包括静态全局变量)和静态局部变量,这些数据是在程序真正运行前就已经确定的数据,所以可以提前加载到内存保存好。
  • bss段:未初始化的全局变量和静态变量,这些变量的值是在程序真正运行起来并为其赋值后才能确定的,所以程序加载之初,只需要记录它的内存地址和所需大小。出于历史原因,这段空间也称为 BSS 段。
  • heap段:存储动态分配的内存中的数据,堆用于存储那些生存期与函数调用无关的数据。如用系统调用 malloc 申请的内存便在堆上,这些申请的内存在不需要时必须手动释放,否则便会出现内存泄漏。
  • stack段:用于保存局部变量和实现函数/过程调用的上下文,它们的大小都是会在进程运行过程中发生变化的,因此中间留有空隙,heap向上增长,stack向下增长,因为不知道heap和stack哪个会用的多一些,这样设置可以最大限度的利用中间的空隙空间。进程每调用一次函数,都将为该函数分配一个栈帧,栈帧中保存了该函数的局部变量、参数值和返回值。
  • 文件映射段:这个段比较特殊,是mmap()系统调用映射出来的。mmap映射的大小也是不确定的。3GB的虚拟地址空间已经很大了,但heap段, stack段,mmap段在动态增长的过程还是有重叠(碰撞)的可能。为了避免重叠发生,通常将mmap映射段的起始地址选在TASK_SIZE/3(也就是1GB)的位置。如果是64位系统,则虚拟地址空间更加巨大,几乎不可能发生重叠。

例子内存空间布局如下图所示:

2.2、建立布局

进程在运行过程中的内存空间分布情况,那么如何建立起这种内存空间呢?首先,在Linux系统中运行一个可执行的ELF文件时,内核首先需要识别这个文件,然后解析并装载它以构建进程的内存空间,最后切换到新的进程来运行。

首先,我们来看一下elf文件的格式,Section头表包含了描述文件Sections的信息。每个Section在这个表中有一个入口,每个入口给出了该Section的名字,大小等信息。同时可执行文件有一个头部,里面有一些关键信息,Entry point Address,入口地址,即程序的起点,后面有一些代码,数据:

当在linux的shell命令中执行某个elf可执行文件的时候,linux系统是如何装载该ELF并执行的呢?其主要是以下几个步骤:

  1. 创建新进程:首先在用户层面,shell进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用来执行指定的ELF。
  2. 检查可执行文件的类型:当进入execve()系统调用之后,Linux内核就开始真正的装载工作。在内核中,execve()系统调用相应的入口是sys_execve(),会执行do_execve()查找被执行的文件,如果找到文件,则读取文件的前128个字节,通过来判断该执行文件是哪一种elf文件,例如a.out,java程序,以及脚本开头的文件。
  3. 搜索匹配的装载处理过程:do_execve()读取128个字节的文件头部后,调用search_binary_handle()去搜索和匹配合适的可执行文件,最常见的可执行文件及处理过程如下:
  • ELF可执行文件:load_elf_binary
  • a.out 可执行文件:load_aout_library
  • 可执行脚本程序:load_script()

在装载的过程中,对于可执行文件,应该创建对应的.text段、.data段、stack段等。在Linux中,每个段都用一个vm_area_strcutvm_area_strcut结构体表示,vma是通过一个双向链表串起来,现存的vma按照起始地址依次递增被归入链表中,每个vma是这个链表的一个节点,首先来看一个进程有一个struct mm_struct用来描述进程的内存信息。

include/linux/mm_types.h

struct mm_struct {
	struct {
		struct maple_tree mm_mt;
#ifdef CONFIG_MMU
		unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
		unsigned long mmap_base;	/* base of mmap area */
		unsigned long mmap_legacy_base;	/* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
		/* Base addresses for compatible mmap() */
		unsigned long mmap_compat_base;
		unsigned long mmap_compat_legacy_base;
#endif
		unsigned long task_size;	/* size of task vm space */
		pgd_t * pgd;

#ifdef CONFIG_MEMBARRIER
		/**
		 * @membarrier_state: Flags controlling membarrier behavior.
		 *
		 * This field is close to @pgd to hopefully fit in the same
		 * cache-line, which needs to be touched by switch_mm().
		 */
		atomic_t membarrier_state;
#endif

		/**
		 * @mm_users: The number of users including userspace.
		 *
		 * Use mmget()/mmget_not_zero()/mmput() to modify. When this
		 * drops to 0 (i.e. when the task exits and there are no other
		 * temporary reference holders), we also release a reference on
		 * @mm_count (which may then free the &struct mm_struct if
		 * @mm_count also drops to 0).
		 */
		atomic_t mm_users;

		/**
		 * @mm_count: The number of references to &struct mm_struct
		 * (@mm_users count as 1).
		 *
		 * Use mmgrab()/mmdrop() to modify. When this drops to 0, the
		 * &struct mm_struct is freed.
		 */
		atomic_t mm_count;

#ifdef CONFIG_MMU
		atomic_long_t pgtables_bytes;	/* PTE page table pages */
#endif
		int map_count;			/* number of VMAs */

		spinlock_t page_table_lock; /* Protects page tables and some
					     * counters
					     */
		/*
		 * With some kernel config, the current mmap_lock's offset
		 * inside 'mm_struct' is at 0x120, which is very optimal, as
		 * its two hot fields 'count' and 'owner' sit in 2 different
		 * cachelines,  and when mmap_lock is highly contended, both
		 * of the 2 fields will be accessed frequently, current layout
		 * will help to reduce cache bouncing.
		 *
		 * So please be careful with adding new fields before
		 * mmap_lock, which can easily push the 2 fields into one
		 * cacheline.
		 */
		struct rw_semaphore mmap_lock;

		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;	   /* Total pages mapped */
		unsigned long locked_vm;   /* Pages that have PG_mlocked set */
		atomic64_t    pinned_vm;   /* Refcount permanently increased */
		unsigned long data_vm;	   /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
		unsigned long exec_vm;	   /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
		unsigned long stack_vm;	   /* VM_STACK */
		unsigned long def_flags;

		/**
		 * @write_protect_seq: Locked when any thread is write
		 * protecting pages mapped by this mm to enforce a later COW,
		 * for instance during page table copying for fork().
		 */
		seqcount_t write_protect_seq;

		spinlock_t arg_lock; /* protect the below fields */

		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 */

		/*
		 * Special counters, in some configurations protected by the
		 * page_table_lock, in other configurations by being atomic.
		 */
		struct mm_rss_stat rss_stat;

		struct linux_binfmt *binfmt;

		/* Architecture-specific MM context */
		mm_context_t context;

		unsigned long flags; /* Must use atomic bitops to access */

#ifdef CONFIG_AIO
		spinlock_t			ioctx_lock;
		struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
		/*
		 * "owner" points to a task that is regarded as the canonical
		 * user/owner of this mm. All of the following must be true in
		 * order for it to be changed:
		 *
		 * current == mm->owner
		 * current->mm != mm
		 * new_owner->mm == mm
		 * new_owner->alloc_lock is held
		 */
		struct task_struct __rcu *owner;
#endif
		struct user_namespace *user_ns;

		/* store ref to file /proc/<pid>/exe symlink points to */
		struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
		struct mmu_notifier_subscriptions *notifier_subscriptions;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
		pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_NUMA_BALANCING
		/*
		 * numa_next_scan is the next time that PTEs will be remapped
		 * PROT_NONE to trigger NUMA hinting faults; such faults gather
		 * statistics and migrate pages to new nodes if necessary.
		 */
		unsigned long numa_next_scan;

		/* Restart point for scanning and remapping PTEs. */
		unsigned long numa_scan_offset;

		/* numa_scan_seq prevents two threads remapping PTEs. */
		int numa_scan_seq;
#endif
		/*
		 * An operation with batched TLB flushing is going on. Anything
		 * that can move process memory needs to flush the TLB when
		 * moving a PROT_NONE mapped page.
		 */
		atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
		/* See flush_tlb_batched_pending() */
		atomic_t tlb_flush_batched;
#endif
		struct uprobes_state uprobes_state;
#ifdef CONFIG_PREEMPT_RT
		struct rcu_head delayed_drop;
#endif
#ifdef CONFIG_HUGETLB_PAGE
		atomic_long_t hugetlb_usage;
#endif
		struct work_struct async_put_work;

#ifdef CONFIG_IOMMU_SVA
		u32 pasid;
#endif
#ifdef CONFIG_KSM
		/*
		 * Represent how many pages of this process are involved in KSM
		 * merging.
		 */
		unsigned long ksm_merging_pages;
		/*
		 * Represent how many pages are checked for ksm merging
		 * including merged and not merged.
		 */
		unsigned long ksm_rmap_items;
#endif
#ifdef CONFIG_LRU_GEN
		struct {
			/* this mm_struct is on lru_gen_mm_list */
			struct list_head list;
			/*
			 * Set when switching to this mm_struct, as a hint of
			 * whether it has been used since the last time per-node
			 * page table walkers cleared the corresponding bits.
			 */
			unsigned long bitmap;
#ifdef CONFIG_MEMCG
			/* points to the memcg of "owner" above */
			struct mem_cgroup *memcg;
#endif
		} lru_gen;
#endif /* CONFIG_LRU_GEN */
	} __randomize_layout;

	/*
	 * The mm_cpumask needs to be at the end of mm_struct, because it
	 * is dynamically sized based on nr_cpu_ids.
	 */
	unsigned long cpu_bitmap[];
};

 对于mmap指向的vm_area_struct,其定义如下:

include/linux/mm_types.h

struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */

	/*
	 * Access permissions of this VMA.
	 * See vmf_insert_mixed_prot() for discussion.
	 */
	pgprot_t vm_page_prot;
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 *
	 * For private anonymous mappings, a pointer to a null terminated string
	 * containing the name given to the vma, or NULL if unnamed.
	 */

	union {
		struct {
			struct rb_node rb;
			unsigned long rb_subtree_last;
		} shared;
		/*
		 * Serialized by mmap_sem. Never use directly because it is
		 * valid only when vm_file is NULL. Use anon_vma_name instead.
		 */
		struct anon_vma_name *anon_name;
	};

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_lock &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifdef CONFIG_SWAP
	atomic_long_t swap_readahead_info;
#endif
#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;

vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

至此,可以看出,虚拟内存即为由一个个vm_area_struct结构体,通过链表组装起来的空间,其示意图如下图所示:

用户进程拥有用户空间的地址,其可以通过malloc和mmap等函数来申请内存,malloc和mmap等函数的实现都是基于进程线性区描述struct vm_erea_struct,内核管理进程地址空间的数据结构struct vm_erea_struct,简称VMA。

对于每个进程的内存描述符mm_struct,都有各自的VMA,通过mm->mmap链表将所有的VMA管理起来,同时会记录到mm->mm_rb的红黑树,用于高速查找合并VMA等操作。

三、虚拟内存区域的表示

先来说说task_struct,task_struct是一个结构体,这个结构体非常的庞大,linux下用它来完整的描述一个进程的所有信息。在每装载一个进程的时候,内核就会帮去创建一个新的task_struct结构体。然后知道一个每一个独立的进程都有自己独立的虚拟空间,所以,在task_struct结构体里会有一个struct mm_struct *mm成员,这个mm成员就是用来描述和管理进程的虚拟空间的。由上图可知,每个区域通过一个vm_eara_struct实例描述,进程的各区域按照以下两种方式排序:

  • 在一个双链表上(开始于mm_struct->mmap)。
  • 在一个红黑树上,跟节点位于mm_rb。

总结来说,简单的理解这三者的关系就是task_struct结构体包含了一个mm_sturcut结构体成员,mm_struct结构体包含了一个vm_area_struct结构体成员mmap,然后这个mmap成员指向一个VMA链表,管理所有的VMA。

用户虚拟地址空间中的每个区域由开始和结束地址描述,现存的区域按照起始地址以递增次序被归入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是非常低效的操作。因此vm_eara_struct的各个实例可以通过红黑树管理,可以显著加快扫描速度。

四、总结


当一个进程要运行起来需要以下的内存结构:

用户态:

  • 代码段、全局变量、BSS
  • 函数栈
  • 内存映射区

内核态:

  • 内核的代码、全局变量、BSS
  • 内核数据结构例如 task_struct
  • 内核栈
  • 内核中动态分配的内存

对于64位的系统,其进程运行状态如下图所示:

Linux 为虚拟内存不同的段,提供了不同的数据结构来描述:

在 Linux 内核眼中所有的进程、线程都是 task 都适用 task_struck 描述。

  • task_struck 数据结构中的 mm 字段(mm_struct 类型)描述了进程或者线程用户态的内存信息;
  • mm_struct 中有映射页的统计信息(总页数, 锁定页数, 数据/代码/栈映射页数等)以及各区域地址
  • mm_struct 维护着 vm_area_struct 的链表,每个链表节点都描述了用户空间虚拟内存的布局划分。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值