标签:def 基于 寄存器 space allocator specific define -- png
在Linux系统中,每个内存地址都是虚拟的。它们不直接指向RAM中的任何地址。每当您访问一个内存位置时,都会执行一种转换机制来匹配相应的物理内存。
让我们从一个介绍虚拟内存概念的小故事开始。给定一个旅馆,每个房间都可以有一个电话,每个电话都有一个私人号码。当然,所有安装的电话都是酒店的。他们都不能从酒店外面直接联系上。
如果你需要联系一个房间的住户,比如说你的朋友,他必须给你酒店的总机号码和他所住的房间号码。一旦你给总机打电话并告诉你需要通话的住户的房间号码,接待员就会把你的电话转接到房间里的私人电话上。只有接待员和房间居住者知道私人号码映射:
(switchboard number + room number) <=> private (real) phone number
每当这座城市(或世界上任何地方)的某个人想要联系住在房间里的人,他都必须通过热线。他需要知道正确的酒店热线号码和房间号码。这样,“总机号码”和“房间号码”就是虚拟地址,“私人电话号码”对应的是物理地址。
有一些与酒店相关的规则也适用于Linux:
Hotel
|
Linux
|
您不能联系房间内没有私人电话的住户。甚至没有办法尝试这样做。您的电话将会突然结束 | 您不能访问地址空间中不存在的内存。这将导致段错误 |
您无法联系不存在的住客,或酒店不知道其入住,或总机找不到其信息的住客 | 如果您访问未映射的内存,CPU会抛出一个页面错误,OS会处理它 |
你不能联系已经离开的住客 | 您不能访问已释放的内存。也许它已经被分配给了另一个进程 |
许多酒店可能拥有相同的品牌,但位于不同的地点,每个酒店都有不同的热线电话 | 不同的进程可能有相同的虚拟地址映射到它们的地址空间中,但是指向不同的物理地址 |
有一本书(或带有数据库的软件)保存着房间号码和私人电话号码之间的映射关系,接待员可以根据需要进行咨询 | 虚拟地址通过页表映射到物理内存,页表由操作系统内核维护,并由处理器查询 |
这就是如何想象虚拟地址在Linux系统中工作。
在这一章中,我们将讨论整个Linux内存管理系统,包括以下主题:
在本章中,像内核空间和用户空间这样的术语都是指它们的虚拟地址空间。在Linux系统中,每个进程都拥有一个虚拟地址空间。它是一种memory sandbox 在进程的生命周期内。这个地址空间在32位系统上是4gb(即使在物理内存小于4gb的系统上)。对于每个进程,4gb地址空间被分成两个部分:
拆分的方式取决于一个特殊的内核配置选项, CONFIG_PAGE_OFFSET ,它定义了内核地址段在进程地址空间中的起始位置。默认情况下,32位系统上的通用值是0xC0000000,但这可能会改变,就像NXP的i.MX6系列处理器一样,它使用0x80000000。在整个章节中,我们将默认考虑0xC0000000。这称为3G/1G分割,其中用户空间使用较低的3gb虚拟地址空间,内核使用剩余的1gb。一个典型进程的虚拟地址空间布局如下所示:
由于内存页映射到页帧,所以页和页帧的大小是相同的,在我们的例子中是4 K。页面的大小是通过 PAGE_SIZE 宏在内核中定义的。
在某些情况下,您需要内存来实现页面对齐。如果一个内存的地址恰好从一个页面的开头开始,那么这个内存就是页面对齐的。例如,在一个4 K页面大小的系统上,4,096、20,480和409,600是页面对齐内存地址的实例。换句话说,任何地址是系统页面大小的倍数的内存都称为页面对齐的。
Linux内核有自己的虚拟地址空间,就像每个用户模式进程一样。内核的虚拟地址空间(以3G/1G分割时的大小为1gb)分为两个部分:
前896 MB的内核地址空间构成了低端内存区域。在引导早期,内核会永久地映射这896 MB。从该映射产生的地址称为logical addresses(逻辑地址)。这些是虚拟地址,但可以通过减去一个固定偏移量来转换为物理地址,因为映射是永久性的,并且是预先知道的。Low memory与物理地址的下界匹配。您可以将低端内存定义为在内核空间中存在逻辑地址的内存。大多数内核内存函数返回低端内存。事实上,为了满足不同的目的,内核内存被划分为一个区域。实际上,LOWMEM的前16 MB是预留给DMA使用的。由于硬件的限制,内核不能将所有页面视为相同的。然后,我们可以在内核空间中确定三个不同的内存区域:
这意味着在一个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的转换表基址中。无论如何,在继续之前,让我们看看进程地址空间的表示:
进程的内存布局
# 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)}:
分配给进程的每个页面都属于一个区域;因此,不存在于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.
虚拟内存是一个概念,是给进程的一种错觉,因此它认为自己拥有巨大的、几乎无限的内存,有时甚至比系统实际拥有的内存还要多。每次访问内存位置时,由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使用一个四级分页模型:
并不是所有的级别都被使用。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。
在MMU继续处理转换之前,还涉及到另一个步骤。正如有一个缓存用于最近访问的数据,也有一个缓存用于最近翻译的地址。由于数据缓存可以加快数据访问过程,TLB可以加快虚拟地址转换的速度。是的,地址转换是一项棘手的任务。它是内容寻址内存(CAM),其中键是虚拟地址,值是物理地址。换句话说,TLB是MMU的缓存。在每次内存访问时,MMU首先检查TLB中最近使用的页面,TLB包含一些当前分配给物理页面的虚拟地址范围。
在虚拟内存访问中,CPU遍历TLB,试图找到正在被访问的页面的虚拟页号。这个步骤称为TLB查找。当找到一个TLB表项时(匹配发生),就说有一个TLB命中,CPU继续运行,并使用在TLB表项中找到的PFN来计算目标物理地址。TLB命中时不会出现页面错误。正如您所看到的,只要在TLB中可以找到一个转换,虚拟内存访问将和物理访问一样快。如果没有找到TLB表项(没有匹配),你说有一个TLB缺失。
在TLB miss事件中,有两种可能,这取决于处理器类型;TLB miss事件可以由软件来处理,也可以由硬件通过MMU来处理:
在这两种情况下,页面错误处理程序是相同的:执行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系统中最低级的分配器,其他分配器都依赖它。系统的物理内存由固定大小的块(称为页帧)组成。页面框架在内核中表示为页面结构结构的实例。页面是操作系统在低级别上给予任何内存请求的最小内存单位。
您将理解内核页面分配器使用buddy算法分配和释放页面块。页面以大小为2的幂的块进行分配(为了从buddy算法中得到最好的结果)。这意味着它可以分配块1页,2页,4页,8,16,等等:
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_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分配器是kmalloc()所依赖的分配器。它的主要目的是消除内存(de)allocation导致的碎片,在内存分配很小的情况下,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
标签:def 基于 寄存器 space allocator specific define -- png
原文地址:https://www.cnblogs.com/wanglouxiaozi/p/14329277.html