一. 原子操作
原子操作,就是代码执行不会在执行完毕前被任何其他任务或事件打断. 原子操作需要硬件的支持,因此是架构相关的, 它们都使用内嵌汇编语言实现,因为C语言并不能实现这样的操作.
内核原子操作的理解:
a. 资源的使用情况计数, atomic_t的counter是int型的,可以记录资源的使用情况;
b. 资源使用的互斥, 当 atomic_t的counter的取值情况来判断资源的占用情况;
(1) 定义
include/linux/types.h :
typedef struct {
int counter;
} atomic_t;
具体的体系架构中都有自己的实现:
ARM平台 : arch/arm/include/asm/atomic.h
(2) 功能API
资源使用情况的counter的取值的计算和获取, 来获取资源占用(使用)情况, 从而决定一些控制逻辑.
ATOMIC_INIT(i); //在声明一个atomic_t变量时候,将他初始化为i
int atomic_read(atomic_t *v); //读取原子变量
void atomic_set(atomic *v,int i); //设置原子变量
void atomic_add(int i,atomic *v); //原子变量之+i
void atomic_inc(atomic *v); //原子变量自加操作
void atomic_dec(atomic *v); //原子变量自减操作
int atomic_sub_and_test(int i,atomic_t *v); //原子变量-i, 并判断是否为0
int atomic_add_negative(int i,atomic_t *v); //原子变量+i, 并判断是否为负
int atomic_add_return(int i,atomic_t *v); //原子变量+i, 并返回
int atomic_sub_return(int i,atomic_t *v); //原子变量-i, 并返回
int atomic_inc_return(atomic_t *v); //原子变量自加, 并返回
int atomic_dec_return(atomic_t * v); //原子变量自减, 并返回
(3) 使用场景
a. 驱动的安全性 :
一个驱动程序,有些时候, 不止一个进程在使用, 可以使用原子操作来使当前只要一个进程在使用, 对于字符设备, 其实只要在原来的字符设备驱动的 open 函数中增加了原子操作判断相关的代码,在关闭函数中增加释放原子操作的代码。 其余的代码基本不用改动,这样就可以保证这个驱动只能被一个进程使用, 保证了驱动的安全性。
原子操作相关接口不仅仅是可以放在 open 函数上,可以根据需要放在不同的地方, 比如放在 write 函数中, 在同一时刻只能被一个进程写操作, 读操作可以被多进程读, 因为读操作不会破坏数据, 而写操作会.
因此, 可以根据具体需要, 灵活使用各个 API.
(4) 使用边界
a. 是否会引起休眠?
执行过程中不能被打断, 如果执行时间长的话, 涉及进程调度的话, 应该会引起休眠.
引起休眠的判断方法:
睡眠 的本质是 该进程 处于一种特殊的不可执行状态 (TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE),
这样, 调度程序就不会去执行他,而实现睡眠。
(1) 从代码上分析,如果函数调用了其他函数,追朔回去只要有对scheduled的调用就可能引起睡眠;
(2) 从操作系统原理解释就是,当你需要获取资源(硬件资源,内存资源,一个数据结构等),而资源不可得,此时不应该忙等, 本进程就应该睡眠,直到资源可用。因此, 可以试着分析是否有这种‘请求语境’出现,比如请求内存,而内存可能分配给了其他进程,要分配内存就意味着可能从其他进程把內存页调来给你用,请求io,io设备有可能不可得,这些情况下都会带来休眠。
待续...
(5) 突破边界(是否有)
a. 是否有规避边界的方法?
待续...
二. spin lock
自旋锁已经被别的执行单元保持时,调用者就一直循环等待是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间,这种特性下,不适用于长时间等待锁的场景),所以自旋锁不应该被持有时间过长。
自旋锁内睡眠禁止睡眠问题:如果自旋锁锁住以后进入睡眠,而此时又不能进行处理器抢占(锁住会disable prempt),其他进程无法获得cpu,这样也不能唤醒睡眠的自旋锁,因此不相应任何操作。
自旋锁广泛用于内核:
a. 效率高;
自旋锁是一种轻量级的互斥锁,不会涉及进程切换, 可以更高效的对互斥资源进行保护。所以自旋锁的效率就远高于互斥锁;
b. 可使用的场景丰富;
不同场景,有不同的api.
(1) 使用场景
a. 适合于保持时间非常短的情况,可以在任何上下文使用;
b. 在任何时刻最多只能有一个执行单元获得锁;
c. 各个接口使用, 不同的上下文, 有不同的函数接口:
1. spin_lock_bh / spin_unlock_bh
在得到自旋锁的同时失效/使能本地软中断(tasklet和timer是用软中断实现).
可能发生软中断的场景,使用最恰当, 软中断的场景最高效;(使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断)
同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此.
2. spin_lock_irq / spin_unlock_irq
禁止内核抢占, 同时禁止本地中断;
如果确定在获取锁之前本地中断是开启的; 解锁的时候直接将本地中断启用就可以, 不保存中断状态;
3. spin_lock / spin_unlock
禁止内核抢占;
4. spin_lock_irqsave/spin_unlock_irqrestore
禁止内核抢占, 同时禁止本地中断, 同时保存CPU所有的中断状态, 解锁时通过 spin_unlock_irqrestore完成释放锁、恢复本地中断到之前的状态等工作;
a. 保存本地中断状态;
b. 关闭本地中断;
c. 获取自旋锁;
5. spin_is_locked / spin_can_locked
判断是否可获取锁;
每种场景下, 都有适合的接口使用, 可根据场景,选择合适的接口, 也有对应的try接口.
(2) 边界
a. 中断上下文, 必须使用自旋锁;
中断上下文不能休眠, 因此被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断)时, 就必须使用自旋锁.
b. 自旋锁保持期间是抢占失效的;
自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作; (信号量和读写信号量保持期间是可以被抢占的)
(3) 比较
a. 锁保持时间;
自旋锁适合于保持时间比较短的情况;
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠;
b. 是否可抢占;
自旋锁保持期间抢占失效;
信号量和读写信号量保持期间可以被抢占;
c. 是否引起休眠;
自旋锁, 调用进程不会睡眠;
信号量和读写信号量, 会导致调用者睡眠;
三. 互斥锁
互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子 操作基础上实现的,获取互斥锁的过程中,是在自旋锁的保护下进行, 但这对于内核用户是不可见的。 互斥锁比当前的内核信号量选项更快,并且更加紧凑。
对它的访问必须遵循一些规则:
a. 同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁;
b. 互斥锁不能进行递归锁定或解锁;
c. 一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化;
d. 一个任务在持有互斥锁的时候是不能结束;
e. 互斥锁所使用的内存区域是不能被释放;
f. 使用中的互斥锁是不能被重新初始化;
g. 并且互斥锁不能用于中断上下文。
a, d, e这样的特性, 决定了他适用于长时间持锁的场景.
(1) 定义
include/linux/mutex.h
kernel/locking/mutex.c
1. mutex_init/ mutex_lock / mutex_unlock;
初始化/获取/释放 锁
2. mutex_trylock();
试图获取互斥锁,如果成功获取则返回1,否则返回0,不等待;
3. mutex_lock_interruptible();
在获得了互斥锁或进入睡眠直到获得互斥锁之后会返回0。如果在等待获取锁的时候进入睡眠状态收到一个信号(被信号打断睡眠),则返回_EINIR;
四. 信号量
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
(1) 定义
include/linux/semaphore.h
/* Please don’t access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
kernel/locking/semaphore.c
信号量的实现和spin lock的实现, 底层的接口是一样的.