Linux内核内存管理:系统内存布局-内核空间和用户空间

在Linux系统中,每个内存地址都是虚拟的。它们不直接指向RAM中的任何地址。每当您访问一个内存位置时,都会执行一种转换机制来匹配相应的物理内存。
让我们从一个介绍虚拟内存概念的小故事开始。给定一个旅馆,每个房间都可以有一个电话,每个电话都有一个私人号码。当然,所有安装的电话都是酒店的。他们都不能从酒店外面直接联系上。

如果你需要联系一个房间的住户,比如说你的朋友,他必须给你酒店的总机号码和他所住的房间号码。一旦你给总机打电话并告诉你需要通话的住户的房间号码,接待员就会把你的电话转接到房间里的私人电话上。只有接待员和房间居住者知道私人号码映射:

(switchboard number + room number) <=> private (real) phone number

每当这座城市(或世界上任何地方)的某个人想要联系住在房间里的人,他都必须通过热线。他需要知道正确的酒店热线号码和房间号码。这样,“总机号码”和“房间号码”就是虚拟地址,“私人电话号码”对应的是物理地址。
有一些与酒店相关的规则也适用于Linux:


Hotel

Linux
您不能联系房间内没有私人电话的住户。甚至没有办法尝试这样做。您的电话将会突然结束 您不能访问地址空间中不存在的内存。这将导致段错误
您无法联系不存在的住客,或酒店不知道其入住,或总机找不到其信息的住客 如果您访问未映射的内存,CPU会抛出一个页面错误,OS会处理它
你不能联系已经离开的住客 您不能访问已释放的内存。也许它已经被分配给了另一个进程
许多酒店可能拥有相同的品牌,但位于不同的地点,每个酒店都有不同的热线电话 不同的进程可能有相同的虚拟地址映射到它们的地址空间中,但是指向不同的物理地址
有一本书(或带有数据库的软件)保存着房间号码和私人电话号码之间的映射关系,接待员可以根据需要进行咨询 虚拟地址通过页表映射到物理内存,页表由操作系统内核维护,并由处理器查询

这就是如何想象虚拟地址在Linux系统中工作。

在这一章中,我们将讨论整个Linux内存管理系统,包括以下主题:

  • 内存布局以及地址转换和MMU
  • 内存分配机制(页面分配器、slab分配器、kmalloc分配器,等等)
  • I / O内存访问
  • 将内核内存映射到用户空间,并实现mmap()回调函数
  • 介绍Linux缓存系统
  • 引入设备管理资源框架(devres)

系统内存布局—内核空间和用户空间

在本章中,像内核空间和用户空间这样的术语都是指它们的虚拟地址空间。在Linux系统中,每个进程都拥有一个虚拟地址空间。它是一种memory sandbox 在进程的生命周期内。这个地址空间在32位系统上是4gb(即使在物理内存小于4gb的系统上)。对于每个进程,4gb地址空间被分成两个部分:

  • 用户空间虚拟地址
  • 内核空间虚拟地址

拆分的方式取决于一个特殊的内核配置选项, CONFIG_PAGE_OFFSET ,它定义了内核地址段在进程地址空间中的起始位置。默认情况下,32位系统上的通用值是0xC0000000,但这可能会改变,就像NXP的i.MX6系列处理器一样,它使用0x80000000。在整个章节中,我们将默认考虑0xC0000000。这称为3G/1G分割,其中用户空间使用较低的3gb虚拟地址空间,内核使用剩余的1gb。一个典型进程的虚拟地址空间布局如下所示:

.--------------------------------------. 0xFFFFFFFF
|                                                | (4 GB)
|          Kernel addresses          |
|                                                |
|                                                |
.--------------------------------------.CONFIG_PAGE_OFFSET
|                                                |(x86: 0xC0000000, ARM: 0x80000000)
|                                                |
|                                                |
|        User space addresses     |
|                                                |
|                                                |
|                                                |
|                                                |
'--------------------------------------' 00000000
 
在内核和用户空间中使用的地址都是虚拟地址。不同之处在于访问内核地址需要特权模式。特权模式具有扩展权限。当CPU运行用户空间端代码时,活动进程被称为以用户模式运行;当CPU运行内核空间端代码时,活动进程被称为以内核模式运行。
给定一个地址(当然是虚拟的),您可以使用前面显示的进程布局来区分它是一个内核空间地址还是一个用户空间地址。每个0- 3gb的地址都来自用户空间;否则,它来自内核。
内核与每个进程共享它的地址空间是有原因的:因为每个进程在给定时刻都使用系统调用,这将涉及到内核。将内核的虚拟内存地址映射到每个进程的虚拟地址空间,可以避免在每个进入(或退出)内核的表项上切换内存地址空间的代价。这就是为什么内核地址空间被永久地映射到每个进程之上,以便通过系统调用加速内核访问的原因。
内存管理单元将内存组织成固定大小的单元,称为页。一个页面由4,096个字节(4 KB)组成。即使这个大小在其他系统上可能不同,它在ARM和x86上是固定的,这是我们感兴趣的架构:
  • Memory page, virtual page,或simply page都是用来指固定长度的连续虚拟内存块的术语。相同的名称page被用作内核数据结构来表示内存页。
  • 另一方面,frame(或page frame)指固定长度的连续物理内存块,操作系统在其上映射内存页。每个page frame都有一个数字,称为page frame number(PFN)。给定一个页面,您可以很容易地获得它的PFN,反之亦然,使用 page_to_pfn 和 pfn_to_page 宏,这将在下一节中详细讨论。
  • page table (页表)是用于存储虚拟地址和物理地址映射的内核和体系结构数据结构。键值对page/frame描述页表中的单个条目。这表示一个映射(mapping)。

由于内存页映射到页帧,所以页和页帧的大小是相同的,在我们的例子中是4 K。页面的大小是通过 PAGE_SIZE 宏在内核中定义的。

在某些情况下,您需要内存来实现页面对齐。如果一个内存的地址恰好从一个页面的开头开始,那么这个内存就是页面对齐的。例如,在一个4 K页面大小的系统上,4,096、20,480和409,600是页面对齐内存地址的实例。换句话说,任何地址是系统页面大小的倍数的内存都称为页面对齐的。

 内核地址-低端内存和高端内存的概念

Linux内核有自己的虚拟地址空间,就像每个用户模式进程一样。内核的虚拟地址空间(以3G/1G分割时的大小为1gb)分为两个部分:

  • 低端内存或LOWMEM,即前896 MB
  • 高端内存或HIGHMEM,由顶部128MB表示
                                                                                          Physical mem
Process address space                 +-------------------> +-------------------------+
                                                    |                           |            3200 M        |
                |           |             |
4 GB     +----------------------+  <----------+                 |         HIGH MEM      |
   |            128 MB    |                                       |                                |
     +-----------------------+  <-------------------+     |                                 |
     +-----------------------+  <----------+         |     |                                 |
     |   896 MB              |                 |          +---> +--------------------------+
3 GB    +---------------------+  <-----+     +-------------> +--------------------------+
          |                             |          |                        |         896 MB            |   LOW MEM
          |            /////           |         +-------------->     +--------------------------+
          |                             |
0 GB   +------------------------+
 

低端内存

前896 MB的内核地址空间构成了低端内存区域。在引导早期,内核会永久地映射这896 MB。从该映射产生的地址称为logical addresses(逻辑地址)。这些是虚拟地址,但可以通过减去一个固定偏移量来转换为物理地址,因为映射是永久性的,并且是预先知道的。Low memory与物理地址的下界匹配。您可以将低端内存定义为在内核空间中存在逻辑地址的内存。大多数内核内存函数返回低端内存。事实上,为了满足不同的目的,内核内存被划分为一个区域。实际上,LOWMEM的前16 MB是预留给DMA使用的。由于硬件的限制,内核不能将所有页面视为相同的。然后,我们可以在内核空间中确定三个不同的内存区域:

  • ZONE_DMA : 这包含低于16 MB的内存页帧,预留给Direct Memory Access (DMA)
  • ZONE_NORMAL : 这包含超过16 MB和低于896 MB的页面帧,对于通常的使用
  • ZONE_HIGHMEM : 这包含896 MB以上的内存页帧

这意味着在一个512 MB的系统上,没有ZONE_HIGHMEM, ZONE_DMA有16 MB,并且ZONE_NORMAL为496 MB。

逻辑地址的另一种定义是内核空间中的地址,线性映射到物理地址上,可以通过一个偏移量或应用位掩码将其转换为物理地址。您可以使用 __pa(address) 宏将物理地址转换为逻辑地址,然后用 __va(address) 宏恢复它。

 高端内存

内核地址空间的顶部128MB被称为高端内存,内核使用它临时映射超过1 GB的物理内存。当需要访问大于1GB(或者更准确地说,896 MB)的物理内存时,内核使用这128 MB创建到其虚拟地址空间的临时映射,从而实现能够访问所有物理页面的目标。您可以将高端内存定义为逻辑地址不存在且不会永久映射到内核地址空间的内存。896 MB以上的物理内存根据需要映射到HIGHMEM区域的128 MB。

访问高端内存的映射是由内核动态创建的,并在完成时销毁。这使得高端内存访问更慢。也就是说,由于巨大的地址范围(2的64次方),高端内存的概念在64位系统上不存在,在这里3G/1G的分割不再有意义。

 用户空间地址

 在本节中,我们将通过进程来处理用户空间。每个进程在内核中都表示为struct task_struct的实例(参见include/linux/ schedt .h),它表示和描述一个进程。每个进程都有一个内存映射表,存储在 struct mm_struct 类型的变量中(参见include/linux/mm_type .h)。然后,你可以猜测到每个 task_struct 中至少嵌入了一个 mm_struct 字段。下面这些行是结构体 task_struct 中我们感兴趣的一部分:
struct task_struct {
     [...]
     struct mm_struct *mm, *active_mm;
     [...]
}

内核全局变量 current 指向当前进程。 *mm 字段指向它的内存映射表。根据定义, current->mm 指向当前进程内存映射表。

现在,让我们看看 struct mm_struct 是什么样子的:
struct mm_struct {
    struct vm_area_struct *mmap;
    struct rb_root mm_rb;
    unsigned long mmap_base;
    unsigned long task_size;
    unsigned long highest_vm_end;
    pgd_t * pgd;
    atomic_t mm_users;
    atomic_t mm_count;
    atomic_long_t nr_ptes;
#if CONFIG_PGTABLE_LEVELS > 2
    atomic_long_t nr_pmds;
#endif
    int map_count;
    spinlock_t page_table_lock;
  struct rw_semaphore mmap_sem;
    unsigned long hiwater_rss;
  
    unsigned long hiwater_vm;
    unsigned long total_vm;
    unsigned long locked_vm;
    unsigned long pinned_vm;
    unsigned long data_vm;
    unsigned long exec_vm;
    unsigned long stack_vm;
    unsigned long def_flags;
    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;
    /* Architecture-specific MM context */
    mm_context_t context;
    unsigned long flags;
    struct core_state *core_state;
#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;
};

我故意删除了一些我们不感兴趣的字段。有一些字段我们稍后将讨论:例如pgd,它是一个指向进程的基础(第一个入口)一级表(pgd)的指针,在上下文切换时写入CPU的转换表基址中。无论如何,在继续之前,让我们看看进程地址空间的表示:

                           进程的内存布局

从进程的角度来看,内存映射可以看作是一组专用于连续虚拟地址范围的页表条目。这个 consecutive virtual address range 称为内存区域,或者virtual memory area(VMA)。每个内存映射都由一个开始地址和长度、权限(例如程序是否可以从该内存读取、写入或执行)和相关资源(例如物理页、交换页、文件内容等等)来描述。
mm_struct有两种方式来存储VMA:
  1. 在红黑树中,其根元素由 mm_struct->mm_rb 字段指向
  2. 在一个链表中,第一个元素由 mm_struct->mmap 字段指向

虚拟内存区域(VMA)

 内核使用虚拟内存区域来跟踪进程的内存映射;例如,一个进程有一个VMA用于它的代码,一个VMA用于每种类型的数据,一个VMA用于每种不同的内存映射(如果有的话),等等。vma是处理器独立的结构,具有权限和访问控制标志。每个VMA都有一个起始地址、一个长度,并且它们的大小总是页面大小的倍数(PAGE_SIZE)。VMA由许多页组成,每个页表中都有一个条目。
VMA所描述的内存区域总是几乎连续的,但不是物理上连续的。可以通过命令查看某个进程关联的所有vma通过/proc/<pid>/maps文件,或者在进程ID上使用pmap命令。

 

 

# cat /proc/1073/maps
00400000-00403000 r-xp 00000000 b3:04 6438 /usr/sbin/net-listener
00602000-00603000 rw-p 00002000 b3:04 6438 /usr/sbin/net-listener
00603000-00624000 rw-p 00000000 00:00 0 [heap]
7f0eebe4d000-7f0eebe54000 r-xp 00000000 b3:04 11717
/usr/lib/libffi.so.6.0.4
7f0eebe54000-7f0eec054000 ---p 00007000 b3:04 11717
/usr/lib/libffi.so.6.0.4
7f0eec054000-7f0eec055000 rw-p 00007000 b3:04 11717
/usr/lib/libffi.so.6.0.4
7f0eec055000-7f0eec069000 r-xp 00000000 b3:04 21629 /lib/libresolv-2.22.so
7f0eec069000-7f0eec268000 ---p 00014000 b3:04 21629 /lib/libresolv-2.22.so
[...]
7f0eee1e7000-7f0eee1e8000 rw-s 00000000 00:12 12532 /dev/shm/sem.thkmcp-231016-sema
[...]

前面的每一行都代表一个VMA,各字段对应如下模式:{address  (start-end)}    {permissions}    {offset}    {device (major:minor)}    {inode}    {pathname   (image)}:

  • address: 这表示VMA的起始地址和结束地址。
  • permissions: 说明区域的访问权限:r(读)、w(写)、x(执行),包括p(私有映射)、s(共享映射)。
  • Offset: 在文件映射(mmap系统调用)的情况下,它是发生映射的文件中的偏移量。否则是0。
  • major:minor: 在文件映射的情况下,这些表示存储文件的设备的主设备号和次设备号(保存文件的设备)。
  • inode: 在从文件映射的情况下,为映射文件的inode号。
  • pathname: 这是映射文件的名称,否则留空。还有其他的区域名,比如[heap]、[stack] 或 [vdso],它们代表虚拟动态共享对象(virtual dynamic shared object),它是一个由内核映射到每个进程地址空间的共享库,以便在系统调用切换到内核模式时减少性能损失。

分配给进程的每个页面都属于一个区域;因此,不存在于VMA中的任何页面都不存在,也不能被进程引用。

高端内存非常适合用户空间,因为用户空间的虚拟地址必须显式映射。因此,大多数高端内存被用户应用程序占用。 __GFP_HIGHMEM 和 GFP_HIGHUSER 是请求分配(潜在的)高端内存的标志。如果没有这些标志,所有内核分配都只返回低端内存。在Linux中,无法从用户空间中分配连续的物理内存。
您可以使用 find_vma 函数来查找与给定虚拟地址相对应的VMA。在linux/mm.h中声明 find_vma :
* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned
long addr);

例子如下:

struct vm_area_struct *vma = find_vma(task->mm, 0x13000);
if (vma == NULL) /* Not found ? */
   return -EFAULT;
if (0x13000 >= vma->vm_end) /* Beyond the end of returned VMA ? */
   return -EFAULT;

内存映射的整个过程可以通过读取这些文件来获得:

/proc/<PID>/maps, /proc/<PID>/smap, 和 /proc/<PID>/pagemap.
posted @ 2021-02-01 18:45  闹闹爸爸  阅读(1455)  评论(0编辑  收藏  举报