Linux Kernel Notes

来源:互联网 发布:sql 表别名 编辑:程序博客网 时间:2024/06/05 20:49
LDK NOTES

『』表示着重注意的部分
 <> 表示有疑虑的部分

2.1, cpu运行在三种状态:
            内核空间的进程上下文
            内核空间的中断上下文
            用户空间的进程
2.2, 微内核就是服务与内核分开,各自运行在不同的内存空间。
       单内核就是把内核的所有部分放在一个大文件里面,单一
       内存空间运行。
       Linux是单内核
       WinNT是变种的微内核,因为它的很多服务,比方说图形都
       在内核地址里面,为了保证快速。
2.3, 2.6.18  主版本号.从版本号.修订版本号
     从版本号是奇数说明是开发版本,不稳定版本
2.4, 内核代码树的根目录下会有一个System.map文件, 是一份
         符号对照表, 用以将内核符号与其起始地址对应起来. 调
         试的时候,如果把内存地址翻译成为可以理解的变量名或
         者函数是很有用的.
2.5, 内核编程不能使用libc库
                1)先有蛋, 先有鸡的悖论 —— 库函数一般要调用系统调用,
                所以系统调用总不能再用库函数写吧.
                2)另外, 库函数无论是二进制大小和效率都不符合要求,
                所以内核自己实现了库函数的一个子集.
                3)同时, 内核源代码不能包含代码树之外的其他头文件
2.6, 内核使用GCC编译, 采用的是GNU C语言规范:
                GNU C遵守ISO C99 以及 GNU C扩展特性
                GNU C扩展特性的比较有趣的部分:
                        1)内联函数(inline),直接在被调用的位置展开(与宏
                        原文替换有区别). 这减少了函数调用的开销,以及
                        使得编译器编译时检查以及编译时优化成为可能. 但
                        是因为这增加了二进制长度, 所以最好是对时间要求
                        比较高以及短的代码进行inline声明. 内联函数一般
                        在.h中『定义』成为static inline func(){....}
                        2)有与体系相关的内联汇编.
                        3)分支声明, likely unlikely GCC编译器内建指令
                        优化.
2.7, 内核内部没有内存保护机制, 内核发生内存错误的时候会导致
         oops. 内核的内存不分页<是不是不换页的意思>, 所以要注意
         内存的使用,每用掉一个字节,物理内存就减少一个字节.
2.8, 不要轻易在内核中使用浮点数.
2.9, 容积小而且固定的栈.
                历史上来说栈是二页,也就是说32位机是8k, 64位是16k.
2.10,同步以及并发
              同步是由于进程调度, 内核抢占, 中断到来.
              并发是由于多核.
2.11,注意可移植性.

3.1    Linux进程是很轻量级的, 因为其采用copy on write(cob). 内
        核不太区分进程和线程. 只不过线程是共享父进程(线程)的vma,
        fs等等,即一些资源. 所以fork,vfork,或者创建线程, 只不过
        是调用clone的时候传的flag参数不同而已.  
        要执行另外一个程序一般有2个步骤: 1)fork(非常轻量)  2)exec
        vfork与fork不同的地方是, vfork以后使用的vma与父进程是同一
        个, exec以后再重新布置. 并且vfork的父进程等待子进程执行结
        束后再进行调度.
        进程的状态 runing, interruptible(uninterruptible), zombie

4.1
        
                                                
        
runqueue, 可以被调度running, 分享时间片的结构
里面有 active以及expired优先级数组
active是所有当前时间片没有耗尽,按照优先级排列的task
active整个数组所有的task时间片耗尽的时候,avtive与expired互换
重新进行时间片的消耗        在schedule调度算法中的

进程阻塞睡眠的概念:
<wait_queue 是否就是 interrupt 以及 uninterupt task的queue>
不是的,是自己初始化的wait_queue_head_t结构

这个结构最终由条件满足以后另一个进程调用wake_up唤醒, 会唤醒挂在上面的wait_queue_t, 并且调到它们的func,这个func的default
值就是try_to_wake_up, 将current加入到可执行队列rq中,并进行重新调度

    
中断上下文 没有进程的概念,所以无法睡眠,
因为他一旦睡眠, 又怎样把他重新调度起来呢
所以中断上下文中不能使用会睡眠的函数


不管是禁止中断还是禁止内核抢占,都没有控制多处理器并发访问的能力
锁机制控制多处理器并发访问; 禁止中断防止同一处理器上的其他中断处理程序



local_irq_save(flags)
local_irq_restore(flags)
这两个方法只能在一个函数局部被调用
而不能将flags传递给其他函数
这是因为,flags是与当前栈帧结合的,传递给别的函数的时候就不准了

内核把处理中断的工作分为两半:
中断处理程序-上半部: 一般做中断的确认,从硬件拷贝数据
                                         中断处理程序会异步执行,并且在『最好的情况下』它也会
                                         锁定当前的中断线
request_irq(irq, irqreturn_t (*handler)(int, void*, struct pt_regs *), irqflags, devname, *dev_id)
irqflags:
    SA_INTERRUPT: fast interrupt handler, 禁止所有中断; 默认不是这个标志, 默认是除了正运行的中断处理程序对应的那条中断线被屏蔽外, 其他所有中断都是激活的.
                                                        时钟中断使用这个标志
    SA_SHIRQ: 表明可以在多个中断处理程序之间共享中断线; 在同一个给定线上注册的每个处理程序必须指定这个标志; 否则在每条线上只能有一个处理程序.
dev_id:
    当共享中断线(即一个中断号)的设备需要删除中断处理程序的时候, 就需要这个号来唯一标识.
free_irq(irq, *dev_id)
中断处理程序:
    static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs)
Linux中的中断处理程序是无需重入的, 当一个给定的中断处理程序正在执行时, 相应的中断线在『所有处理器』上都会被屏蔽掉, 以防止在同一中断线上接收另一个新的中断.
在不使用SA_INTERRUP的情况下(default), 所有其他的中断都是打开的, 所以这些不同中断线上的其他中断都能被处理, 但当前中断线总是被禁止。
看起来是防止嵌套.  (『所有处理器』的原因是, 1.SMP用一条总线和中断控制器? 2.因为中断发生后在不同的CPU上也是使用同样的中断处理程序,这样就是重入了, 这是不行的)

do_IRQ 而这个函数大体上在local_irq_disable()情况下执行的  本地中断disable, 但是handle_IRQ_event调用中断处理程序的时候,是enable的, 这个时候其他中断线上的中断
       过来, 会打断当前的中断处理程序执行, 进行其他中断处理程序的执行. 只是当前中断线上的中断不会再次出现.
       调用 mask_and_ack_8259A()  这条中断线上的中断被MASK了,不再响应
       然后调用 handle_IRQ_event()  local_irq_enable()了,允许当前处理器的其他中断线上的中断被响应
        
       spin_lock_irq也只是禁止本地中断 local_irq_disable(), 这是因为持有锁的时候想占着CPU时间片, 不想被中断换出, 因为被中断换出以后, 中断可能争用这个锁, 导致死锁.
       CPU的时间片是针对单个CPU, local cpu来说的.
        
        SA_INTERRUP并不是要避免race condition, 而是确保实时性,不被其他中断打断, 如rtc, 因为default的中断本身不会有两个同样的中断处理程序嵌套
        do_IRQ 使用 local_irq_diable()就可以实现 一个中断处理程序调用时不被其他中断打断, 因为CPU的时间片是对一个核来说的, 一个核是一个计算单元 .
        原因是 1.当前中断线中断不会再来   2.在本地处理当前中断不会被其他中断打扰换出   3.其他CPU也可以响应其他的中断, 但是不会与你的中断处理程序处理的变量,数据完全不同
               4.如果有数据的race condition, 还是得使用锁来处理3的情况, 并且在考虑到2的时候,  要使用spin_lock_irq, 禁用中断. 不然2就死锁了.

               5.锁的机制无非是, 轮询一块内存, 不同核的CPU看到的是同一个, 一旦有改变就拿下锁.


    其实有spin_lock的时候进程上下文不允许preempt内核抢占, 也是一种纵向, 类似于2的避免死锁的方式


内核接收一个中断后, 它将依次调用在该中断线上注册的每一个『共享』(SA_SHIRQ)处理程序
 
禁止中断,可以确保某个中断处理程序不会抢占当前的代码,还可以禁止内核抢占.但是没有提供任何保护机制来防止来自其他处理器的并发访问.
解决不同处理器的并发访问需要使用锁的机制.
禁止中断是防止来自其他中断处理程序的并发访问.  


下半部-bottom half

原则是:
时间非常敏感的, 将其放在中断处理程序中执行
硬件相关的, 将其放在中断处理程序中执行
保证不被其他中断(特别是相同的中断)打断, 将其放在中断处理程序中执行
其他所有的任务,考虑放置在下半部执行

中断处理程序在运行时,当前中断线在所有处理器上都会被屏蔽
更有甚者,当中断处理程序是SA_INTERRUPT类型,它执行的时候
会禁止所有本地中断(而且把本地中断先全局地屏蔽掉)。

处理下半部的时候, 所有中断是开放的, 允许响应所有的中断

下半部环境: bottom half
BH, task queue, softirqs and tasklet
这里的softirq和int 0x80陷入系统调用不一样的概念
由于历史原因,其中一些借口被废弃了
2.6内核提供了三种不同形式的下半部实现:软中断, tasklet和工作队列
还有一个关系,tasklet通过软中断实现

软中断由编译期间静态分配,不像tasklet那样动态注册或去除
一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。
不过,其他的软中断——甚至是相同类型的软中断——可以在其他处理器上同时执行。
触发软中断(raising the softirq),中断处理程序会在返回前标记它的软中断,使其在
稍后被执行。于是,在合适的时候,该软中断就会运行。在下列地方,待处理的软中断会
被检查和执行:
    1)从一个硬件中断代码处返回时
    2)在ksoftirqd内核线程中
    3)在那些显式检查和执行待处理的软中断的代码中,如网络子系统

不管是用什么办法唤起,软中断都要在do_softirq()中执行。
softirq_pending()返回软中断标记的位图
do_softirq 遍历整个位图标记,把标记的软中断的action执行掉。

struct softirq_action {
    void (*action) (struct softirq_action *);
    void *data;
};
static struct softirq_action softirq_vec[32] 软中断数组

软中断保留给系统中对时间要求最严格的以及最重要的下半部使用。目前,
只有两个子系统——网络和SCSI——直接使用软中断。

在中断处理程序中触发软中断是最常见的形式。这种情况下,中断处理程序
执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行
完中断处理程序以后,马上就会调用do_softirq()函数。并且在中断处理程
序中,中断是屏蔽的,所以用 raise_softirq_irqoff()非常符合要求。


『同一时间里,相同类型的tasklet只能有一个执行(其他不同类型的tasklet可以同时进行, 在不同的CPU上),
由TASKLET_STATE_RUN来控制的。』  很像真正的中断, 不可重入嵌套
这就是同一个tasklet不需要锁保护机制
而不同的tasklet之间有数据共享的话,需要加锁

而相同的softirq是可以在多处理上同时运行.  这点是它和中断处理程序以及tasklet的最大区别

因为传统软中断没有这个STATE_RUN字段,所以同一种软中断可以同时在不同的CPU上运行。
软中断(tasklet)无法被自己抢占,只能被中断处理程序抢占。

因为是靠软中断实现,所以tasklet不能睡眠。这意味着你不能在tasklet中使用信号量或者
其他阻塞式的函数。(因为处于中断上下文中)

一个tasklet总在调度它的处理器上执行——这是希望能更好地利用处理器的高速缓存。


softirq 全局有32个
其中两个是留给tasklet的  HI_SOFTIRQ以及TASKLET_SOFTIRQ
单处理器数据结构:tasklet_vec 以及 tasklet_hi_vec, 它是tasklet的链表.
tasklet_schedule()以及tasklet_hi_schedule()接收一个tasklet的参数,然
后将其挂到schedule执行的cpu的tasklet_vec链表上, 调度就是软中断所谓的挂起.
这个时候将 HI_SOFTIRQ或者TASKLET_SOFTIRQ 软中断类型触发.

在软中断do_softirq(),  根据HI_SOFTIRQ或者TASKLET_SOFTIRQ这两种不同的软中
断类型,调用相应的action ---- tasklet_action(), tasklet_hi_action()

tasklet_action() 会将挂在当前cpu上的 tasklet_vec 上的所有tasklet,执行其
注册的handler()函数



工作队列是用内核线程实现的下半部
是唯一能在进程上下文运行的下半部实现的机制,也只有它能够睡眠。

            
虽然工作队列的操作处理函数运行在进程上下文,但是它不能访问用户空间。因为
内核线程在用户空间<没有相关的用户内存映射>。
通常在系统调用时候,内核会代表用户空间的进程运行,此时它才能访问用户空间,
也只有『此时它才会映射用户空间的内存。』


工作队列是用工作线程workqueue_struct实现的
struct workqueue_struct
{
        struct cpu_workqueue_struct cpu_wq[NR_CPUS];
        const char* name;
        struct list_head list;
}                                
表示一类工作者线程.  一类工作者线程在每个CPU上都有单独的该类的工作者线程 cpu_workqueue_struct
cpu_wq中的worklist就是实际的工作链表; 工作是由 work_struct定义的。

如内核默认的工作队列由event工作线程类实现
event/0 event/1 是实际在各个cpu上的工作线程

软中断并发性最好,其次是tasklet(因为同一种tasklet无法并发)
工作队列可以睡眠,运行于进程上下文

在下半部之间加锁:
        软中断的所有共享数据必须加锁
        进程上下文与下半部共享数据时候,需要禁止下半部并得到锁 (而不需要倒过来,因为进程上下文运行时,
                                                                                                                        进程必定睡眠; 并且下半部是会抢占进程运行的)
        中断上下文与一个下半部共享数据,需要禁止中断并得到锁   (而不需要倒过来,因为中断抢占下半部)
        工作队列必须使用锁机制 (因为进程上下文会被抢占并且smp)
        
所以概念就是, 上层和底层共享数据时候, 一般上层需要禁止下层的并发, 并且取得锁


critical region 是访问和操作共享数据的代码段
必须做到原子性, 原子的意思就是就像是 执行一条不可分割的指令

如果两个执行线程(包括中断处理程序,内核线程等)同时访问临界区,就产生 race condition
为了避免race condition, 就要使用同步 -- synchronization

锁的争用(lock contention),简称争用。
争用的意思是多处个处理器都在等待这个锁
在设计锁的阶段就应该考虑保证良好的扩展性:
        即使在小型机器上,如果对重要资源锁的太粗,也很容易造成行系统性能瓶颈
        而锁争用不明显时候,加锁过细会加大系统开销,带来浪费
这两种情况都会造成系统性能下降。
锁加得过粗或者过细,差别往往在一线之间。『精髓在于力求简单』
可扩展性的意思是 扩展硬件资源,如大型SMP环境引入。

加锁的时候要考虑到:临界访问资源; 满足不死锁; 可扩展(加锁粗细); 简洁.

atomic_t
原子性和顺序性是不同的概念
内核提供的原子操作包括:整数、位的原子操作
不要自加锁从而导致锁死

中断可以抢占一切上下文,所以有必要禁止中断
下半部不会被换出,只会被中断抢占
进程上下文会被中断抢占,进而下半部,执行过程中会被换出

对于单cpu来说,下半部只需要关中断就行了,而不需要加锁
但是对于SMP来说,由于下半部会并发到多个CPU,所以需要加锁


我个人感觉原子性和互斥性又有区别:
原子性保证指令执行期间不被打断
顺序性是同步,顺序执行的概念
互斥性感觉是单例的一种概念,TR28691


读写锁 照顾读比较多一点, 大量的读操作肯定会使挂起的写者处于饥饿状态


锁和信号量的选择:
若是占用时间不长,使用锁。
若是占用时间很长,或者代码有可能睡眠,那么使用信号量。


其实信号量又被称为睡眠锁
而且通常在进程上下文中使用,因为中断上下文不能睡眠

信号量分为互斥信号量和couting semaphore
而内核基本都使用互斥信号量
所有读-写信号量都是互斥信号量

<当使用自旋锁的时候,内核是关调度的,即不能被抢占>,但是可以被中断和下半部抢占
schedule的时候会查看当前线程的锁持有数量,有锁即不会被换出,所以单处理器的时候,
自旋锁在线程之间是不起实际作用的。


锁不仅是smp_safe 还是 preempt_safe的

若是单一处理器上是独立变量,并且是进程上下文的互斥
不需要使用锁,只要禁止内核抢占就行了 -- preempt_disable()

屏障
mb() rmb() wmb() 是用来防止编译器以及处理器 读/写顺序优化的


定时器和时间管理
周期性事件与推迟执行事件
系统定时器(定时器中断) 以及 动态定时器

两次时钟中断的间隔叫做tick,节拍
它的倒数就是tick rate   HZ    一秒钟有多少节拍
全局变量jiffies用来记录自系统启动以来产生的节拍的总数
jiffies在32位的机子中会回绕,要注意

两种硬件时钟
体系结构提供两种设备进行计时 —— RTC 与 系统定时器
RTC电池供电,启动时提供xtime(墙上时间)
PIT 可编程中断时间  一种硬件


内核处理时间的机制
时钟中断处理程序分为两部分:体系相关, 体系无关(do_timer)
定时器(动态定时器,内核定时器)是作为软中断下半部执行的。
struct timer_list my_timer;
init_timer(&my_timer);

my_timer.expires = jiffies + delay;
my_timer.data = 0;
my_timer.function = my_function;            //超时了会被回调

add_timer(&my_timer);

del_timer_sync()

定时器是通过raise_softirq(TIMER_SOFTIRQ); 使得下半部处理程序在下半部处理的

使程序延时的方法有  忙等待, 短延迟可以使用udelay什么的
                                         还有一种是睡眠  schedule_timeout()  不能在中断上下文中使用

有lock的线程<不能睡眠>  ?   不能睡眠不代表着不能换出



linux内存管理
内核用page结构来管理系统中所有的页(页的存在是由MMU决定的), 因为内核需要知道一个页是否空闲.
如果页已被分配, 内核还需要知道谁拥有这个页. 拥有者可能是用户空间进程、动态分配的的内核数据、
静态内核代码或页高速缓存等等.


kfree(NULL)是安全的
其实kmalloc也是建立在slab机制之上,使用通用的高速缓存而已
一个结构有自己的高速缓存(kmem_cache_s结构表示), 它包括很多slab(通常是一个页), slab内有很
多预先分配好的该结构其实高速缓存也是通过__get_free_pages()这种低级的分配函数获得的内存页。

__get_free_pages()  alloc_pages() 这种属于低级的最原生的分配内存方法,去要一个页page
kmalloc是属于比较高级的内存分配调用, 它使用的slab机制内部也使用低级页分配(kmem_getpages).

kmem_cache_create



内核有一种数据叫作CPU数据,即每个CPU有一个自己的变量
因为这种编程规范上的规定导致了不会被多个CPU并发执行, 所以只要使用一定的函数
禁止内核抢占就可以省去锁开销。  并且, CPU数据cacheline上对齐且不同行, 使得刷新
cache的动作大大降低, 从而极大的提高了CPU效率.  (持续不断的缓存失效称为缓存抖动cache)(内存一致性)

若要使用连续的物理页,使用kmalloc或者低级页分配函数
传递GFP_ATOMIC进行不睡眠的高优先级分配
传递GFP_KERNEL可以睡眠, 如不持有锁的进程上下文

如果你想从高端内存进行分配, 使用alloc_pages()函数, 只能使用这种低级页分配函数. 因为高端内存并不一定
映射到逻辑地址, 访问它就要通过struct page结构.

若是不需要连续的页,那么使用vmalloc就可以了

对象告诉缓存slab(空闲链表)极大提高频繁操作对象内存的效率.







虚拟文件系统(VFS):

超级块对象(文件系统控制块)
索引节点对象
目录项对象
文件对象:文件对象是已打开的文件在内存中的表示

与进程相关的文件系统的数据结构
file_struct
fs_struct
namespace
这些数据结构都是通过进程描述符链接起来的.
对多数进程来说,它们的描述符都指向唯一的files_struct和fs_struct结构体.
但是,对于那些使用克隆标志CLONE_FILES或CLONE_FS创建的进程, 会共享这两个结构体.



块I/O层
系统中能够『随机』(不需要按顺序)访问固定大小数据片(chunk)的设备被称为块设备,
这些数据片就称作块.最常见的块设备是硬盘.
另一种基本的设备类型是字符设备.字符设备按照字符流的方式被有序访问,像串口和键盘
都属于字符设备.

如果一个硬件设备是以字符流的方式被访问的话, 那就应该将它归于字符设备; 反过来, 如果
一个设备是随机(无序的)访问的, 那么它就属于块设备.

块设备中最小的可寻址单元是扇区, 一般是2的整数倍, 最常见的512byte.
扇区是物理寻址单位
块是最小逻辑可寻址单元, 块是文件系统的一种抽象
块不能比扇区还小, 只能数倍于扇区大小. 并且要小于一个页的长度, 所以通常块长度是512byte,1k,4k

扇区--硬扇区, 设备块
块  --文件块, I/O块

一般来讲"get"是增加引用计数
                "put"是减少引用计数
                
块存储在一个缓冲区中 有一个缓冲区头描述它buffer_head                
buffer_head 目的在于描述磁盘块和物理内存缓冲区(特定页面上的字节序列)之间的映射关系.
buffer_head中的I/O操作单元已经过时

bio结构体
使用bio结构体的目的主要是代表正在现场执行的I/O操作.
见图P192 以及下面的解释, 很牛叉的, 包括那个向量表示
总而言之,每一个块I/O请求都通过一个bio结构体表示.

块设备将它们挂起的块I/O请求保存在请求队列中, 该队列由request_queue结构体表示
通过内核中像文件系统这样高层的代码将请求加入到队列中.
请求队列只要不为空, 队列对应的块设备驱动程序就会从队列头提取请求,然后将其送入对应的块设备上去。
请求队列表中的每一项都是一个单独的请求,由request结构体表示. 由于一个请求可能要操作多个连续的
磁盘块,所以每个请求可以由多个bio结构体组成。

I/O调度程序
如果简单地以内核产生请求的次序直接将请求发向块设备的话,性能肯定让人难以接受,磁盘寻址是整个计算机
中最慢的操作之一。所以为了优化寻址操作,内核既不会简单的按请求接收次序,也不会立即将其提交给磁盘。
相反,它会在提交前,先执行名为合并与排序的预操作,这种预操作可以极大地提高系统的整体性能。在内核中
负责提交I/O请求的子系统称为I/O调度程序。
I/O调度程序的工作是管理块设备的请求队列。通过合并和排序,使整个请求队列按扇区增长方向有序排列,被
称作『电梯调度』

Linus Elevator, Deadline I/O Sceduler(3个队列——2个有超时时间的FIFO队列以及一个排序队列,主要保证读),
Anticipatory I/O Scheduler, Complete Fair Queuing(CFQ), Noop I/O Sceduler (给非磁盘这种物理结构的blockI/O用,如flash)


进程地址空间
翻译后缓冲器 Translation Lookaside Buffer, TLB  是一个将虚拟地址映射到物理地址的硬件缓存, 命中的话
就不需要利用pmd去读一次实际的页式管理的物理内存.
pmap pid
内存描述符 mm_struct
内核线程没有mm_struct,因为它们不需要用户虚拟地址空间,它会使用前一个进程的mm_struct,即只适用系统部分的,用户部分不使用
系统堆栈,代码段,数据段等等这些信息全部存于mm_struct中.
mm_struct中有mmap作为vma的链表
                     有mm_rb作为vma的快速查找红黑树
vma  vm_area_struct 代表一段段state相同的内存区域
所有进程的mm_struct都通过mmlist域连接为一个双向链表,该链表的首元素是init_mm内存描述符,代表init进程的地址空间.                    

BSS block started by symbol, 存放未赋值的全局变量
创建线程时候 指定CLONE_VM标志, 所以tsk->mm = current->mm 即线程间共享内存空间.



页高速缓存和页回写
页高速缓存(page cache)是LINUX内核实现的一种主要磁盘缓存.它主要用来减少对磁盘的I/O操作
具体的讲,是通过把磁盘的数据缓冲到物理内存上,把对磁盘的访问变为对内存的访问.

它可以用在虚拟内存换页上, 读/写文件的磁盘操作等等.
需要搞明白的是 bio是实际写到block I/O的驱动接口, 它必然引起磁盘操作, 只是可能它使这个操作更合理
页高速缓存页I/O是机制, 是一种尽量少的引起磁盘操作的内存页管理机制. 最终写出也是通过bio接口写的.
缓冲区buffer是对block磁盘块来说的,一般小于等于页大小, 并且磁盘块也寄生于page之中.
cache是针对一整个页进行管理

buffer缓冲区          指的是bio,块设备的概念
cache 页高速缓存    指的是页I/O机制
其实缓冲区也受益于页高速缓存机制.     以前版本的内核有缓冲区高速缓存机制呢.

linux使用address_space组织描述page cache中的页面.
radix search tree用于在address_space中快速查找管理的页面, 它取代了以前的全局的页散列表.

page cache使用SavePageDirty(page)使页的某个标志变脏, 在以后的某个时机, 脏页会被写出.
result of page cache 就是页的写操作会被延迟. 有两种情况下,脏也会被写回磁盘.
    1)系统空闲内存低于阈值, 要写出到磁盘, 释放内存空间    dirty_background_ratio
    2)脏页在内存中的驻留时间超过阈值, 写回磁盘                    dirty_expire_centisecs/100  seconds
/proc/sys/vm

多个动态调整数量的pdflush线程来完成所有的回写动作以确保最大限度的使用多个磁盘.