Linux内核设计与实现---页高速缓存和页回写


页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存,通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变成对物理内存的访问,主要用来减少对磁盘的I/O操作。它由RAM中的物理页组成,缓存中每一项对应着磁盘中的多个块,每当内核开始执行一个页I/O操作时,首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。

高速缓存的价值在于两个方面:第一,访问磁盘的速度要远远低于访问内存的速度,因为,在内存访问数据比从磁盘访问速度更快。第二,数据一旦被访问,就很有可能在短期内再次被访问到。如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。

1 页高速缓存

页高速缓存缓存的是页。Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。为了满足普遍性的要求,Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。该结构体定义在文件linux/fs.h中。

struct address_space {
	struct inode		*host;		/* owner: inode, block_device */
	struct radix_tree_root	page_tree;	/* radix tree of all pages */
	spinlock_t		tree_lock;	/* and spinlock protecting it */
	unsigned int		i_mmap_writable;/* count VM_SHARED mappings */
	struct prio_tree_root	i_mmap;		/* tree of private and shared mappings */
	struct list_head	i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
	spinlock_t		i_mmap_lock;	/* protect tree, count, list */
	atomic_t		truncate_count;	/* Cover race condition with truncate */
	unsigned long		nrpages;	/* number of total pages */
	pgoff_t			writeback_index;/* writeback starts here */
	struct address_space_operations *a_ops;	/* methods */
	unsigned long		flags;		/* error bits/gfp mask */
	struct backing_dev_info *backing_dev_info; /* device readahead, etc */
	spinlock_t		private_lock;	/* for use by the address_space */
	struct list_head	private_list;	/* ditto */
	struct address_space	*assoc_mapping;	/* ditto */
} __attribute__((aligned(sizeof(long))));

i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_space中所有共享的与私有的映射页面。优先搜索树是一种将堆与radix树结合的快速检索树。address space空间大小由nrpages字段描述,表示共有多少页。
address_space结构往往会和某些内核对象关联,通常情况下会与一个索引节点关联,这时host域就会指向该索引节点,如果关联对象不是一个索引节点的话,host就被设置为NULL。
a_ops域指向地址空间对象中的操作函数表,操作函数表定义在linux/fs.h文件中,由address_space_operations结构体表示:

struct address_space_operations {
	int (*writepage)(struct page *page, struct writeback_control *wbc);
	int (*readpage)(struct file *, struct page *);
	int (*sync_page)(struct page *);

	/* Write back some dirty pages from this mapping. */
	int (*writepages)(struct address_space *, struct writeback_control *);

	/* Set a page dirty */
	int (*set_page_dirty)(struct page *page);

	int (*readpages)(struct file *filp, struct address_space *mapping,
			struct list_head *pages, unsigned nr_pages);

	/*
	 * ext3 requires that a successful prepare_write() call be followed
	 * by a commit_write() call - they must be balanced
	 */
	int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
	int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
	/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
	sector_t (*bmap)(struct address_space *, sector_t);
	int (*invalidatepage) (struct page *, unsigned long);
	int (*releasepage) (struct page *, int);
	ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
			loff_t offset, unsigned long nr_segs);
};

2 基树

每个address_space对象都有唯一的基树(radix tree),它保存在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据,页高速缓存的搜索函数find_get_page()要调用函数radix_tree_loopup(),该函数会在指定基树中搜索指定页面。

基树核心代码的通用形式可以在文件lib/radix-tree.c中找到,要想使用基树,需要包含头文件linux/radix_tree.h

3 缓冲区高速缓存

现在的Linux系统中已经不再有独立的缓冲区高速缓存了。但在2.2版本的内核中,存在两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页,后者缓存缓冲。两种缓存并不同一:一个磁盘块可以在两种缓存中同时存在,因此需要对缓存中的同一拷贝进行很麻烦的同步操作。
在2.4版本的内核开始,统一了这两种缓存,现在Linux只有唯一的页高速缓存。

4 pdflush后台例程

由于页高速缓存的缓存作用,写操作实际上会被延迟,当页高速缓存中的数据比磁盘存储的数据更新时,这时候页高速缓存中的数据被称为脏数据,脏数据所在的页被称为脏页,这些脏页最终必须被写回磁盘。在以下两种情况发送时,脏页被写回磁盘:

  • 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
  • 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存中。

上面两种工作的目的完全不同。在老内核中,这是由两个独立的内核线程分别完成(bdflush和kupdated两个线程)的,但是在2.6内核中,由一群内核线程,pdflush后台回写线程同一执行两种工作。首先,pdflush线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回磁盘。此目的是在可用物理内存过低时,释放脏页以重新获得内存。特定的内存阈值可以通过dirty_backgriud_radio sysctl系统调用设置。当空闲内存比阈值dirty_background_ratio低时,内核便会调用wakeup_bdflush()唤醒一个pdflush线程。随后pdflush线程进一步调用函数background_writeout()开始将脏页回写磁盘。函数background_writeout()需要一个长整型参数,该参数指定试图写回的页面数目。函数background_writeout会连续写出数据,直到满足以下两个条件:

  • 已经有指定的最小数目的页被写出到磁盘
  • 空闲内存数已经回升,超过了阈值dirty_background_ratio

上述条件确保了pdflush操作可以减轻系统中内存不足的压力,回写操作不会在达到这两个条件前停止,除非pdflush写回了所有的脏页,没有剩下的脏页可以写回了。

为了满足第二个目标,pdflush后台例程会被周期性唤醒,将那些在内存驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。在系统启动时,内核初始化一个定时器,让它周期地唤醒pdflush线程,随后使其运行函数wb_kupdate()。该函数将把所有驻留时间超过百分之drity_expire_centisece秒的脏页写回。然后定时器将再次被初始化为百分之drity_expire_centisece秒后唤醒pdflush线程。

pdflush线程的实现代码在文件mm/pdflush.c中,回写机制的实现代码在文件mm/page-writebacke.c和fs/fs-writeback.c中。

膝上型电脑模式

膝上型电脑模式是一种特殊的页回写策略,该策略主要目的是将磁盘转动的机械行为最小化,允许磁盘尽可能长时间停滞,以此延长电池供电时间。该模式可通过/proc/sys/vm/laptop_mode文件进行配置,通常,该文件内容为0,膝上电脑模式关闭,如果需要启动,则向配置文件写入1.

bdflush和kupdated

在2.6内核版本前,pdflush线程的工作是分别由bdflush和kupdated两个线程共同完成。当可用内存过低时,bdflush内核线程在后台执行脏页回写操作,与pdflush一样,它也有一组阈值参数,当系统中空闲内存消耗到特定内存阈值以下时,bdflush线程就被wakeup_bdflush函数唤醒。

bdflush和pdflush之间主要有两个区别。第一个是系统中只有一个bdflush线程,而pdflush线程的数目可以动态改变;第二个是bdflush线程基于缓冲,它将脏缓冲写回磁盘,pdflush基于页,它将整个脏页写回磁盘。

因为只有在内存过低和缓冲数量过大时,bdflush才刷新缓冲,所以kupdate线程被引入,以便周期性地写回脏页。

bdflush和kupdate内核线程现在完全被pdflush线程取代了。

避免拥塞的方法:使用多线程

bdflush仅仅只有一个线程,因此很有可能在页回写任务很重时,造成阻塞,这是因为单一的线程很可能堵塞在某个设备的已阻塞请求队列上,而其他设备的请求队列却没法得到处理。

2.6内核通过使用多个pdflush线程来解决上述问题。每个线程可以相互独立地将脏页刷新回磁盘,而且不同的pdflush线程处理不同的设备队列。

通过一个简单的算法,pdflush线程的数目可以根据系统的运行时间进行调整,如果所有已存在的pdflush线程都已经持续工作1秒以上,内核就会创建一个新的pdflush线程。线程数量最多不能超过MAX_PDFLUSH_THREADS,默认值是8.如果一个pdflush线程睡眠超过1秒,内核就会终止该线程,线程的数量最少不得小于MIN_PDFLUSH_THREADS,默认值是2.pdflush线程数量取决于页回写的数量和阻塞情况,动态调整。

这种方式看起来很理想,但是如果每一个pdflush线程都挂起在同一个阻塞的队列上会怎么样?在这种情况下,多个pdflush线程的性能并不会比单个线程提高多少,反而会造成严重的内存浪费。为了克服这种负面影响,pdflush线程利用阻塞避免策略,它们会积极地试图写回那些不属于阻塞队列的页面。这样一来,pdflush通过分派回写工作,阻止多个线程在同一个忙设备纠缠。所以pdflush线程很忙,此时会有一个新的pdflush线程被创建,它们才是真正的繁忙。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Linux内核 前言 第1章 硬件基础与软件基础 6 1.1 硬件基础 6 1.1.1 CPU 7 1.1.2 存储器 8 1.1.3 总线 8 1.1.4 控制器和外设 8 1.1.5 地址空间 9 1.1.6 时钟 9 1.2 软件基础 9 1.2.1 计算机语言 9 1.2.2 什么是操作系统 11 1.2.3 内核数据结构 13 第2章 内存管理 15 2.1 虚拟内存抽象模型 15 2.1.1 请求调 17 2.1.2 交换 17 2.1.3 共享虚拟内存 18 2.1.4 物理寻址模式和虚拟寻址模式 18 2.1.5 访问控制 18 2.2 高速缓存 19 2.3 Linux表 20 2.4 分配和回收 21 2.4.1 分配 22 2.4.2 回收 22 2.5 内存映射 22 2.6 请求调 23 2.7 Linux缓存 24 2.8 换出和淘汰 25 2.8.1 减少缓冲区和缓存大小 25 2.8.2 换出System V共享内存 26 2.8.3 换出和淘汰 27 2.9 交换缓存 27 2.10 换入 28 第3章 进程 29 3.1 Linux进程 29 3.2 标识符 31 3.3 调度 32 3.4 文件 34 3.5 虚拟内存 35 3.6 创建进程 36 3.7 时间和定时器 37 3.8 执行程序 38 3.8.1 ELF 39 3.8.2 脚本文件 40 第4章 进程间通信机制 41 4.1 信号机制 41 4.2 管道 42 4.3 套接字 44 4.3.1 System V的进程间通信机制 44 4.3.2 消息队列 44 4.3.3 信号量 45 4.3.4 共享存储区 47 第5章 PCI 49 5.1 PCI的地址空间 49 5.2 PCI配置头 50 5.3 PCI的I/O和存储地址空间 51 5.4 PCI-ISA桥 51 5.5 PCI-PCI 桥 51 5.5.1 PCI-PCI桥:PCI I/O和存储地址 空间的窗口 51 5.5.2 PCI-PCI桥:PCI配置周期和PCI 总线编号 52 5.6 Linux PCI初始化 53 5.6.1 Linux内核PCI数据结构 53 5.6.2 PCI设备驱动程序 53 5.6.3 PCI的BIOS函数 56 5.6.4 PCI修正过程 57 第6章 中断处理与设备驱动程序 60 6.1 中断与中断处理 60 6.1.1 可编程中断控制器 61 6.1.2 初始化中断处理数据结构 61 6.1.3 中断处理 62 6.2 设备驱动程序 63 6.2.1 测试与中断 64 6.2.2 直接存储器访问(DMA) 65 6.2.3 存储器 66 6.2.4 设备驱动程序与内核的接口 66 6.2.5 硬盘 69 6.2.6 网络设备 74 第7章 文件系统 77 7.1 第二个扩展文件系统EXT2 78 7.1.1 EXT2系统的inode节点 79 7.1.2 EXT2系统的超级块 80 7.1.3 EXT2系统的组描述符 80 7.1.4 EXT2系统的目录 81 7.1.5 在EXT2文件系统中查找文件 81 7.1.6 在EXT2文件系统中改变文件 的大小 82 7.2 虚拟文件系统 83 7.2.1 VFS文件系统的超级块 84 7.2.2 VFS文件系统的inode节点 84 7.2.3 注册文件系统 85 7.2.4 装配文件系统 85 7.2.5 在虚拟文件系统中查找文件 87 7.2.6 卸载文件系统 87 7.2.7 VFS文件系统的inode缓存 87 7.2.8 目录缓存 88 7.3 缓冲区缓存 88 7.3.1 bdflush内核守护进程 90 7.3.2 update进程 90 7.4 /proc文件系统 91 7.5 特殊设备文件 91 第8章 网络 92 8.1 TCP/IP网络概述 92 8.2 Linux中的TCP/IP网络层次结构 95 8.3 BSD套接字接口 96 8.4 INET的套接字层 97 8.4.1 创建BSD套接字 98 8.4.2 为INET BSD Socket绑定地址 99 8.4.3 建立INET BSD Socket连接 99 8.4.4 INET BSD Socket侦听 100 8.4.5 接受连接请求 100 8.5 IP层 100 8.5.1 套接字缓冲区 100 8.5.2 接收IP报文 101 8.5.3 发送IP报文 102 8.5.4 数据分片 102 8.6 地址解析协议 103 8.7 IP路由 104 第9章 内核机制与模块 107 9.1 内核机制 107 9.1.1 Bottom Half控制 107 9.1.2 任务队列 108 9.1.3 定时器 109 9.1.4 等待队列 110 9.1.5 自旋锁 110 9.1.6 信号量 110 9.2 模块 111 9.2.1 模块载入 112 9.2.2 模块卸载 113 第10章 处理器 115 10.1 X86 115 10.2 ARM 115 10.3 Alpha AXP处理器 115 第11章 Linux内核源代码 117 11.1 怎样得到Linux内核源码 117 11.2 内核源码的编排 117 11.3 从何处看起 118 第12章 Linux数据结构 120 附录A 有用的Web和FTP站点 138 附录B 词汇表 139
本PDF电子书包含上下两册,共1576,带目录,高清非扫描版本。 作者: 毛德操 胡希明 丛书名: Linux内核源代码情景分析 出版社:浙江大学出版社 目录 第1章 预备知识 1.1 Linux内核简介. 1.2 Intel X86 CPU系列的寻址方式 1.3 i386的式内存管理机制 1.4 Linux内核源代码中的C语言代码 1.5 Linux内核源代码中的汇编语言代码 第2章 存储管理 2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理面的使用和周转 2.7 物理面的分配 2.8 面的定期换出 2.9 面的换入 2.10 内核缓冲区的管理 2.11 外部设备存储空间的地址映射 2.12 系统调用brk() 2.13 系统调用mmap() 第3章 中断、异常和系统调用 3.1 X86 CPU对中断的硬件支持 3.2 中断向量表IDT的初始化 3.3 中断请求队列的初始化 3.4 中断的响应和服务 3.5 软中断与Bottom Half 3.6 面异常的进入和返回 3.7 时钟中断 3.8 系统调用 3.9 系统调用号与跳转表 第4章 进程与进程调度 4.1 进程四要素 4.2 进程三部曲:创建、执行与消亡 4.3 系统调用fork()、vfork()与clone() 4.4 系统调用execve() 4.5 系统调用exit()与wait4() 4.6 进程的调度与切换 4.7 强制性调度 4.8 系统调用nanosleep()和pause() 4.9 内核中的互斥操作 第5章 文件系统 5.1 概述 5.2 从路径名到目标节点 5.3 访问权限与文件安全性 5.4 文件系统的安装和拆卸 5.5 文件的打开与关闭 5.6 文件的写与读 5.7 其他文件操作 5.8 特殊文件系统/proc 第6章 传统的Unix进程间通信 6.1 概述 6.2 管道和系统调用pipe() 6.3 命名管道 6.4 信号 6.5 系统调用ptrace()和进程跟踪 6.6 报文传递 6.7 共享内存 6.8 信号量 第7章基于socket的进程间通信 7.1系统调用socket() 7.2函数sys—socket()——创建插口 7.3函数sys—bind()——指定插口地址 7.4函数sys—listen()——设定server插口 7.5函数sys—accept()——接受连接请求 7.6函数sys—connect()——请求连接 7.7报文的接收与发送 7.8插口的关闭 7.9其他 第8章设备驱动 8.1概述 8.2系统调用mknod() 8.3可安装模块 8.4PCI总线 8.5块设备的驱动 8.6字符设备驱动概述 8.7终端设备与汉字信息处理 8.8控制台的驱动 8.9通用串行外部总线USB 8.10系统调用select()以及异步输入/输出 8.11设备文件系统devfs 第9章多处理器SMP系统结构 9.1概述 9.2SMP结构中的互斥问题 9.3高速缓存与内存的一致性 9.4SMP结构中的中断机制 9.5SMP结构中的进程调度 9.6SMP系统的引导 第10章系统引导和初始化 10.1系统引导过程概述 10.2系统初始化(第一阶段) 10.3系统初始化(第二阶段) 10.4系统初始化(第三阶段) 10.5系统的关闭和重引导

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

p0inter

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值