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线程来完成所有的回写动作以确保最大限度的使用多个磁盘.
『』表示着重注意的部分
<> 表示有疑虑的部分
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线程来完成所有的回写动作以确保最大限度的使用多个磁盘.