正文:
- Linux内核的标准链表采用环形双向链表形式实现的。
- Linux内核是将链表节点塞入数据结构,而不是将数据结构塞入链表。
- 在C语言中,一个给定结构体中的变量偏移在编译时地址就被ABI固定下来了。
- kfifo对象维护了两个偏移量:入口偏移量,出口偏移量。入口偏移指下一次入队列时的位置,出口偏移指下次出队列时的位置。
- enqueue操作拷贝数据队列中的入口偏移位置。
- 映射,也称关联数组,其实是一个由唯一键组成的集合,而每个键必然关联一个特定的值。映射至少有三种操作:add,remove,lookup.
- 并非所有的映射都需要通过散列表实现,还可以通过自平衡二叉搜索树存储数据。
- 平衡二叉搜索树是一个所有叶子节点深度差不超过1的二叉搜索树!
正文:
- 临界区:访问和操作共享数据的代码段。
- 竞争条件:如果两个执行线程有可能处于同一个临界区中同时执行。
- 避免并发和防止竞争条件称同步。
- 线程持有锁,锁保护了数据结构。
- 锁是采用原子操作实现的,不存在竞争。
- 中断安全代码,SMP安全代码,抢占安全代码。
- 单核伪并发,多核真并发。
- 局部自动变量(还有动态分配的数据结构,其地址仅存放在堆栈中)不需要锁,因为它们独立存在于执行线程的栈中。如果数据只会被特定的进程访问,那么也不需要加锁(因为进程一次只在一个处理器上执行)。
- 给数据加锁而不是给代码。
- 死锁产生条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所有线程都在相互等待,但它们永远不会释放已经占有的资源。
- 一个执行线程也有可能会试图去获得一个自己已经持有的锁。
- 有些内核提供了递归锁来防止自死锁现象。
- 避免死锁的规则:按顺序加锁;防止发生饥饿;不要重复请求同一个锁;设计应力求简单。
- 只要嵌套地使用多个锁,就必须按照相同的顺序去获取它们。而释放锁就按照相反顺序.
- 锁的争用:指锁正在被占用时,有其他线程试图获得该锁。
- 加锁粒度用来描述加锁保护的数据规模。一个过粗的锁保护大块数据,一个过于精细的锁保护很小的一块数据。
- 当锁争用过于严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销。
- 内核提供了两组原子操作接口:一组针对整数进行操作,一组针对单独的位进行操作。(所有体系结构都实现了这两组接口,大多数体系结构还提供了支持原子操作的简单算数指令,有些为单步执行提供了锁内存总线的指令)
- 针对整数的原子操作只能对atomic_t类型的数据进行处理。使用atomic_t类型确保编译器不对相应的值进行访问优化。
- 原子操作通常是内联函数,往往是通过内嵌汇编指令来实现。
- 在编写代码时,尽可能使用原子操作,而不是更复杂的加锁机制。
- 自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用。所以自旋锁不应该被长时间持有(适合在短时间内进行轻量级加锁)。
- 自旋锁的实现与体系结构密切相关。代码往往通过汇编实现<asm/spinlock.h>,实际需要的接口定义在<linux/spinlock.h>。
- 自旋锁在linux内核中是不可递归的。自旋锁可以使用在中断处理程序中(不能用信号量,因为他们会导致睡眠)。
- 读—写自旋锁(共享/排斥锁):一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。自旋等待的写者必须等所有的读者释放锁之后才能获得锁。
- 如果加锁时间不长并且代码不会睡眠,利用自旋锁是最佳选择。
- 信号量是一种睡眠锁,如果有一个任务试图获得一个不可用的信号量时,信号量会将其推入一个等待队列,然后让它睡眠。信号量适用于锁会被长时间持有的情况,且线程只能在进程上下文中才能获取信号量锁(因为中断上下文中是不能进行调度的)。
- 信号量不同于自旋锁,它不会禁止内核抢占,所有持有信号量的代码可以被抢占。
- 二值信号量(同一时刻最多有一个线程持有),计数信号量(同一时刻可以有多个线程持有)。
- 信号量支持两个原子操作P()和V(),前者叫做测试操作,后者叫做增加操作。(后来的系统把两重操作分别叫做down(),up())
- 读—写信号量(互斥信号量),直对写者互斥。
- 互斥体(mutex):任何可以睡眠的强制锁。
- 任何时刻中只有一个任务可以持有mutex;给mutex上锁者必须负责给其解锁,即在同一上下文中上锁和解锁;不允许递归上锁和解锁;当持有一个mutex时,进程不可以退出;mutex不能在中断或者下半部中使用。
- 完成变量:如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。
- 大内核锁(BKL)是一个全局自旋锁。持有大内核锁的任务任然可以睡眠;BKL是一种递归锁,只使用在进程上下文中。
- 顺序锁(seq)提供了一种很简单的机制,用于读写共享数据。顺序锁的实现主要依靠一个序列计数器。
- 如果一个自旋锁被持有,内核便不能进行抢占。
- 指令重排序的发生是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序。
正文:
- 内核把物理页作为内存管理的基本单元。从虚拟内存的角度来看,页就是最小单位。
- page结构与物理页相关,而并非与虚拟页相关。
- 内核把页划分为不同的区,通过区对具有相似特性的页进行分组。Linux主要使用了四种区:ZONE_DMA,ZONE_DMA32(只能被32位设备访问),ZONE_NORMAL,ZONE_HIGHEM(这个区包含高端内存,期中的页并不能永久地映射到内核地址空间)。区的实际使用和分布是与体系结构相关的。Linux把系统的页划分为分区,形成不同的内存池,这样就可以根据用途进行分配了。
- 可以直接在内核中申请一整页或者多页内存,也可以用kmalloc()以字节为单位申请内存(kfree()释放内存)。
- gfp_mask标志:行为修饰符(表示内核应该如何分配所需的内存),区修饰符(表示从哪分配内存),类型(组合了行为修饰符和区修饰符)。
- vmalloc()函数分配的内存虚拟地址是连续的,而物理地址则无需连续,这也是用户空间分配函数的工作方式:malloc()返回的页在进程的虚拟地址空间内是连续的。而kmalloc()函数确保页在物理地址上是连续的(自然虚拟地址也是连续的),多数情况下,只有硬件设备需要得到物理地址连续的内存。
- vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。而且它获得的页必须一个个地进行映射(因为物理地址不连续),这就会导致比直接内存映射大的多的TLB抖动。所以vmalloc()一般只在要获得大块内存时才使用,例如当模块被动态插入到内核中时,就把模块装载到由vmalloc()分配的内存上。
- 空闲链表包含可供使用的,已经分配好的数据结构块,当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存。空闲链表相当于对象高速缓存,快速存储频繁使用的对象类型。
- slab分配器扮演了通用数据结构缓存层的角色。slab层把不同对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。每个对象类型对应一个高速缓存。
- 用户空间能够负担起非常大的栈,而且栈空间可以动态增长,相反,内核栈小且固定。当给每个进程分配一个固定大小的小栈后,不但可以减少内存的消耗,而且内核也无需负担太重的栈管理任务。每个进程的内核栈大小既依赖体系结构,也与编译时的选项有关。一般每个进程都有两页的内核栈。
- 永久映射和原子映射。
- 支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般来说,每个CPU的数据存放在一个数组中,数组中的每一项对应着系统上一个存在的处理器。
- 使用每个CPU数据减少了数据锁定,因为按照每个处理器访问每个CPU数据的逻辑,你可以不再需要任何锁;使用每个CPU数据可以大大减少缓存失效(失效发生在处理器试图使它们的缓存保持同步时),在使用时必须禁止内核抢占,否则可能发生欺骗行为。
正文:
- 异常和中断不同,异常产生必须与处理器时钟同步,也称为同步中断。
- 中断处理程序要负责通知硬件设备中断已被接收。
- kernel把中断分为上半部和下半部。
- irqf_disabled标志,意味着内核在处理中断处理程序本身期间要禁止所有其他中断。
- irqf_sample_random标志,表明这个设备产生的中断对内核熵池(负责提供从各种随机事件导出的真正的随机数)有贡献。
- irqf_timer标志,为系统定时器的中断处理准备的。
- irqf_shared标志,表明可以在多个中断处理程序间共享中断线。
- Linux的中断处理程序是无需重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有的处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。
- 内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取锁的同时也伴随着禁止本地中断,不过禁止中断则是防止来自其他中断处理程序的并发访问。
- 在发出中断的处理器上,它将禁止和激活中断的传递。
- in_interrupt()宏表示内核是否处于任意中断处理中(包括处于下班部处理程序中);in_irq()宏只表示内核是否处于中断处理程序中(不包含下半部处理)。
正文:
- 下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。
- 驱动程序开发者必须把中断上,下半部的处理工作分好。
- 下半部执行的关键在于当它们运行时可以响应所有中断。
- 软中断必须在编译期间就进行静态注册,而tasklet可以通过代码进行动态注册。
- 内核提供了三种不同形式的下半部实现机制:软中断,tasklet,工作队列。
- 可以用于将工作推后执行的机制是内核定时器。
- 一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是中断处理程序。其他软中断可以在其他处理器上同时执行。
- 一个注册的软中断必须在被标记后才会执行。通常中断处理程序在返回前标记它的软中断。
- 待处理的软中断会被检查和执行的地方:从一个硬件中断代码处返回时;在ksoftirqd内核线程中;在那些显式检查和执行待处理的软中断的代码中。
- 软中断保留给系统中对时间要求最严格以及最重要的下半部使用,比如网络,SCSI.此外,内核定时器,tasklet都是建立在软中断上的。
- 软中断处理程序执行时,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止,但其他处理器仍然可以执行别的软中断。
- 使用tasklet时,同一个处理程序的多个实例不能在多个处理器上同时运行。
- tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。
- 一个tasklet总在调度它的处理器上执行,这是希望能更好的利用处理器的高速缓存。
- 每个处理器都有一组辅助处理软中断的内核线程,当内核中出现大量软中断的时候,这些内核线程就会辅助处理他们。
- 内核不会立即处理重新触发的软中断,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。
- 工作队列交由一个内核线程在进程上下文执行,它允许重新调度和睡眠。
- 尽管工作队列处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。