码迷,mamicode.com
首页 > 其他好文 > 详细

Kernel Memory Management

时间:2021-02-02 11:12:12      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:def   基于   寄存器   space   allocator   specific   define   --   png   

在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>/map, /proc/<PID>/smap, 和 /proc/<PID>/pagemap.

地址转换和MMU

虚拟内存是一个概念,是给进程的一种错觉,因此它认为自己拥有巨大的、几乎无限的内存,有时甚至比系统实际拥有的内存还要多。每次访问内存位置时,由CPU将虚拟地址转换为物理地址。这种机制称为地址转换,由
内存管理单元(MMU)完成,是CPU的一部分。

MMU保护内存免受未经授权的访问。给定一个进程,需要访问的任何页面必须存在于进程VMAs中,因此必须存在于进程页表中(每个进程都有自己的页表)。

内存由固定大小的命名页(用于虚拟内存)和帧(用于物理内存)组织,在我们的示例中大小为4 KB。无论如何,您不需要猜测您为之编写驱动程序的系统的页面大小。它是通过内核中的PAGE_SIZE宏定义和访问的。因此,请记住,页面大小是由硬件(CPU)决定的。
考虑到一个4 KB的页面大小的系统,0到4095字节属于第0页,4096-8191字节属于第1页,以此类推。

引入页表的概念来管理页和框架之间的映射。页面分布在各个表上,这样每个PTE都对应于页面和框架之间的映射。然后给每个进程一组页表来描述它的整个内存空间。

为了遍历页面,每个页面都分配了一个索引(类似数组),称为页号。当谈到一个框架,它是PFN。这样,虚拟内存地址由两部分组成:页号和偏移量。偏移量表示地址的低12位有效位,而在8kb页面大小的系统中,低13位有效位表示地址:

技术图片

操作系统或CPU如何知道哪个物理地址对应一个给定的虚拟地址?他们使用页表作为转换表,并且知道每个条目的索引是一个虚拟页码,值是PFN。要访问给定虚拟内存的物理内存,操作系统首先提取偏移量、虚拟页号,然后遍历进程的页表,以便匹配虚拟页号和物理页。
一旦匹配发生,就可以访问该页面帧中的数据:

技术图片

 

偏移量用来指向帧中的正确位置。页表不仅包含物理页号和虚拟页号之间的映射,还包含访问控制信息(读写访问、特权等):

技术图片

                  Virtual to physical address translation

 

用来表示偏移量的位数由内核宏PAGE_SHIFT定义。PAGE_SHIFT是左移一位以获得PAGE_SIZE值的位数。它也是右移将虚拟地址转换为页码和物理地址转换为PFN的位数。下面是这些宏的定义/include/asm-generic/page.h:

#define PAGE_SHIFT 12
#ifdef __ASSEMBLY__
#define PAGE_SIZE (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#endif

页表是部分解决方案。让我们看看这是为什么。大多数架构需要32位(4字节)来表示一个PTE,每个进程都有其私有的3gb用户空间地址,所以我们需要786432个条目来描述和覆盖一个进程地址空间。它表示每个进程花费了太多的物理内存,只是为了描述内存映射。事实上,一个进程通常使用它的虚拟地址空间的一小部分但分散的部分。为了解决这个问题,我们引入了关卡的概念。页表按级别(页级)分层。存储多级存储器所必需的空间。

为了解决这个问题,我们引入了分级的概念。页表按级别(页级)分层。存储多级页表所需的空间只取决于实际使用的虚拟地址空间,而不是与虚拟地址空间的最大大小成比例。这样,就不再表示未使用的内存,并且页表遍历时间也减少了。这样,第N层的每个表项都指向第N+1层的表项。第1级是较高的级别。

Linux使用一个四级分页模型:

  • Page Global Directory (PGD):它是第一级(第一级)页表。在内核中,每个条目的类型都是pgd_t(通常是unsigned long),并指向表中的第二级条目。在内核中,tastk_struct结构表示一个进程的描述,该描述又有一个类型为mm_struct的成员(mm),它描述并表示进程的内存空间。在mm_struct中,有一个特定于处理器的字段pgd,它是一个指针,指向进程的level-1 (pgd)页表的第一个条目(条目0)。每个进程有且只有一个PGD,它可能包含多达1024个条目。
  • Page Upper Directory (PUD):这只存在于使用四级表的体系结构上。它代表了间接的第二层。
  • 页面中间目录(PMD):这是第三个间接级别,仅存在于使用四级表的体系结构上。
  • 页表(PTE):树的叶子。它是一个pte_t数组,其中每个入口都指向物理页面。

并不是所有的级别都被使用。i.MX6的MMU只支持两层页表(PGD和PTE),这是几乎所有32位的情况。在这种情况下,PUD和PMD被简单地忽略。

技术图片

                    Two-level tables overview

您可能会问MMU是如何知道进程页表的。它很简单,MMU不存储任何地址。相反,在CPU中有一个特殊的寄存器,称为页表基址寄存器(PTBR)或转换表基址寄存器0 (TTBR0),它指向进程的level-1(顶级)页表(PGD)的条目0。它正是mm_struct的pdg字段指向:current->mm.pgd = = TTBR0。

在上下文切换(当一个新的进程被调度并且给定了CPU)时,内核立即配置MMU并使用新进程的pgd更新PTBR。现在,当一个虚拟地址给MMU,它使用PTBR的内容来定位进程的第1级页表(PGD),然后它使用第1级索引,从虚拟地址的最有效位(MSBs)中提取,查找适当的表项,该表项包含指向适当的第2级页表基址的指针。然后,从该基地址开始,它使用level-2索引查找适当的条目,以此类推,直到它到达PTE。ARM架构(在我们的例子中是i.MX6)有一个两层的页表。在本例中,第2级条目是PTE,并指向物理页面(PFN)。这一步只找到物理页面。为了访问页面中确切的内存位置,MMU提取内存偏移量,也就是虚拟地址的一部分,并指向物理页面中相同的偏移量。

当一个进程需要读取或写入内存位置(当然,我们讨论的是虚拟内存)时,MMU将执行转换到该进程的页表中,以找到正确的条目(PTE)。虚拟页码是从虚拟地址中提取出来的,处理器将其用作进程页表的索引,以检索其页表条目。如果在该偏移量处有一个有效的页表条目,处理器将获取PFN从这个条目。如果没有,则意味着进程访问了其虚拟内存的未映射区域。然后引发一个页面错误,操作系统应该处理它。

在现实世界中,地址转换需要页表遍历,而且并不总是一次性操作。内存访问实例的数量至少与表级别相同。一个四级页表需要4次内存访问。换句话说,每个虚拟访问实例将导致5次物理内存访问。如果虚拟内存的访问比物理访问慢四倍,那么虚拟内存的概念就毫无用处了。

幸运的是,SoC制造商努力寻找一个聪明的技巧来解决这个性能问题: 现代cpu使用一个称为转义查找缓存(TLB)的小型且非常快的关联内存,来缓存最近访问的虚拟页面的pte。

页面查找和TLB

 在MMU继续处理转换之前,还涉及到另一个步骤。正如有一个缓存用于最近访问的数据,也有一个缓存用于最近翻译的地址。由于数据缓存可以加快数据访问过程,TLB可以加快虚拟地址转换的速度。是的,地址转换是一项棘手的任务。它是内容寻址内存(CAM),其中键是虚拟地址,值是物理地址。换句话说,TLB是MMU的缓存。在每次内存访问时,MMU首先检查TLB中最近使用的页面,TLB包含一些当前分配给物理页面的虚拟地址范围。

 TLB是如何工作的?

在虚拟内存访问中,CPU遍历TLB,试图找到正在被访问的页面的虚拟页号。这个步骤称为TLB查找。当找到一个TLB表项时(匹配发生),就说有一个TLB命中,CPU继续运行,并使用在TLB表项中找到的PFN来计算目标物理地址。TLB命中时不会出现页面错误。正如您所看到的,只要在TLB中可以找到一个转换,虚拟内存访问将和物理访问一样快。如果没有找到TLB表项(没有匹配),你说有一个TLB缺失。

在TLB miss事件中,有两种可能,这取决于处理器类型;TLB miss事件可以由软件来处理,也可以由硬件通过MMU来处理:

  • 软件处理: CPU引起TLB miss中断,被操作系统捕获。然后,操作系统遍历进程的页表,找到正确的PTE,如果有匹配的有效的条目,CPU就会把新的翻译安装到TLB。否则,将执行页面错误处理程序。
  • 硬件处理: 由CPU(实际上是MMU)在硬件中遍历进程的页表。如果有匹配且有效的条目,CPU会在TLB中添加新的翻译。否则,CPU将引发页面错误中断,由操作系统处理。

在这两种情况下,页面错误处理程序是相同的:执行do_page_fault()函数,这是依赖于体系结构的。对于ARM, do_page_fault在arch/arm/mm/fault.c中定义:

技术图片

                MMU and TLB walkthrough process

 页表和页目录条目依赖于体系结构。表的结构是否与MMU识别的结构相对应,由操作系统决定。在ARM处理器上,您必须在CP15 (coprocessor 15)寄存器c2中写入转换表的位置,然后通过写入CP15寄存器c1来启用缓存和MMU。从http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/BABHJIBH.htm 和 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0433c/CIHFDBEJ.html获取更详细的信息。

 内存分配机制

让我们看看下面的图表,向我们展示基于linux的系统上存在的不同内存分配器,并在后面讨论它。(inspired by http://free-electrons.com/doc/training/linux-kernel/linux-kernel-slides.pdf):

技术图片

          Overview of kernel memory allocator

有一种分配机制可以满足任何类型的内存请求。根据你需要内存的用途,你可以选择最接近你目标的内存。主分配器是页分配器,它只与页一起工作(页是它能够交付的最小内存单元)。然后是SLAB分配器,它构建在页面分配器之上,从它获得页面并返回更小的内存实体(通过SLAB和缓存)。这是kmalloc分配器所依赖的分配器。

页面分配器

页面分配器是Linux系统中最低级的分配器,其他分配器都依赖它。系统的物理内存由固定大小的块(称为页帧)组成。页面框架在内核中表示为页面结构结构的实例。页面是操作系统在低级别上给予任何内存请求的最小内存单位。

页面配置API

您将理解内核页面分配器使用buddy算法分配和释放页面块。页面以大小为2的幂的块进行分配(为了从buddy算法中得到最好的结果)。这意味着它可以分配块1页,2页,4页,8,16,等等:

  1. alloc_pages(mask, order)分配2个order页面,并返回一个表示保留块的第一页的struct page实例。若要仅分配一个页面,则顺序应为0。这就是alloc_page(mask)的作用:
struct page *alloc_pages(gfp_t mask, unsigned int order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

__free_pages()用于释放由alloc_pages()函数分配的内存。它以指向已分配页面的指针作为形参,其顺序与分配页面时相同:

void __free_pages(struct page *page, unsigned int order);

  2. 还有其他函数以同样的方式工作,但它们不是struct page的实例,而是返回保留块的地址(当然是虚的)。这是__get_free_pages(掩码,顺序)和__get_free_page(掩码):

unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);

free_pages()用于释放由__get_free_pages()分配的页面。它接受表示已分配页面的起始区域的内核地址,以及顺序,应该与分配时使用的相同:

free_pages(unsigned long addr, unsigned int order);

在这两种情况下,mask都指定了关于请求的详细信息,即内存分区和分配器的行为。选择是:

  • GFP_USER: 用于用户内存分配。
  • GFP_KERNEL: 用于内核分配的常用标志。
  • GFP_HIGHMEM: 从HIGH_MEM区域请求内存。
  • GFP_ATOMIC: 以不能休眠的原子方式分配内存。当需要从中断上下文分配内存时使用。

使用GFP_HIGHMEM有一个警告,它不应该与__get_free_pages()(或__get_free_page())一起使用。因为HIGHMEM内存不能保证是连续的,所以您不能返回从该区域分配的内存的地址。全局来说,在内存相关的函数中只允许GFP_*的子集:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
   struct page *page;
   /*
   * __get_free_pages() returns a 32-bit address, which cannot represent
   * a highmem page
   */
   VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
   page = alloc_pages(gfp_mask, order);
   if (!page)
     return 0;
   return (unsigned long) page_address(page);
}

您可以分配的最大页数是1024。这意味着在一个大小为4 KB的系统上,最多可以分配1024 *4 KB = 4 MB。kmalloc也是如此。

转换函数

page_to_virt()函数用于将结构页面(例如,由alloc_pages()返回)转换为内核地址。virt_to_page()接受内核虚拟地址,并返回与其关联的结构页实例(就像使用alloc_pages()函数分配的一样)。<asm/page.h>中定义了virt_to_page()和page_to_virt():

struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *pg)

宏page_address()可用于返回与一个结构页面实例的起始地址(当然是逻辑地址)相对应的虚拟地址:

void *page_address(const struct page *page)

我们可以看到它是如何在get_zeroed_page()函数中使用的:

unsigned long get_zeroed_page(unsigned int gfp_mask)
{
     struct page * page;
     page = alloc_pages(gfp_mask, 0);
     if (page) {
         void *address = page_address(page);
         clear_page(address);
         return (unsigned long) address;
     }
     return 0;
}       

__free_pages()和free_pages()可以混合使用。它们之间的主要区别是free_page()接受一个虚拟地址作为参数,而__free_page()接受一个struct页面结构。

Slab分配器

slab分配器是kmalloc()所依赖的分配器。它的主要目的是消除内存(de)allocation导致的碎片,在内存分配很小的情况下,buddy系统会导致碎片,并加速常用对象的内存分配。

buddy 算法

要分配内存,将请求的大小四舍五入到2的幂,伙伴分配器搜索适当的列表。如果所请求的列表中不存在条目,则下一个上层列表中的条目(其块的大小是前一个列表的两倍)将被分成两部分(称为buddy)。分配器使用前一半,而另一半则添加到下面的下一个列表中。这是一种递归方法,当伙伴分配器成功找到可以分割的块,或者达到块的最大大小且没有可用的空闲块时,该方法停止。

下面的案例研究很大程度上受到了http://dysphoria.net/OperatingSystems1/4_allocation_buddy_system.html的启发。作为一个例子,如果最小分配大小是1 KB,内存大小是1 MB, buddy分配器将创建一个空列表1 KB的洞,空列表2 KB的洞,一个4 KB的洞,8 KB、16 KB, 32 KB、64 KB、128 KB、256 KB、512 KB、和一个列表1 MB的洞。它们最初都是空的,除了1mb的列表,它只有一个洞。

现在,让我们设想一个场景,我们想分配一个70K的区块。buddy分配器将它四舍五入到128K,最后将1mb分成两个512K块,然后是256K块,最后是128K块,然后它将把其中一个128K块分配给用户。以下是对该场景的总结方案:

技术图片

 

 

    Allocation using buddy algorithm

 
重新分配和分配一样快。下面的图总结了回收算法:
技术图片
          Deallocation using buddy algorithm

 

 

Kernel Memory Management

标签:def   基于   寄存器   space   allocator   specific   define   --   png   

原文地址:https://www.cnblogs.com/wanglouxiaozi/p/14329277.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!