kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

Linux中Buffer cache

http://www.linuxidc.com/Linux/2013-01/78140.htm

buffer cache和page cache的区别

Page cache和buffer cache到底有什么区别呢?很多时候我们不知道系统在做IO操作的时候到底是走了page cache还是buffer cache?其实,buffer cache和page cache是Linux中两个比较简单的概念,在此对其总结说明。

Page cache是vfs文件系统层的cache,例如 对于一个ext3文件系统而言,每个文件都会有一棵radix树管理文件的缓存页,这些被管理的缓存页被称之为page cache。所以,page cache是针对文件系统而言的。例如,ext3文件系统的页缓存就是page cache。Buffer cache是针对设备的,每个设备都会有一棵radix树管理数据缓存块,这些缓存块被称之为buffer cache。通常对于ext3文件系统而言,page cache的大小为4KB,所以ext3每次操作的数据块大小都是4KB的整数倍。Buffer cache的缓存块大小通常由块设备的大小来决定,取值范围在512B~4KB之间,取块设备大小的最大公约数。


http://alanwu.blog.51cto.com/3652632/1112079

Linux中Buffer cache性能问题一探究竟

1, Buffer cache的作用

为了提高磁盘设备的IO性能,我们采用内存作为磁盘设备的cache。用户操作磁盘设备的时候,首先将数据写入内存,然后再将内存中的脏数据定时刷新到磁盘。这个用作磁盘数据缓存的内存就是所谓的buffer cache。在以前的Linux系统中,有很完善的buffer cache软件层,专门负责磁盘数据的缓存。在磁盘设备的上层往往会架构文件系统,为了提高文件系统的性能,VFS层同样会提供文件系统级别的page cache。这样就导致系统中存在两个cache,并且重叠在一起,显得没有必要和冗余。为了解决这个问题,在现有的Linux系统中对buffer cache软件层进行了弱化,并且和page cache进行了整合。Buffer cache和page cache都采用radix tree进行维护,只有当访问裸设备的时候才会使用buffer cache,正常走文件系统的IO不会使用buffer cache。

我们知道ext3文件系统的page cache都是以page页大小为单位的,那么buffer cache中缓存块大小究竟是多大呢?其对性能影响如何呢?这两天我在Linux-2.6.23平台上针对这个问题做了很多实验,得到了一些数据结果,并从源代码分析中得到设置缓存块大小的方法。在此对这个buffer cache的性能问题进行分析说明,供大家讨论。

2, Buffer cache的性能问题

2.1 测试实验

首先让我们来做一个实验,在Linux-2.6.23平台上,采用dd工具对一个块设备进行顺序写操作,可以采用如下的命令格式:

1
dd if=/dev/zero of=/dev/sda2 bs=<request_size> count=100

采用该命令在不同buffer cache块(blk_size)大小配置的情况下测试不同请求大小(req_size)的IO性能,可以得到如下表所示的测试数据:

表:不同buffer cache块大小配置下的吞吐量

将表中的数据做成性能对比图,如下图所示:

从图中可以看出,在请求大小小于Cache块大小的时候,Cache块越大,IO性能越高;但是,请求大小大于Cache块大小之后,性能都有明显的飞跃。

例如,当buffer cache块大小被配置成2KB时,小于2KB的块性能基本都在19MB/s左右;当buffer cache块大小被配置成512B时,小于512B的写性能都保持在5MB/s;当buffer cache块大小被配置成1024B时,小于1KB的写性能基本都保持在9.5MB/s上下。这就说明对于小于cache块大小的small_write,buffer cache越大,其性能会越好,反之,性能越差,这就是buffer cache的作用。

观察发现一旦请求大小大于等于cache块大小之后,性能急剧提升,由于测试工具的IO压力足够大,能够一下子将磁盘性能耗尽。这是为什么呢?其实,当请求块比较小时,对于cache块而言是“局部操作”,这种“局部操作”会引入buffer cache的数据读操作,并且数据读操作和用户写操作存在顺序关系,这就极大的影响了IO的写性能。因此,当请求大小大于cache块时,并且能够和Cache块对齐时,就能够充分利用磁盘的IO带宽,所以就产生了上图中所示的性能飞跃。

看到上图中的测试结果之后,我们就会想在实际应用中,我们该如何选择buffer cache的块大小?如果请求大小是512B时,显然将buffer cache块设置成512比较合适;如果请求大小是256B时,显然将buffer cache块设置成2KB比较合适。所以,个人认为块大小的设置还需要根据实际的应用来决定,不同的应用需要设置不同的块大小,这样才能使整体性能达到最佳。

2.2 Buffer cache块大小

Linux系统在创建块设备的时候是如何设置块大小的呢?这里面涉及到Linux针对块大小设置的一个小小算法。在此结合源码对Linux的这个方法加以说明。

总体来说,Linux决定buffer cache块大小采用的是“最大块大小”的设计思想。Linux根据块设备容量决定buffer cache的块大小,并且将值域限定在512B和4KB之间。当然,这个值域内的元素不是连续的,并且都是2的幂。在这个值域的基础上取块设备大小的最大公约数,这个值就是buffer cache的块大小。这种算法的指导思想就是buffer cache的块越大越好,因此,能够取2KB就不会选择512B。Linux中算法实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void bd_set_size(struct block_device *bdev, loff_t size)
{
	unsigned bsize = bdev_logical_block_size(bdev);

	bdev->bd_inode->i_size = size;      //size为块设备大小
	while (bsize < PAGE_CACHE_SIZE) {   //bsize不能大于Page size
		if (size & bsize)
			break;
		bsize <<= 1;    //bsize只能取2的幂
	}
	bdev->bd_block_size = bsize;
	/* 设置buffer cache块大小 */
	bdev->bd_inode->i_blkbits = blksize_bits(bsize);
}

3, 小结

本文对buffer cache的性能问题进行了分析,通过实验发现当请求块比较小时,buffer cache块大小对IO性能有很大的影响。Linux根据块设备的容量采用“最大cache块”的思想决定buffer cache的块大小。在实际应用中,我们应该根据应用特征,通过实际测试来决定buffer cache块大小。


通常Linux的“block size”指的是1024 bytes,Linux用1024-byte blocks 作为buffer cache的基本单位。但linux的文件系统的block确不一样。例如ext3系统,block size是4096。使用tune2fs可以查看带文件系统的磁盘分区的相关信息,包括block size。

例如:

1
2
tune2fs -l /dev/sda2 |grep "Block size"
Block size:               4096

另一个工具dumpe2fs也可以。 dumpe2fs /dev/sda2 | grep “Block size”

其实本来这几个概念不是很难,主要是NND他们的名字都一样,都叫“Block Size”。

1.硬件上的 block size, 应该是"sector size",linux的扇区大小是512byte

2.有文件系统的分区的block size, 是"block size",大小不一,可以用工具查看

3.没有文件系统的分区的block size,也叫“block size”,大小指的是1024 byte

4.Kernel buffer cache 的block size, 就是"block size",大部分PC是1024

5.磁盘分区的"cylinder size",用fdisk -l可以查看。

我们来看看fdisk显示的不同的信息,理解一下这几个概念:

1
2
3
4
5
6
7
Disk /dev/hda: 250.0 GB, 250059350016 bytes
255 heads, 63 sectors/track, 30401 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
   Device Boot    Start       End    Blocks   Id  System
/dev/hda1   *         1      1305  10482381   83  Linux
/dev/hda2          1306      1566   2096482+  82  Linux swap
/dev/hda3          1567     30401 231617137+  83  Linux

8225280就是cylinder size。一共有30401个cylinder。Start和End分别标记的是各个分区的起始cylinder。第4列显示的就是以1024为单位的block(这一列最容易把人搞晕)。为什么“2096482+”有个“+”号呢?因为啊,总size除1024除不尽,是个约数。

Linux内核页回收 swappiness参数

http://www.douban.com/note/349467816/

本文主要尝试解释两个问题:
1. swappiness的确切含义是什么,它对内核进行页回收机制的影响。
2. swappiness设置成0,为什么系统仍然可能会有swap发生。

一. 关于内存分配与页回收(page reclaim)

page reclaim发生的场景主要有两类,一个是kswapd后台线程进行的活动,另一个是direct reclaim,即分配页时没有空闲内存满足,需要立即直接进行的页回收。大体上内存分配的流程会分为两部分,一部分是fast path,另一部分是slow path,通常内存使用非紧张情况下,都会在fast path就可以满足要求。并且fast path下的内存分配不会出现dirty writeback及swap等页回收引起的IO阻塞情况。

fast path大体流程如下:

1.如果系统挂载使用了memory cgroup,则首先检查是否超过cgroup限额,如果超过则进行direct reclaim,通过do_try_to_free_pages完成。如果没超过则进行cgroup的charge工作(charge是通过两阶段提交完成的,这里不展开了)。

2.从本地prefered zone内存节点查找空闲页,需要判断是否满足系统watermark及dirty ratio的要求,如果满足则从buddy system上摘取相应page,否则尝试对本地prefered zone进行页回收,本次fast path下页回收只会回收clean page,即不会考虑dirty page以及mapped page,这样就不会产生任何swap及writeback,即不会引起任何blocking的IO操作,如果这次回收仍然无法满足请求的内存页数目则进入slow path

slow path大体流程如下:

  1. 首先唤醒kswapd进行page reclaim后台操作。

  2. 重新尝试本地prefered zone进行分配内存,如果失败会根据请求的GFP相关参数决定是否尝试忽略watermark, dirty ratio以及本地节点分配等要求进行再次重试,这一步中如果分配页时有指定__GFP_NOFAIL标记,则分配失败会一直等待重试。

  3. 如果没有__GFP_NOFAIL标记,则会需开始进行page compact及page direct reclaim操作,之后如果仍然没有可用内存,则进入OOM流程。

相关内容可以参阅内核代码__alloc_pages函数的逻辑,另外无论page reclaim是由谁发起的,最终都会统一入口到shrink_zone,即针对每个zone独立进行reclaim操作,最终会进入shrink_lruvec函数,进行每个zone相应page lru链表的扫描与回收操作。

二. 关于页回收的一些背景知识

页回收大体流程会先在每个zone上扫描相应的page链表,主要包括inactive anon/active anon(匿名页链表)以及inactive file/active file链表(file cache/映射页链表),一共四条链表,我们所有使用过的page在被回收前基本是保存在这四条链表中的某一条中的(还有一部分在unevictable链表中,忽略),根据其被引用的次数会决定其处于active还是inactive链表中,根据其类型决定处于anon还是file链表中。

页回收总体会扫描逐个内存节点的所有zone,然后先扫描active,将不频繁访问的页挪到inactive链表中,随后扫描inactive链表,会将其中被频繁引用的页重新挪回到active中,确认不频繁的页则最终被回收,如果是file based的页则根据是否clean进行释放或回写(writeback,filecache则直接释放),如果是anon则进行swap,所以本文实际关心的是swappiness参数对anon链表扫描的影响。

另外还需要了解前面描述的四个链表原来是放在zone数据结构上的,后来引入了mem_cgroup则,重新定义了一组mem_cgroup_per_zone/mem_cgroup_per_node的数据结构,这四个链表同时定义在这组数据结构上,如果系统开启了mem cgroup则使用后者,否则用前者。

另外再重点说下swap只是page reclaim的一种处理措施,主要针对anon page,我们最终来看下swappiness的确切含义

三. swappiness对page reclaim的确切影响

page reclaim逻辑中对前面所述四个链表进行扫描的逻辑在vmscan.c中的get_scan_count函数内,该函数大部分逻辑注释写得非常清楚,我们简单梳理下,主要关注scan_balance变量的取值:

  1. 首先如果系统禁用了swap或者没有swap空间,则只扫描file based的链表,即不进行匿名页链表扫描 代码:
1
2
3
4
if (!sc->may_swap || (get_nr_swap_pages() <= 0)) {
	scan_balance = SCAN_FILE;
	goto out;
}
  1. 如果当前进行的不是全局页回收(cgroup资源限额引起的页回收),并且swappiness设为0,则不进行匿名页链表扫描,这个是没得商量,这里swappiness值直接决定了是否有swap发生,设成0则肯定不会发生,另外需要注意,这种情况下需要设置的是cgroup配置文件memory.swappiness,而不是全局的sysctl vm.swappiness 代码:
1
2
3
4
if (!global_reclaim(sc) && !vmscan_swappiness(sc)) {
	scan_balance = SCAN_FILE;
	goto out;
}
  1. 如果进行链表扫描前设置的priority(这个值决定扫描多少分之一的链表元素)为0,且swappiness非0,则可能会进行swap 代码:
1
2
3
4
if (!sc->priority && vmscan_swappiness(sc)) {
	scan_balance = SCAN_EQUAL;
	goto out;
}
  1. 如果是全局页回收,并且当前空闲内存和所有file based链表page数目的加和都小于系统的high watermark,则必须进行匿名页回收,则必然会发生swap,可以看到这里swappiness的值如何设置是完全无关的,这也解释了为什么其为0,系统也会进行swap的原因,另外最后我们会详细解释系统page watermark是如何计算的。 代码:
1
2
3
4
5
6
7
8
9
10
11
12
anon = get_lru_size(lruvec, LRU_ACTIVE_ANON) +
		get_lru_size(lruvec, LRU_INACTIVE_ANON);
file = get_lru_size(lruvec, LRU_ACTIVE_FILE) +
		get_lru_size(lruvec, LRU_INACTIVE_FILE);

if (global_reclaim(sc)) {
	free = zone_page_state(zone, NR_FREE_PAGES);
	if (unlikely(file + free <= high_wmark_pages(zone))) {
		scan_balance = SCAN_ANON;
		goto out;
	}
}
  1. 如果系统inactive file链表比较充足,则不考虑进行匿名页的回收,即不进行swap 代码:
1
2
3
4
if (!inactive_file_is_low(lruvec)) {
	scan_balance = SCAN_FILE;
	goto out;
}
  1. 最后一种情况则要根据swappiness值与之前统计的file与anon哪个更有价值来综合决定file和anon链表扫描的比例,这时如果swappiness设置成0,则也不会扫描anon链表,即不进行swap,代码比较多,不再贴出。

四. 系统内存watermark的计算

前面看到系统内存watermark对页回收机制是有决定影响的,其实在内存分配中也会频繁用到这个值,确切的说它有三个值,分别是low,min和high,根据分配页时来指定用哪个,如果系统空闲内存低于相应watermark则分配会失败,这也是进入slow path或者wakeup kswapd的依据。

实际这个值的计算是通过sysctl里的vm.min_free_kbytes来决定的,大体的计算公式如下:

1
2
3
4
5
6
pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
tmp = (u64)pages_min * zone->managed_pages;
do_div(tmp, lowmem_pages);
zone->watermark[WMARK_MIN] = tmp;
zone->watermark[WMARK_LOW] = min_wmark_pages(zone) + (tmp >> 2);
zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1);

即根据min_free_kbytes的值按照每个zone管理页面的比例算出zone的min_watermark,然后再加min的1/4就是low,加1/2就是high了

总结:

swappiness的值是个参考值,是否会发生swap跟当前是哪种page reclaim及系统当前状态都有关系,所以设置了swappiness=0并不代表一定没有swap发生,同时设为0也确实会可能发生OOM。

个人仍然认为线上环境设置swappiness=0是没有任何问题的。

Linux swap实现

http://blog.csdn.net/freas_1990/article/details/9090601

swap是现代Unix操作系统一个非常重要的特性。尤其在大型数据库服务器上,swap往往是性能首要查看指标。

通俗的说法,在Unix里,将开辟一个磁盘分区,用作swap,这块磁盘将作为内存的的替代品,在内存不够用的时候,把一部分内存空间交换到磁盘上去。

而Unix的swap功能也成为了Unixer们认为Unix由于windows的一个论据(?)。在Unix里,swap一般被认为设置为内存的2倍大小。这个2倍大小的指标出自哪里,到目前为止我也没有找到(?如果你找到了可以留言或发私信)。

不过,在内存不断掉价的今天,swap的功效已经越来越弱化了——在2013年6月13日23:01,如果一个OLTP系统的swap使用超过了2G以上,基本上可以对这个系统的性能产生怀疑了。swap并不是一种优化机制,而是一种不得已而为之的手段,防止在内存紧张的时刻,操作系统性能骤降以至瞬间崩溃。swap的价值主要体现在可以把这个崩溃的时间提升至几小时到几十个小时不等。

本文主要关注CPU访问一个内存page时,发现该page不在内存中的情况。废话不多说了,先把swap的核心函数调用栈贴一下。

当CPU检查一个页目录项/页表项的Present标志位时,如果发现该标志位为0,则表示相应的物理页面不在内存。此时,CPU会被激发“页面异常”(中断中的fault),而去执行一段代码。

至于到底是这个内存页面需要重新构建、还是页面的内容是存储到磁盘上去了,CPU本身是不关心的,CPU只知道中断条件发生了,要根据中断描述符跳转到另外一段代码去执行,而真正的swap或者是真的缺页的智能判断是在这段中断服务程序里做的——真正的技术是在这段中断服务程序里。(所以我在《中断——一鞭一条痕(下)》里说,作为一个初学者,不必深究中断(interrupt)、异常(exception)、陷阱(trap)这三个概念)

pte_present()函数会检查当前页面的描述entry的present标志位,查看该page是否在内存中。如果不在内存中,调用pte_none()判断是否建立了页目录、页表映射。如果连映射都没建立,说明是“真没在内存中”,需要从头建立映射关系。如果建立了映射关系,说明此时,该页面被暂时存储到磁盘上去了,应该到磁盘上去把该page取回来放到内存里。

如何去取呢?

如何到磁盘取一个page的数据到内存中去,这是一个多么熟悉的概念!思考一下Oracle的内存管理,一个block如何读入到SGA的buffer cache里去吧。其实这几十年来,核心的本源技术无论是在操作系统内核还是在数据库内核里,都是通用的,都是用来极大限度提升CPU任务管理能力、内存管理效率的,所有的理念、技术都是通用的——如果你站在一个系统程序猿的角度来思考,一定能明白的——不要把自己局限在一个产品里,无论这个产品是数据库、CPU、还是操作系统,这些看似绚烂神秘的技术在30年以前,已经被人反复的讨论和意淫过了。

接下来就到了核心部分了——do_swap_page()函数。

源代码如下(linux/mm/memory.c line 2022~1060):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static int do_swap_page(struct mm_struct * mm,
	struct vm_area_struct * vma, unsigned long address,
	pte_t * page_table, swp_entry_t entry, int write_access)
{ 
	struct page *page = lookup_swap_cache(entry);
	pte_t pte;

	if (!page) {
		lock_kernel();
		swapin_readahead(entry);
		page = read_swap_cache(entry);
		unlock_kernel();
		if (!page)
			return -1;

		flush_page_to_ram(page);
		flush_icache_page(vma, page);
	}

	mm->rss++;

	pte = mk_pte(page, vma->vm_page_prot);

	/*
	 * Freeze the "shared"ness of the page, ie page_count + swap_count.
	 * Must lock page before transferring our swap count to already
	 * obtained page count.
	 */
	lock_page(page);
	swap_free(entry);
	if (write_access && !is_page_shared(page))
		pte = pte_mkwrite(pte_mkdirty(pte));
	UnlockPage(page);

	set_pte(page_table, pte);
	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, address, pte);
	return 1;   /* Minor fault */
}

这里有2个参数需要重点关注,一个是(pte_t *)page_table,另外一个是(swp_entry_t*)entry

当一个page在内存中,不需要swap in时,描述该page的entry是pte_t类型的;反之,是swp_entry_t类型。

swap_entry_t(include/linux/shmem_fs.h)定义如下:

1
2
3
typedef struct {
	unsigned long val;
} swp_entry_t;

问题出来了,既然都进入do_swap_page()函数了,说明是需要swap in了,为什么还会传入一个pte_t类型的变量呢?

答案是,当在do_swap_page()之前,page是在磁盘上的,描述类型是swp_entry_t,而do_swap_page()之后,页面已经从磁盘交换到内存了,这个时候描述类型就是pte_t了。

至于lookup_swap_cache、swapin_readahead(预读——read ahead)等函数就不一一分析了,从名字就可以看出其技巧了。都是些在数据库server上的常用技巧。如果你是行家,一眼就能看出来。

Linux Cache 机制探究

http://www.penglixun.com/tech/system/linux_cache_discovery.html

相关源码主要在:
./fs/fscache/cache.c Cache实现的代码
./mm/slab.c SLAB管理器代码
./mm/swap.c 缓存替换算法代码
./mm/mmap.c 内存管理器代码
./mm/mempool.c 内存池实现代码

0. 预备:Linux内存管理基础

创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。Linux除了内核以外,App都不能直接使用内存,因为Linux采用Memory Map的管理方式,App拿到的全部是内核映射自物理内存的一块虚拟内存。malloc分配很少会失败,因为malloc只是通知内存App需要内存,在没有正式使用之前,这段内存其实只在真正开始使用的时候才分配,所以malloc成功了并不代表使用的时候就真的可以拿到这么多内存。据说Google的tcmalloc改进了这一点。

进程对内存区域的分配最终多会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap())。内核使用do_mmap()函数创建一个新的线性地址区间,如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中,无论是扩展已存在的内存区域还是创建一个新的区域。同样释放一个内存区域使用函数do_ummap(),它会销毁对应的内存区域。

另一个重要的部分是SLAB分配器。在Linux中以页为最小单位分配内存对于内核管理系统物理内存来说是比较方便的,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块,因为大都是一些描述符。一个整页中可以聚集多个这种这些小块内存,如果一样按页分配,那么会被频繁的创建/销毁,开始是非常大的。

为了满足内核对这种小内存块的需要,Linux系统采用了SLAB分配器。Slab分配器的实现相当复杂,但原理不难,其核心思想就是Memory Pool。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到Memory Pool里,留做下次使用,这就避免了频繁创建与销毁对象所带来的额外负载。

Slab技术不但避免了内存内部分片带来的不便,而且可以很好利用硬件缓存提高访问速度。但Slab仍然是建立在页面基础之上,Slab将页面分成众多小内存块以供分配,Slab中的对象分配和销毁使用kmem_cache_alloc与kmem_cache_free。

关于SALB分配器有一份资料:http://lsec.cc.ac.cn/~tengfei/doc/ldd3/ch08s02.html

关于内存管理的两份资料:http://lsec.cc.ac.cn/~tengfei/doc/ldd3/ch15.html

http://memorymyann.javaeye.com/blog/193061

1. Linux Cache的体系

在 Linux 中,当App需要读取Disk文件中的数据时,Linux先分配一些内存,将数据从Disk读入到这些内存中,然后再将数据传给App。当需要往文件中写数据时,Linux先分配内存接收用户数据,然后再将数据从内存写到Disk上。Linux Cache 管理指的就是对这些由Linux分配,并用来存储文件数据的内存的管理。

下图描述了 Linux 中文件 Cache 管理与内存管理以及文件系统的关系。从图中可以看到,在 Linux 中,具体的文件系统,如 ext2/ext3/ext4 等,负责在文件 Cache和存储设备之间交换数据,位于具体文件系统之上的虚拟文件系统VFS负责在应用程序和文件 Cache 之间通过 read/write 等接口交换数据,而内存管理系统负责文件 Cache 的分配和回收,同时虚拟内存管理系统(VMM)则允许应用程序和文件 Cache 之间通过 memory map的方式交换数据,FS Cache底层通过SLAB管理器来管理内存。

下图则非常清晰的描述了Cache所在的位置,磁盘与VFS之间的纽带。

2. Linux Cache的结构

在 Linux 中,文件 Cache 分为两层,一是 Page Cache,另一个 Buffer Cache,每一个 Page Cache 包含若干 Buffer Cache。内存管理系统和 VFS 只与 Page Cache 交互,内存管理系统负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射;VFS 负责 Page Cache 与用户空间的数据交换。而具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。读缓存以Page Cache为单位,每次读取若干个Page Cache,回写磁盘以Buffer Cache为单位,每次回写若干个Buffer Cache。 Page Cache、Buffer Cache、文件以及磁盘之间的关系如下图所示。

Page 结构和 buffer_head 数据结构的关系如下图所示。Page指向一组Buffer的头指针,Buffer的头指针指向磁盘块。在这两个图中,假定了 Page 的大小是 4K,磁盘块的大小是 1K。

在 Linux 内核中,文件的每个数据块最多只能对应一个 Page Cache 项,它通过两个数据结构来管理这些 Cache 项,一个是 Radix Tree,另一个是双向链表。Radix Tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项,图 4 是 radix tree的一个示意图,该 radix tree 的分叉为4(22),树高为4,用来快速定位8位文件内偏移。Linux(2.6.7) 内核中的分叉为 64(26),树高为 6(64位系统)或者 11(32位系统),用来快速定位 32 位或者 64 位偏移,Radix tree 中的每一个到叶子节点的路径上的Key所拼接起来的字串都是一个地址,指向文件内相应偏移所对应的Cache项。

查看Page Cache的核心数据结构struct address_space就可以看到上述结构(略去了无关结构):

1
2
3
4
5
6
7
struct address_space  {
	struct inode             *host;              /* owner: inode, block_device */
	struct radix_tree_root      page_tree;         /* radix tree of all pages */
	unsigned long           nrpages;  /* number of total pages */
	struct address_space       *assoc_mapping;      /* ditto */
	......
} __attribute__((aligned(sizeof(long))));

下面是一个Radix Tree实例:

另一个数据结构是双向链表,Linux内核为每一片物理内存区域(zone) 维护active_list和inactive_list两个双向链表,这两个list主要用来实现物理内存的回收。这两个链表上除了文件Cache之 外,还包括其它匿名(Anonymous)内存,如进程堆栈等。

相关数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
truct page{
	struct list_head list;   //通过使用它进入下面的数据结构free_area_struct结构中的双向链队列
	struct address_space * mapping;   //用于内存交换的数据结构
	unsigned long index;//当页面进入交换文件后
	struct page *next_hash; //自身的指针,这样就可以链接成一个链表
	atomic t count; //用于页面交换的计数,若页面为空闲则为0,分配就赋值1,没建立或恢复一次映射就加1,断开映射就减一
	unsigned long flags;//反应页面各种状态,例如活跃,不活跃脏,不活跃干净,空闲
	struct list_head lru;
	unsigned long age; //表示页面寿命
	wait_queue_head_t wait;
	struct page ** pprev_hash;
	struct buffer_head * buffers;
	void * virtual
	struct zone_struct * zone; //指向所属的管理区
}
typedef struct free_area_struct {
	struct list_head free_list;   //linux 中通用的双向链队列
	unsigned int * map;
} free_area_t;
typedef struct zone_struct{
	spinlock_t        lock;
	unsigned long offset;  //表示该管理区在mem-map数组中,起始的页号
	unsigned long free pages;
	unsigned long inactive_clean_pages;
	unsigned long inactive_dirty_pages;
	unsigned pages_min, pages_low, pages_high;
	struct list_head inactive_clean_list;   //用于页面交换的队列,基于linux页面交换的机制。这里存贮的是不活动“干净”页面
	free_area_t free_area[MAX_ORDER]; //一组“空闲区间”队列,free_area_t定义在上面,其中空闲下标表示的是页面大小,例如:数组第一个元素0号,表示所有区间大小为2的 0次方的页面链接成的双向队列,1号表示所有2的1次方页面链接链接成的双向队列,2号表示所有2的2次方页面链接成的队列,其中要求是这些页面地址连续
	char * name;
	unsigned long size;
	struct pglist_data * zone_pgdat;   //用于指向它所属的存贮节点,及下面的数据结构
	unsigned  long  zone_start_paddr;
	unsigned  long    zone_start_mapnr;
	struct page * zone_mem_map;
} zone_t;

3. Cache预读与换出

Linux 内核中文件预读算法的具体过程是这样的: 对于每个文件的第一个读请求,系统读入所请求的页面并读入紧随其后的少数几个页面(不少于一个页面,通常是三个页 面),这时的预读称为同步预读。对于第二次读请求,如果所读页面不在Cache中,即不在前次预读的group中,则表明文件访问不是顺序访问,系统继续 采用同步预读;如果所读页面在Cache中,则表明前次预读命中,操作系统把预读group扩大一倍,并让底层文件系统读入group中剩下尚不在 Cache中的文件数据块,这时的预读称为异步预读。无论第二次读请求是否命中,系统都要更新当前预读group的大小。

此外,系统中定义了一个 window,它包括前一次预读的group和本次预读的group。任何接下来的读请求都会处于两种情况之一:

第一种情况是所请求的页面处于预读 window中,这时继续进行异步预读并更新相应的window和group;

第二种情况是所请求的页面处于预读window之外,这时系统就要进行同步 预读并重置相应的window和group。

下图是Linux内核预读机制的一个示意图,其中a是某次读操作之前的情况,b是读操作所请求页面不在 window中的情况,而c是读操作所请求页面在window中的情况。

Linux内核中文件Cache替换的具体过程是这样的:刚刚分配的Cache项链入到inactive_list头部,并将其状态设置为active,当内存不够需要回收Cache时,系统首先从尾部开始反向扫描 active_list并将状态不是referenced的项链入到inactive_list的头部,然后系统反向扫描inactive_list,如果所扫描的项的处于合适的状态就回收该项,直到回收了足够数目的Cache项。其中Active_list的含义是热访问数据,及多次被访问的,inactive_list是冷访问数据,表示尚未被访问的。如果数据被访问了,Page会被打上一个Refrence标记,如果Page没有被访问过,则打上Unrefrence标记。这些处理在swap.c中可以找到。 下图也描述了这个过程。

下面的代码描述了一个Page被访问它的标记为变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
 * Mark a page as having seen activity.
 *
 * inactive,unreferenced        ->      inactive,referenced
 * inactive,referenced          ->      active,unreferenced
 * active,unreferenced          ->      active,referenced
 */
void mark_page_accessed(struct page *page)
{
	if (!PageActive(page) && !PageUnevictable(page) &&
			PageReferenced(page) && PageLRU(page)) {
		activate_page(page);
		ClearPageReferenced(page);
	} else if (!PageReferenced(page)) {
		SetPageReferenced(page);
	}
}

参考文章:

http://lsec.cc.ac.cn/~tengfei/doc/ldd3/

http://memorymyann.javaeye.com/blog/193061

http://www.cublog.cn/u/20047/showart.php?id=121850

http://blog.chinaunix.net/u2/74194/showart_1089736.html

关于内存管理,Linux有一个网页:http://linux-mm.org/

linux mmap 详解

http://blog.chinaunix.net/uid-20321537-id-3483405.html

一.前言

mmap的具体实现以前在学习内核时学习过,但是对于其中的很多函数是一知半解的,有些只能根据其函数名来猜测其具体的功能,在本文中,一起来重新深入理解其具体的实现。

二.mmap的用户层应用

1
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);

具体参数含义

start : 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length: 代表将文件中多大的部分映射到内存。
prot : 映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
flags : 影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd : 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE的整数倍。

返回值:
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

错误代码:
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。

用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。

三.mmap的内核实现

对于mmap的内核有了解的都会知道用户层的mmap到内核层的mmap其中多了一个参数vma_struct这个结构体,在开始时对于这个参数很疑惑就是这个参数的值是哪儿来的,在这里我们会一一来讲述。

mmap() —> sys_mmap_pgoff() 内核系统调用函数

munmap() —>sys_munmap() 内核系统调用函数,其最终调用unmap_region()来解除映射关系,不需要对应的file_operation有unmap操作项.

还是从do_mmap开始吧。

3.1 do_mmap

参数说明:
file :就是用户层想要映射的file
addr :欲映射的起始地址,即用户层的start
prot :用户层传入的port
flag :同上
offset:同上

从这里可以知道,这里面的参数几乎均是用户层传入的参数。

1
2
3
4
5
6
7
8
9
10
11
static inline unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,
									unsigned long flag, unsigned long offset)
{
	unsigned long ret = -EINVAL;
	if ((offset + PAGE_ALIGN(len)) < offset)  --页对齐len,检测传入参数是否有误。
		goto out;
	if (!(offset & ~PAGE_MASK))           --检测offset是否页对齐。映射时只能映射页对齐的长度。
		ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
	return ret;
}

3.2 do_mmap_pgoff

这个函数是巨大的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flags, unsigned long pgoff)
{
	struct mm_struct * mm = current->mm;      --当前用户进程的mm
	struct inode *inode;
	unsigned int vm_flags;
	int error;
	int accountable = 1;
	unsigned long reqprot = prot;

	if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))   --是否隐藏了可执行属性。
		if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
			prot |= PROT_EXEC;

	if (!len)
		return -EINVAL;

	if (!(flags & MAP_FIXED))              -
		addr = round_hint_to_min(addr);    --判断输入的欲映射的起始地址是否小于最小映射地址,如果小于,将addr修改为最小地址,不过前提是MAP_FIXED旗标没有设置。

	error = arch_mmap_check(addr, len, flags);   --不同平台对于mmap参数的不同检测。这里之间返回0
	if (error)
		return error;

	len = PAGE_ALIGN(len);        --检测len是否越界,len的范围在0~TASK_SIZE之间。
	if (!len || len > TASK_SIZE)
		return -ENOMEM;             --错误值为nomem

	if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)  --再次检测是否越界。我们这里不得不小心哪个晕头了传入一个莫名其妙的值
	return -EOVERFLOW;

	if (mm->map_count > sysctl_max_map_count)   --在一个进程中对于mmap个数是有限制的。超出了还是nomem的错误。
		return -ENOMEM;

	addr = get_unmapped_area(file, addr, len, pgoff, flags);  --获取没有映射的地址,这个是查询mm中空闲的内存地址,这个在下面理解。
	if (addr & ~PAGE_MASK)
		return addr;

	vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags |
			   VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;      --设置vm_flags,根据传入的port和flags以及mm本身自有的旗标来设置。

	if (flags & MAP_LOCKED) {
		if (!can_do_mlock())
			return -EPERM;
		vm_flags |= VM_LOCKED;
	}

	if (vm_flags & VM_LOCKED) {
		unsigned long locked, lock_limit;
		locked = len >> PAGE_SHIFT;
		locked += mm->locked_vm;
		lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
		lock_limit >>= PAGE_SHIFT;
		if (locked > lock_limit && !capable(CAP_IPC_LOCK))
			return -EAGAIN;
	}
	--关于锁定的内存区在以后学习中再看,这里就不细看。
	inode = file ? file->f_path.dentry->d_inode : NULL;  --判断是否匿名映射,如果不是则赋值inode

	if (file) {
		switch (flags & MAP_TYPE) {   --MAP_TYPE = 0x0F type的掩码
		case MAP_SHARED:
			if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))   --file应该被打开并允许写入。
				return -EACCES;
			if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))  --不能写入一个只允许写追加的文件
				return -EACCES;
			if (locks_verify_locked(inode))      --确保文件没有被强制锁定。
				return -EAGAIN;

			vm_flags |= VM_SHARED | VM_MAYSHARE;  --尝试允许其他进程共享。
			if (!(file->f_mode & FMODE_WRITE))    --如果file不允许写就算了,共享也没有用啊,因为file就一直固定死了,共享也没有意义。
				vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
		case MAP_PRIVATE:
			if (!(file->f_mode & FMODE_READ))
				return -EACCES;
			if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
				if (vm_flags & VM_EXEC)
					return -EPERM;
				vm_flags &= ~VM_MAYEXEC;
			}
			if (is_file_hugepages(file))
				accountable = 0;

			if (!file->f_op || !file->f_op->mmap)
				return -ENODEV;
			break;

		default:
			return -EINVAL;
		}
	} else {
		switch (flags & MAP_TYPE) {
		case MAP_SHARED:
			pgoff = 0;
			vm_flags |= VM_SHARED | VM_MAYSHARE;
			break;
		case MAP_PRIVATE:
			pgoff = addr >> PAGE_SHIFT;
			break;
		default:
			return -EINVAL;
		}
	}
	--上面就是对一些旗标进行检测,防止出现旗标冲突,比如我欲映射的文件不允许写,而我映射的旗标却设定是可写并可以共享的,这个就冲突了。
	error = security_file_mmap(file, reqprot, prot, flags, addr, 0);   --这个函数就忽略了。
	if (error)
		return error;

	return mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable);  --最后一个参数为是否为大页,如果是的就为0.其余的参数都好理解。
}

3.3 get_unmapped_area

这个是获取没有被映射的内存区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags)
{
	unsigned long (*get_area)(struct file *, unsigned long,unsigned long, unsigned long, unsigned long);
	get_area = current->mm->get_unmapped_area;
	if (file && file->f_op && file->f_op->get_unmapped_area)
		get_area = file->f_op->get_unmapped_area;
	addr = get_area(file, addr, len, pgoff, flags);
	if (IS_ERR_VALUE(addr))
		return addr;

	if (addr > TASK_SIZE - len)
		return -ENOMEM;
	if (addr & ~PAGE_MASK)
		return -EINVAL;

	return arch_rebalance_pgtables(addr, len);
}

对于get_area函数我们以arch_get_unmapped_area为例来看如何查找一个空闲的mmap area

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff, unsigned long flags)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma;
	unsigned long start_addr;

	if (len > TASK_SIZE)
		return -ENOMEM;

	if (flags & MAP_FIXED)    --还记否这个MAP_FIXED是什么含义不?
		return addr;

	if (addr) {
		addr = PAGE_ALIGN(addr);
		vma = find_vma(mm, addr); --vma为NULL即addr的地址不在任一个VMA(vma->vm_start~vma->vm_end) addr的地址没有被映射,
						而且空洞足够我们这次的映射,那么返回addr以准备这次的映射
		if (TASK_SIZE - len >= addr &&(!vma || addr + len <= vma->vm_start))
			return addr;
	}
	if (len > mm->cached_hole_size) { --如果所需的长度大于当前vma之间的空洞长度
			start_addr = addr = mm->free_area_cache;
	} else {
			start_addr = addr = TASK_UNMAPPED_BASE;  --需要的长度小于当前空洞,为了不至于时间浪费,那么从0开始搜寻,
					这里的搜寻基地址TASK_UNMAPPED_BASE很重要,用户mmap的地址的基地址必须在TASK_UNMAPPED_BASE之上,
					但是一定这样严格 吗?看上面的if (addr)判断,如果用户给了一个地址在TASK_UNMAPPED_BASE之下,
					映射实际上还是会发生的。
			mm->cached_hole_size = 0;
	}

full_search:
	for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
		if (TASK_SIZE - len < addr) {
			if (start_addr != TASK_UNMAPPED_BASE) {
				addr = TASK_UNMAPPED_BASE;
			  start_addr = addr;
				mm->cached_hole_size = 0;
				goto full_search;
			}
			return -ENOMEM;
		}
	
		if (!vma || addr + len <= vma->vm_start) {        --如果第一次find_vma返回值即为NULL ,vma没有被映射并且空洞足够映射
						!vma的条件只有可能在循环的第一次满足,在其后不可能满足,在其后的判断条件即为
						vma->vma_end~vma->vma_next->vma_start之间的空洞大小大于所需要映射的长度即可,
						下面判断条件中的addr为vma->vma_end,而vma->vm_start为 vma->vma_next->vma_start
			mm->free_area_cache = addr + len;
			return addr;
		}
		if (addr + mm->cached_hole_size < vma->vm_start)  --在循环的第一次如果vma不为NULL,不会满足下面的条件,在以后循环中mm->cached_hole_size
						则为该次vma->vm_start 与上一次的vma->vm_end之间的差值

				mm->cached_hole_size = vma->vm_start - addr;
		addr = vma->vm_end;
	}
}

还记否以前看的红黑树,这里就现实的用了红黑树的算法。关于这个我们就不看了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
	struct vm_area_struct *vma = NULL;

	if (mm) {
		vma = mm->mmap_cache;
		if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
			struct rb_node * rb_node;
			rb_node = mm->mm_rb.rb_node;
			vma = NULL;
			while (rb_node) {
				struct vm_area_struct * vma_tmp;

				vma_tmp = rb_entry(rb_node,struct vm_area_struct, vm_rb);
				if (vma_tmp->vm_end > addr) {
					vma = vma_tmp;
					if (vma_tmp->vm_start <= addr)
						break;
					rb_node = rb_node->rb_left;
				} else
					rb_node = rb_node->rb_right;
			}
			if (vma)
				mm->mmap_cache = vma;
		}
	}
	return vma;
}

3.4 mmap_region

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, unsigned long flags,
				unsigned int vm_flags, unsigned long pgoff,int accountable)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma, *prev;
	struct vm_area_struct *merged_vma;
	int correct_wcount = 0;
	int error;
	struct rb_node **rb_link, *rb_parent;
	unsigned long charged = 0;
	struct inode *inode =  file ? file->f_path.dentry->d_inode : NULL;

	/* Clear old maps */
	error = -ENOMEM;
munmap_back:
	vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); --函数find_vma_prepare()与find_vma()基本相同,它扫描当前进程地址空间的vm_area_struct
						结构所形成的红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,
						说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()
						把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;
						如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区
	if (vma && vma->vm_start < addr + len) {
		if (do_munmap(mm, addr, len))
			return -ENOMEM;
		goto munmap_back;
	}
	if (!may_expand_vm(mm, len >> PAGE_SHIFT))                   -- 页数和超过限定值返回 0 ,不超过返回1
		return -ENOMEM;

	if (flags & MAP_NORESERVE)              -- 如果flags参数中没有设置MAP_NORESERVE标志,新的虚拟区含有私有的可写页,空闲页面数小于要映射的虚拟区
						的大小;则函数终止并返回一个负数;其中函数security_vm_enough_memory()用来检查一个
						进程的地址空间中是否有足够的内存来进行一个新的映射
		vm_flags |= VM_NORESERVE;

	if (accountable && (!(flags & MAP_NORESERVE) ||
				sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
		if (vm_flags & VM_SHARED) {
			/* Check memory availability in shmem_file_setup? */
			vm_flags |= VM_ACCOUNT;
		} else if (vm_flags & VM_WRITE) {
			charged = len >> PAGE_SHIFT;
			if (security_vm_enough_memory(charged))
				return -ENOMEM;
			vm_flags |= VM_ACCOUNT;
		}
	}
	if (!file && !(vm_flags & VM_SHARED)) { --如果是匿名映射(file为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和与它紧挨的前一个虚拟区进行合并;
		虚拟区的合并是由vma_merge()函数实现的。如果合并成功,则转out处,请看后面out处的代码。
		vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
					NULL, NULL, pgoff, NULL);
		if (vma)
			goto out;
	}
	vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
	if (!vma) {
		error = -ENOMEM;
		goto unacct_error;
	}

	vma->vm_mm = mm;
	vma->vm_start = addr;
	vma->vm_end = addr + len;
	vma->vm_flags = vm_flags;
	vma->vm_page_prot = vm_get_page_prot(vm_flags);
	vma->vm_pgoff = pgoff;

	if (file) {
		error = -EINVAL;
		if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
			goto free_vma;
		if (vm_flags & VM_DENYWRITE) {
			error = deny_write_access(file);
			if (error)
				goto free_vma;
			correct_wcount = 1;
		}
		vma->vm_file = file;
		get_file(file);
		error = file->f_op->mmap(file, vma);    -- (⊙o⊙)哦 ,终于可以调用设备文件中真正的mmap
		if (error)
			goto unmap_and_free_vma;
		if (vm_flags & VM_EXECUTABLE)
			added_exe_file_vma(mm);
	} else if (vm_flags & VM_SHARED) {
		error = shmem_zero_setup(vma);// it will call shmem_file_setup(), the same way as called in ashmem.c
		if (error)
			goto free_vma;
	}

如果建立的是从文件到虚存区间的映射,则:

1.当参数flags中的VM_GROWSDOWN或VM_GROWSUP标志位为1时,说明这个区间可以向低地址或高地址扩展,但从文件映射的区间不能进行扩展,因此转到free_vma,释放给vm_area_struct分配的Slab,并返回一个错误;

2.当flags中的VM_DENYWRITE标志位为1时,就表示不允许通过常规的文件操作访问该文件,所以要调用deny_write_access()排斥常规的文件操作(参见第八章)。

3.get_file()函数的主要作用是递增file结构中的共享计数;

4.每个文件系统都有个fiel_operation数据结构,其中的函数指针mmap提供了用来建立从该类文件到虚存区间进行映射的操作,这是最具有实质意义的函数;对于大部分文件系统,这个函数为generic_file_mmap( )函数实现的,该函数执行以下操作:

(1)初始化vm_area_struct结构中的vm_ops域。如果VM_SHARED标志为1,就把该域设置成file_shared_mmap, 否则就把该域设置成file_private_mmap。从某种意义上说,这个步骤所做的事情类似于打开一个文件并初始化文件对象的方法。

(2)从索引节点的i_mode域(参见第八章)检查要映射的文件是否是一个常规文件。如果是其他类型的文件(例如目录或套接字),就返回一个错误代码。

(3)从索引节点的i_op域中检查是否定义了readpage( )的索引节点操作。如果没有定义,就返回一个错误代码。

(4)调用update_atime( )函数把当前时间存放在该文件索引节点的i_atime域中,并将这个索引节点标记成脏。

5.如果flags参数中的MAP_SHARED标志位为1,则调用shmem_zero_setup()进行共享内存的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
		vma->vm_flags &= ~VM_ACCOUNT;

	addr = vma->vm_start;
	pgoff = vma->vm_pgoff;
	vm_flags = vma->vm_flags;

	if (vma_wants_writenotify(vma))
		vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED);

	merged_vma = NULL;
	if (file)
		merged_vma = vma_merge(mm, prev, addr, vma->vm_end,
			vma->vm_flags, NULL, file, pgoff, vma_policy(vma));
	if (merged_vma) {
		mpol_put(vma_policy(vma));
		kmem_cache_free(vm_area_cachep, vma);
		fput(file);
		if (vm_flags & VM_EXECUTABLE)
			removed_exe_file_vma(mm);
		vma = merged_vma;
	} else {
		vma_link(mm, vma, prev, rb_link, rb_parent);
		file = vma->vm_file;
	}

此时,把新建的虚拟区插入到进程的地址空间,这是由函数vma_link()完成的,该函数具有三方面的功能:
(1)把vma 插入到虚拟区链表中
(2)把vma插入到虚拟区形成的红黑树中
(3)把vam插入到索引节点(inode)共享链表中

函数atomic_inc(x)给*x加1,这是一个原子操作。在内核代码中,有很多地方调用了以atomic为前缀的函数。原子操作,在操作过程中不会被中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	if (correct_wcount)
		atomic_inc(&inode->i_writecount);
out:
	mm->total_vm += len >> PAGE_SHIFT;
	vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
	if (vm_flags & VM_LOCKED) {
		long nr_pages = mlock_vma_pages_range(vma, addr, addr + len);
		if (nr_pages < 0)
			return nr_pages;    /* vma gone! */
		mm->locked_vm += (len >> PAGE_SHIFT) - nr_pages;
	} else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK))
		make_pages_present(addr, addr + len);
	return addr;

unmap_and_free_vma:
	if (correct_wcount)
		atomic_inc(&inode->i_writecount);
	vma->vm_file = NULL;
	fput(file);

	unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
	charged = 0;
free_vma:
	kmem_cache_free(vm_area_cachep, vma);
unacct_error:
	if (charged)
		vm_unacct_memory(charged);
	return error;
}

ok!到此mmap的内核核心就可以了,关于具体的mmap的实现,以后再看。

四.总结

mmap的实质是什么,其实就是从每一个进程中的用户空间分配一段空间用于映射。 这里面的机关重重,需要好好理解,不过谨记一点,进程的vma_struct是采用了红黑树来管理的。对于每一段的内存区都会有一个vma_struct 来描述,比如数据区,code区等等,以及mmap所需要的一段内存区。

五.其它

1、特点:

① 进程相关的
② 与XSI共享内存一样,需要与同步原语一起使用
③ 只能是有共同祖先的进程才能使用

2、使用

系统调用mmap()用于共享内存的两种方式:
(1)使用普通文件提供的内存映射:
适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用mmap()

典型调用代码如下:

1
2
fd=open(name, flag, mode); if(fd<0) ...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);

通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,可以参看UNIX网络编程第二卷。【3】

(2)使用特殊文件提供匿名内存映射:
适用于具有亲缘关系的进程之间。由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程 继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。一般来说,子进程单独维护从父进程继 承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可。

3、说明

(1)
1
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t offset );

把文件或设备映射或解除映射到内存中

0)flag:必须有MAP_SHARED 标志
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名共享。此时会忽略参数fd(fd可以指定为-1),不涉及文件,而且映射区域无法和其他进程共享(只能用于具有亲缘关系的进程间通信)。
映射/dev/zero可为调用程序提供零填充的虚拟内存块。

1)start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。

2)length:代表将文件中多大的部分映射到内存。

3)offset 必须是页面大小的整数倍。页面大小由 getpagesize(2)得到。

4)被映射的文件大小应是页面大小的整数倍。如一个文件大小不是页面大小的整数倍,映射时多出来的区域将被赋为0,对这些区域的写不会被写回到文件中。

5)munmap()系统调用将删除指定地址范围内的映射区域。随后对这个范围内区域的引用将产生非法的内存引用。当这个进程终止后,这个区域也会被删除。另一方面,关闭文件描述符并不会删除映射区域。

6)fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。

7)若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1)。

(2) munmap
1
int munmap( void * addr, size_t len )

在进程地址空间中解除一个映射关系,当映射关系解除后,对原来映射地址的访问将导致段错误发生。

void * addr :调用mmap()时返回的地址
size_t len :映射区的大小

(3)
1
int msync ( void * addr , size_t len, int flags)

一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以调用msync()实现磁盘上文件与共享内存区的内容一致。

void * addr :调用mmap()时返回的地址
size_t len :映射区的大小
int flags :MS_ASYN: 异步写,MS_SYN : 同步写,MS_INVALIDAT : 无效的cache 数据。

5、其他

1)进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

2)一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

3)mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

4)最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小。文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。