物理内存页框的管理
对于内存管理来讲,包括两个方面,一个是物理内存的管理,另一个是线性地址空间的管理。物理内存的管理是从“供应”的角度来看,我现在仓库里面有多少资源可以供分配,而线性地址空间的管理,则是从“需求”的角度去考虑,我需要多少的线性地址空间来满足进程的运行。把供应和需求连接起来的纽带就是缺页调用,以及页面的分配了。
下面从“供应”的角度谈谈,Linux系统如何来管理有限的物理内存资源的。
以I386为例,我们都知道,系统启动的时候,Bios检索内存,检查一下本机的物理内存有多大MB,对于CPU里面的MMU来讲,在寻址的时候,以4KB的页面来进行页面寻址的,既是每一个页面的大小为4KB,当然,如果是2MB的话,每一个页面大小就是2MB了,同样道理,某些I386CPU也支持4MB和1GB大小的页面,我们现在假定以4KB为例。以4KB为例,操作系统把全部的物理内存以4KB来分割,进行管理,在给进程分配的时候,最小的单位是1页,而进行SWAP和回收的时候 ,也是以最小的页为单位来进行处理的。因此,就需要有一个数据结构来描述一个物理页面的状态了:
1、 这个页面分配了么?也就是这个页面在使用中么?如果在使用,那么被几个进程使用了。
2、 这个页面被几个页表项映射了?
这个也挺重要,因为,如果,这个页要是换出的话,得告诉自己的“顶头上司”映射自己的页表项。让他们有所改变才行。
3、 另外,如果页面被使用了,那么是存放的某些文件的内容,还是用于进程自己使用的?
如果是某些文件的内容,那么标识一下,以后的其他进程使用的话,就直接在内存中读,而不需要舍近求远了。----这就是传说中的高速缓冲区
4、 这个页面内容如果存放的是某个文件的内容,那么页面存放的数据相对于文件头来讲,具体是那一部分的数据?
这就是说,如果我要写回,我写到文件的什么位置捏?!
5、 这个页面现在是被使用的么,还是暂时没有被使用?
如果没有被使用,就可以分配给其他进程了,如果被使用了,那么就不能给别的进程映射了。
由了这里的分析,那么我们就看一下Linux系统是怎样来描述一个物理页的状态的吧:
先看第一个字段:flags
这个字段就是说明了page对应页的属性:
内容是内核代码段么?
还是被保留的不能换出的,还是被锁定,不能被换出;还是刚刚被访问过的,如果刚刚被访问过那就是比较年轻,距离换出还有相当长的时间;还是将要被回收的;该页的内容是不是”脏”的。那么这些选项都对应一个标志位,一共有20个标志位。在include/linux/page-flags.h里面有定义。
第二个字段:_count.
这个字段,说明对应页被用到的次数。
第三个字段:map_count
这个字段是,该页在几个页表项中存在,这个字段和第二个字段_count很相似,但一般情况下,对页进行操作的时候,先把该页描述符的_count+1,然后操作完后 ,再进行-1。而map_count是说明这个页在页表项中映射的次数。有可能的情况是映射了到了好几个页表项,但_count的值为1。这个map_count字段最大的作用就是在页面被换出的时候,我要查找一下,有几个页表项映射的是这个页,然后通过反向查找,把所有对应的页表项的值该为swap里面的索引。
第四个字段是一个union结构体。mapping指向的是映射文件的inode的address_spaces字段。可以根据它和page里面的index字段,去查找是否在页高速缓存中,如果在也页高速缓存中没有找到 ,那么,就看一下是否被交换到了swap去里面,利用private里面的值去找到swap里面的数据内容,然后交换到页高速缓冲区中。
第五个字段,就是index。它的值代表了,页框的内容如果是映射文件的话,相对于文件开始的地方偏移量。
第六个字段,是lru。这是链接的活动、非活动链表。根据第一个字段flag属性,确定链接的是活动链表,还是非活动链表。
第七个字段,还不清楚,但根据注释,指向的是该页描述符对应页框的线性地址。我想,这个线性地址在对应内核线性地址空间的永久影射区的空间地址吧。
好了,以上就是对于页框的管理。似乎到这里就可以管理内存的每一个页框了。
我们现在假设一种情况,内核在给进程分配页框的时候,随机分配,假如把低16MB的地址,除去BIOS等占用的保留页框之外,都已经分配结束了。而在高于16MB的地方大部分还没有分配。这个时候,有一个用户进程请求了一个DMA传输,而这个DMA控制器是基于ISA总线的,我们清楚,对于ISA总线上的DMA控制器,是不经过MMU的,因此,它只认识总线地址而且是低于16M(0x 10,00000)的线性地址才认识。但这个时候,0~16MB的物理地址区间内的页框都已经被用完了,而高于16MB的地方,还有很多内存可用,但是这个时候,是用不了的。所以,我们在进行页框管理的时候 ,还要在页框之上再加一个管理层,就是“区”的概念了。
一般情况下,内存分为3个区:
ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。
除了ZONE_DMA必须之外,为什么还要再分一个ZONE_HIGHMEM区呢?因为,对于正常的区,内核区间的线性地址进行映射的时候是物理地址加上一个3G偏移量,就得到线性地址了,我们知道,内核直接映射的区域是0~896MB,对于大于1G的内存,内核是不能通过物理地址加上3G的偏移量,直接映射的,因此在高于896MB的物理内存在被映射的时候,是通过其他方法来映射的,因此要单独拿出来作为一个分区。
好了,现在两个特殊的分区,都划出来了,中间剩下的就作为一个区了—Normal区。
我们来看一下ZONE区的结构:
区作为物理页框的上级管理者,它首先需要知道,我所管辖的区里面所管辖的页框在物理内存的那个范围之内,总共有多少个页框,有多少空闲的内存可以分配,有哪些页框是永远也不能分配的。
另外,作为管理者,我有必要在空闲内存少的时候,启动回收方案,将暂时不用的页进行回收,或者交换,以腾出页面供将要进行的进程进行使用。那么为了进行有效的回收,我就必须知道什么时候来进行回收,哪些页暂时没有用,哪些页正在使用,因此就需要用两个对应的链表,把对应的页给穿起来(穿起来要依靠page结构里面的lru—有一个前指针,有一个后指针)。另外,为了管理的方便,需要知道有多少非活动页,有多少活动页。另外为了提高性能,Linux系统为管理区中单独分配一个页面的情况作了特殊处理。如果分配一个页面的时候,将用到的数据与前面的数据是相关的,可能已经在高速缓存中存在了,那么,就给它分配一个特殊的页面,也就是“热”页,操作系统为每一个CPU都维护了一个热页面的集合,通过链表,将热页面给管理起来,相对,如果我用到的数据,和之前的数据是无关的,那么我就分配一个一般的一面,相对与“热”页面,就是冷页面了。
根据上面的分析,基本上Linux都安排了相应的字段。
随着体系结构的发展,出现了NUMA的体系结构,在NUMA的体系结构中,存储器是分布式的。每一个节点都有一个本地的存储器,既可以访问本地的存储器,又可以访问系统其他节点的存储器,但访问其他节点和本地节点的时间是不一样的。在NUMA体系结构中,我们再假象一个场景,在一个节点中,我的物理内存用的差不多了,但其他节点上的存储器还有很多闲置的内存存在,因此,我就想从其他节点上分一些页框归我所用。那么我怎么定位其他节点上的页框地址呢?当然具体一点是先定位到节点,然后再定义到节点下面的存储区,然后再定义到区里面的页框。
很显然,在NUMA的体系结构中,在管理物理内存的时候,除了页描述符和管理区之外,还需要有一个新的管理机构就是节点。
在Linux中,节点的数据结构是pglist_data。
作为物理内存最高的管理机构,Pglist_data所做的事情主要是大局的。比如,我下面有几个管理区,如果给进程分配内存的时候,我没有那么多的内存,我首先该向哪一个管理区去借。另外,对于我本身的存储器而言,页描述符在物理内存的什么位置放着,第一个页框的序号是什么(一般都是0),总共有多少个页面,除去不能分配的,还有多少的页面。在进行页框分配的时候,我允许一次最多能分配多少的连续页框。另外,如果剩余的内存比较少,我应该调用哪一个进程去回收页框,回收页框完毕后,需要唤醒当初被阻塞的进程。
上面的分析,基本上也就是pglist_data结构下面的字段了。
下面从“供应”的角度谈谈,Linux系统如何来管理有限的物理内存资源的。
以I386为例,我们都知道,系统启动的时候,Bios检索内存,检查一下本机的物理内存有多大MB,对于CPU里面的MMU来讲,在寻址的时候,以4KB的页面来进行页面寻址的,既是每一个页面的大小为4KB,当然,如果是2MB的话,每一个页面大小就是2MB了,同样道理,某些I386CPU也支持4MB和1GB大小的页面,我们现在假定以4KB为例。以4KB为例,操作系统把全部的物理内存以4KB来分割,进行管理,在给进程分配的时候,最小的单位是1页,而进行SWAP和回收的时候 ,也是以最小的页为单位来进行处理的。因此,就需要有一个数据结构来描述一个物理页面的状态了:
1、 这个页面分配了么?也就是这个页面在使用中么?如果在使用,那么被几个进程使用了。
2、 这个页面被几个页表项映射了?
这个也挺重要,因为,如果,这个页要是换出的话,得告诉自己的“顶头上司”映射自己的页表项。让他们有所改变才行。
3、 另外,如果页面被使用了,那么是存放的某些文件的内容,还是用于进程自己使用的?
如果是某些文件的内容,那么标识一下,以后的其他进程使用的话,就直接在内存中读,而不需要舍近求远了。----这就是传说中的高速缓冲区
4、 这个页面内容如果存放的是某个文件的内容,那么页面存放的数据相对于文件头来讲,具体是那一部分的数据?
这就是说,如果我要写回,我写到文件的什么位置捏?!
5、 这个页面现在是被使用的么,还是暂时没有被使用?
如果没有被使用,就可以分配给其他进程了,如果被使用了,那么就不能给别的进程映射了。
由了这里的分析,那么我们就看一下Linux系统是怎样来描述一个物理页的状态的吧:
223 struct page {
224 unsigned long flags; /* Atomic flags, some possibly
225 * updated asynchronously */
226 atomic_t _count; /* Usage count, see below. */
227 atomic_t _mapcount; /* Count of ptes mapped in mms,有几个pet映射此页面
228 * to show when page is mapped
229 * & limit reverse map searches.
230 */
231 union {
232 struct {
233 unsigned long private; /* Mapping-private opaque data:
234 * usually used for buffer_heads
235 * if PagePrivate set; used for
236 * swp_entry_t if PageSwapCache;
237 * indicates order in the buddy
238 * system if PG_buddy is set.
239 */
240 struct address_space *mapping; /* If low bit clear, points to
241 * inode address_space, or NULL.
242 * If page mapped as anonymous
243 * memory, low bit is set, and
244 * it points to anon_vma object:
245 * see PAGE_MAPPING_ANON below.
246 */
247 };
248 #if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS
249 spinlock_t ptl;
250 #endif
251 };
252 pgoff_t index; /* Our offset within mapping. */
253 struct list_head lru; /* Pageout list, eg. active_list
254 * protected by zone->lru_lock !
255 */
256 /*
257 * On machines where all RAM is mapped into kernel address space,
258 * we can simply calculate the virtual address. On machines with
259 * highmem some memory is mapped into kernel virtual memory
260 * dynamically, so we need a place to store that address.
261 * Note that this field could be 16 bits on x86 ... ;)
262 *
263 * Architectures with slow multiplication can define
264 * WANT_PAGE_VIRTUAL in asm/page.h
265 */
266 #if defined(WANT_PAGE_VIRTUAL)
267 void *virtual; /* Kernel virtual address (NULL if
268 not kmapped, ie. highmem) */
269 #endif /* WANT_PAGE_VIRTUAL */
270 };
先看第一个字段:flags
这个字段就是说明了page对应页的属性:
内容是内核代码段么?
还是被保留的不能换出的,还是被锁定,不能被换出;还是刚刚被访问过的,如果刚刚被访问过那就是比较年轻,距离换出还有相当长的时间;还是将要被回收的;该页的内容是不是”脏”的。那么这些选项都对应一个标志位,一共有20个标志位。在include/linux/page-flags.h里面有定义。
第二个字段:_count.
这个字段,说明对应页被用到的次数。
第三个字段:map_count
这个字段是,该页在几个页表项中存在,这个字段和第二个字段_count很相似,但一般情况下,对页进行操作的时候,先把该页描述符的_count+1,然后操作完后 ,再进行-1。而map_count是说明这个页在页表项中映射的次数。有可能的情况是映射了到了好几个页表项,但_count的值为1。这个map_count字段最大的作用就是在页面被换出的时候,我要查找一下,有几个页表项映射的是这个页,然后通过反向查找,把所有对应的页表项的值该为swap里面的索引。
第四个字段是一个union结构体。mapping指向的是映射文件的inode的address_spaces字段。可以根据它和page里面的index字段,去查找是否在页高速缓存中,如果在也页高速缓存中没有找到 ,那么,就看一下是否被交换到了swap去里面,利用private里面的值去找到swap里面的数据内容,然后交换到页高速缓冲区中。
第五个字段,就是index。它的值代表了,页框的内容如果是映射文件的话,相对于文件开始的地方偏移量。
第六个字段,是lru。这是链接的活动、非活动链表。根据第一个字段flag属性,确定链接的是活动链表,还是非活动链表。
第七个字段,还不清楚,但根据注释,指向的是该页描述符对应页框的线性地址。我想,这个线性地址在对应内核线性地址空间的永久影射区的空间地址吧。
好了,以上就是对于页框的管理。似乎到这里就可以管理内存的每一个页框了。
我们现在假设一种情况,内核在给进程分配页框的时候,随机分配,假如把低16MB的地址,除去BIOS等占用的保留页框之外,都已经分配结束了。而在高于16MB的地方大部分还没有分配。这个时候,有一个用户进程请求了一个DMA传输,而这个DMA控制器是基于ISA总线的,我们清楚,对于ISA总线上的DMA控制器,是不经过MMU的,因此,它只认识总线地址而且是低于16M(0x 10,00000)的线性地址才认识。但这个时候,0~16MB的物理地址区间内的页框都已经被用完了,而高于16MB的地方,还有很多内存可用,但是这个时候,是用不了的。所以,我们在进行页框管理的时候 ,还要在页框之上再加一个管理层,就是“区”的概念了。
一般情况下,内存分为3个区:
ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。
除了ZONE_DMA必须之外,为什么还要再分一个ZONE_HIGHMEM区呢?因为,对于正常的区,内核区间的线性地址进行映射的时候是物理地址加上一个3G偏移量,就得到线性地址了,我们知道,内核直接映射的区域是0~896MB,对于大于1G的内存,内核是不能通过物理地址加上3G的偏移量,直接映射的,因此在高于896MB的物理内存在被映射的时候,是通过其他方法来映射的,因此要单独拿出来作为一个分区。
好了,现在两个特殊的分区,都划出来了,中间剩下的就作为一个区了—Normal区。
我们来看一下ZONE区的结构:
139 struct zone {
140 /* Fields commonly accessed by the page allocator */
141 unsigned long free_pages;//有多少空闲页
142 unsigned long pages_min, pages_low, pages_high;//阈值,在分配物理页的时候对不同的阈值,正常,或者减慢页的分配速度
152 unsigned long lowmem_reserve[MAX_NR_ZONES];//每一个区需要保留的内存,应急用
154 #ifdef CONFIG_NUMA
155 /*
156 * zone reclaim becomes active if more unmapped pages exist.
157 */
158 unsigned long min_unmapped_ratio;//换出的页太多了,就要启动reclaim
159 unsigned long min_slab_pages;//用于slab页太少了,因此也要reclaim
160 struct per_cpu_pageset *pageset[NR_CPUS];//每个CPU需要维护的冷热页表,在高速缓存中的页为热页
161 #else
162 struct per_cpu_pageset pageset[NR_CPUS];
163 #endif
164 /*
165 * free areas of different sizes
166 */
167 spinlock_t lock;
168 #ifdef CONFIG_MEMORY_HOTPLUG
169 /* see spanned/present_pages for more description */
170 seqlock_t span_seqlock;
171 #endif
172 struct free_area free_area[MAX_ORDER];
177 /* Fields commonly accessed by the page reclaim scanner */
178 spinlock_t lru_lock;
179 struct list_head active_list;
180 struct list_head inactive_list;
181 unsigned long nr_scan_active;//回收内存时需要扫描的活动页数
182 unsigned long nr_scan_inactive;//回收内存时需要扫描的非活动页数
183 unsigned long nr_active;//活动链表上的页数
184 unsigned long nr_inactive;//非活动链表上的页数
185 unsigned long pages_scanned; /* since last reclaim */
186 int all_unreclaimable; /* All pages pinned *///当填满不可回收页时置位
187
188 /* A count of how many reclaimers are scanning this zone */
189 atomic_t reclaim_in_progress;//有几个回收进程在扫描该区
190
191 /* Zone statistics */
192 atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];//存放Zone属性的统计信息
207 int prev_priority;//回收内存时,先处理的优先级范围0~12
244 struct pglist_data *zone_pgdat;//隶属于哪个节点
245 /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
246 unsigned long zone_start_pfn;//zone区从哪一个页框号开始
258 unsigned long spanned_pages; /* total size, including holes */
259 unsigned long present_pages; /* amount of memory (excluding holes) */
264 char *name;
265 } ____cacheline_internodealigned_in_smp;
区作为物理页框的上级管理者,它首先需要知道,我所管辖的区里面所管辖的页框在物理内存的那个范围之内,总共有多少个页框,有多少空闲的内存可以分配,有哪些页框是永远也不能分配的。
另外,作为管理者,我有必要在空闲内存少的时候,启动回收方案,将暂时不用的页进行回收,或者交换,以腾出页面供将要进行的进程进行使用。那么为了进行有效的回收,我就必须知道什么时候来进行回收,哪些页暂时没有用,哪些页正在使用,因此就需要用两个对应的链表,把对应的页给穿起来(穿起来要依靠page结构里面的lru—有一个前指针,有一个后指针)。另外,为了管理的方便,需要知道有多少非活动页,有多少活动页。另外为了提高性能,Linux系统为管理区中单独分配一个页面的情况作了特殊处理。如果分配一个页面的时候,将用到的数据与前面的数据是相关的,可能已经在高速缓存中存在了,那么,就给它分配一个特殊的页面,也就是“热”页,操作系统为每一个CPU都维护了一个热页面的集合,通过链表,将热页面给管理起来,相对,如果我用到的数据,和之前的数据是无关的,那么我就分配一个一般的一面,相对与“热”页面,就是冷页面了。
根据上面的分析,基本上Linux都安排了相应的字段。
随着体系结构的发展,出现了NUMA的体系结构,在NUMA的体系结构中,存储器是分布式的。每一个节点都有一个本地的存储器,既可以访问本地的存储器,又可以访问系统其他节点的存储器,但访问其他节点和本地节点的时间是不一样的。在NUMA体系结构中,我们再假象一个场景,在一个节点中,我的物理内存用的差不多了,但其他节点上的存储器还有很多闲置的内存存在,因此,我就想从其他节点上分一些页框归我所用。那么我怎么定位其他节点上的页框地址呢?当然具体一点是先定位到节点,然后再定义到节点下面的存储区,然后再定义到区里面的页框。
很显然,在NUMA的体系结构中,在管理物理内存的时候,除了页描述符和管理区之外,还需要有一个新的管理机构就是节点。
在Linux中,节点的数据结构是pglist_data。
303 typedef struct pglist_data {
304 struct zone node_zones[MAX_NR_ZONES];//节点中管理区描述符的数组
305 struct zonelist node_zonelists[GFP_ZONETYPES];//页分配器使用的管理区数组,代表不同的分配策略
306 int nr_zones;//
307 #ifdef CONFIG_FLAT_NODE_MEM_MAP
308 struct page *node_mem_map;//page链表
309 #endif
310 struct bootmem_data *bdata;
311 #ifdef CONFIG_MEMORY_HOTPLUG
319 spinlock_t node_size_lock;
320 #endif
321 unsigned long node_start_pfn;//节点中第一个页框的下标
322 unsigned long node_present_pages; /* total number of physical pages */
323 unsigned long node_spanned_pages; /* total size of physical page
324 range, including holes */
325 int node_id;//node编号
326 wait_queue_head_t kswapd_wait;//在运行进程时,kswapd在运行,那么将进程阻塞,放入等待队列中
327 struct task_struct *kswapd;//指向kswapd内核线程
328 int kswapd_max_order;//允许创建空闲块的最大值,取对数
329 } pg_data_t;
作为物理内存最高的管理机构,Pglist_data所做的事情主要是大局的。比如,我下面有几个管理区,如果给进程分配内存的时候,我没有那么多的内存,我首先该向哪一个管理区去借。另外,对于我本身的存储器而言,页描述符在物理内存的什么位置放着,第一个页框的序号是什么(一般都是0),总共有多少个页面,除去不能分配的,还有多少的页面。在进行页框分配的时候,我允许一次最多能分配多少的连续页框。另外,如果剩余的内存比较少,我应该调用哪一个进程去回收页框,回收页框完毕后,需要唤醒当初被阻塞的进程。
上面的分析,基本上也就是pglist_data结构下面的字段了。
评论
发表评论