以前看了书,总是疏于记笔记,若干天后就忘差不不了。为了避免再犯以前的错误,今天把看完了就马上梳理一下放到这儿。
linux内核开发中,多线程并发的管理毫无疑问是重中之重;不可避免地,并发的相关缺陷也是最容易制造的,也是最难找的。即使是Linux内核专家也会偶尔制造并发相关的缺陷。为此,我们有必要好好学习和理解内核对并发的管理。
并发和竞态:
并发是多个执行单元被同时执行。竞态通常是作为对资源的共享访问而产生,竞态会导致对共享数据的非控制访问。并发是因,竞态是果。在现在Linux系统中存在大量的并发来源,因此会导致可能的竞态。多个正在运行的用户态程序并发访问我们的代码,SMP在多个处理上同时执行我们的代码,内核代码是可抢占的,中断处理的异步执行,内核提供的软中断机制,这些都可能导致竞态的产生。
为了尽可能避免竞态的发生,需要记住几个原则:
一、只要可能,就应该尽可能避免资源的共享(避开原则)。如果没有并发的访问,也就不会有竞态的产生。比如,尽可能避免使用全局变量。
二、资源共享不可避免情况下,有个资源共享的硬规则(加锁保护原则):在单个线程之外共享硬件和软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显式地管理对资源的访问。对共享资源管理的常见技术称为“锁定”或者“互斥”——确保一次只有一个执行线程可操作共享资源。比如,办公室中两个同事都要使用网络打印机打印材料,但彼此都不知道对方也要用打印机,不做任何资源保护势必就会出问题。因此,需要使用互斥锁来保护对打印机的共享访问。
三、共享对象的使用原则(引用计数保护原则):当内核创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。可以这样理解:在对象上不能正确工作时,不能将其对内核可用。对内核可用之后,对其必须得到跟踪。对共享对象的跟踪常用的技术是引用计数。
刚开始写内核程序时,公司大牛就对我说,写好内核程序的关键是使用好锁和引用计数。读到这里,顿时有种大彻大悟。
信号量和互斥体
先解释几个概念
临界区:在任意给定的时刻,代码只能被一个线程执行。
“进入休眠”:当一个Linix进程到达某个时间点,此时它不能进行任何处理时,它将进入休眠(或“阻塞”)状态,这将把处理器让给其他执行线程直到将来它能继续完成自己的处理为止。需要注意:等待获取信号量时会导致线程”进入睡眠“;在拥有信号量时,线程也允许睡眠。
正确使用锁定机制的关键是,明确指定需要保护的资源,并确保每一个对这些资源的访问都使用正确的锁定。
信号量和互斥体,读取者/写入者信号量几乎无人不晓,所以也就不多写了。
completion
自旋锁
自旋锁API
要使用自旋锁原语,需要包含头文件<linux/spinlock.h>。spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t *lock); /* 运行时初始化spinlock*/
/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t *lock); /* 获得spinlock*/
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t *lock); /* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t *lock) /* 获得spinlock,禁止软件中断,保持硬件中断打开*/
/* 以下是对应的锁释放函数*/
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
/* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);