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。一个典型进程的虚拟地址空间布局如下所示:
- 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表示
低端内存
前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 { [...] 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的转换表基址中。无论如何,在继续之前,让我们看看进程地址空间的表示:
进程的内存布局
- 在红黑树中,其根元素由 mm_struct->mm_rb 字段指向
- 在一个链表中,第一个元素由 mm_struct->mmap 字段指向
虚拟内存区域(VMA)
# 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中的任何页面都不存在,也不能被进程引用。
* 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.
本文来自博客园,作者:闹闹爸爸,转载请注明原文链接:https://www.cnblogs.com/wanglouxiaozi/p/14329277.html