基本概念
何为并发呢?
并发:指的是多个执行单元同时、并行的被执行。在并行执行的过程中很容易产生竞态。比如对共享资源的访问(硬件资 源、软件中的全局变量、静态变量等)
临界区:被访问共享资源的代码区。
Linux内核中几种主要竞态的发生
1) 对称多处理器SMP的多个CPU
由于多CPU共享诸多资源,比如使用共同的系统总线、外设。
2) 单个CPU内进程与抢占
内核态执行时,当前进程有可能呗其他高优先级的进程打断,出现抢占。
3) 中断(软硬中断、Tasklet、底半部)和进程之间
中断可以打断正在执行的进程,如果中断处理程序也访问进程的资源,则会产生竞态。以及多个中断之间的并发导致的竞态
通常为避免竞态的产生,需要对共享资源进行互斥访问,即在一个被执行单元访问共享资源时,禁止其他的执行单元的访问。
Linux 设备驱动可采用中断屏蔽、原子操作、自旋锁、信号量等互斥方式。
互斥方式
中断屏蔽
对于单CPU来说,避免竞态的简单方法是在被执行单元进入临界区之前进行系统中断屏蔽。这样确保在执行的内核不会被打断。中断屏蔽使得并发和调度不在发生。
中断屏蔽使用的函数:
local_irq_disable() //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断
由于系统的诸多操作都依赖于中断,长时间关闭中断是不靠谱的。这样很多的请求无法响应,这要求临界区的代码执行要很快
中断屏蔽只能禁止和使能本地CPU的中断,解决不了多CPU的竞态。但是适合与自旋锁联合使用。
于是引申出local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前 CPU 的中断位信息,local_irq_restore(flags) 进行的是与 local_irq_save(flags)相反的操作。如果只是想禁止中断的底半部,应使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部应该调用 local_bh_enable()。
原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
原子操作分为两类:对位和整型变量进行原子操作。
(位和整型变量原子操作都依赖底层CPU 的原子操作来实现,因此所有这些函数都与 CPU 架构密切相关。)
整型原子操作:
1.设置原子变量的值
void atomic_set(atomic_t *v, int i); //设置原子变量的值为 i
atomic_t v = ATOMIC_INIT(0); //定义原子变量 v 并初始化为 0
2.获取原子变量的值
atomic_read(atomic_t *v); //返回原子变量的值
3.原子变量加/减
void atomic_add(int i, atomic_t *v); //原子变量增加 i
void atomic_sub(int i, atomic_t *v); //原子变量减少 i
4.原子变量自增/自减
void atomic_inc(atomic_t *v); //原子变量增加 1
void atomic_dec(atomic_t *v); //原子变量减少 1
5.操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为 0,为 0 则返回 true,否则返回 false。
位原子操作
1.设置位
void set_bit(nr, void *addr); // 操作设置 addr 地址的第 nr 位,所谓设置位即将位写为 1。
2.清除位
void clear_bit(nr, void *addr); // 操作清除 addr 地址的第 nr 位,所谓清除位即将位写为 0。
3.改变位
void change_bit(nr, void *addr); // 操作对 addr 地址的第 nr 位进行反置。
4.测试位
test_bit(nr, void *addr); // 操作返回 addr 地址的第 nr 位。
5.测试并操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
test_and_xxx_bit(nr, void *addr) // 操作等同于执行 test_bit (nr, void *addr)后再执行 xxx_bit(nr, void *addr)。
自旋锁
自旋锁(spin lock)是一种对临界资源进行互斥手访问的典型手段.为了获得一个自旋锁,在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。
1.定义自旋锁
spinlock_t spin;
2.初始化自旋锁
spin_lock_init(lock)
该宏用于动态初始化自旋锁 lock
3.获得自旋锁
spin_lock(lock)
该宏用于获得自旋锁 lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;
spin_trylock(lock)
该宏尝试获得自旋锁 lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转”;
4.释放自旋锁
spin_unlock(lock)
该宏释放自旋锁 lock,它与 spin_trylock 或 spin_lock 配对使用。
自旋锁一般这样被使用,如下所示:
//定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock) ; //获取自旋锁,保护临界区
...//临界区
spin_unlock (&lock) ; //解锁
信号量
信号量(semaphore)是用于保护临界区的一种常用方法。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁
不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
1.定义信号量
struct semaphore sem;
2.初始化信号量
void sema_init (struct semaphore *sem, int val);
初始化信号量,并设置信号量 sem 的值为 val。尽管信号量可以被初始化
为大于 1 的值从而成为一个计数信号量,但是它通常不被这样使用。
void init_MUTEX(struct semaphore *sem);
初始化一个用于互斥的信号量,它把信号量 sem 的值设置为 1,等同于
sema_init (struct semaphore *sem, 1)。
void init_MUTEX_LOCKED (struct semaphore *sem);
初始化一个信号量,但它把信号量 sem 的值设置为 0,等同于
sema_init (struct semaphore *sem, 0)。
此外,两个宏是定义并初始化信号量的“快捷方式”。
DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)
前者定义一个名为 name 的信号量并初始化为 1,
后者定义一个名为 name 的信号量并初始化为 0。
3.获得信号量
void down(struct semaphore * sem);
用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用。
int down_interruptible(struct semaphore * sem);
与 down()类似,不同之处为,因为 down()而进入睡眠状态的进程不能
被信号打断,而因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号
也会导致该函数返回,这时候函数的返回值非 0。
int down_trylock(struct semaphore * sem);
尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回 0,
否则,返回非 0 值。它不会导致调用者睡眠,可以在中断上下文使用。
在使用 down_interruptible()获取信号量时,对返回值一般会进行检查,如果非 0,
通常立即返回-ERESTARTSYS,如:
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
4.释放信号量
void up(struct semaphore * sem);
释放信号量 sem,唤醒等待者。
信号量一般这样被使用,如下所示:
//定义信号量
DECLARE_MUTEX(mount_sem);
down(&mount_sem);//获取信号量,保护临界区
...
critical section //临界区
...
up(&mount_sem);//释放信号量