Linux并发是指多个执行单元同时、并行被执行,竞态是指多个执行线程访问同一个临界资源时导致的竞态,互斥锁是用以保护对共享资源的操作,自旋锁是专为防止多处理器并发而引入的一种锁,信号量是用来记录对某个资源存取状况的计数器。
1、Linux并发
并发是指多个执行单元同时、并行被执行。linux 系统产生并发的原因:
- 多线程并发访问,linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
- 抢占式并发访问,从内核2.6版本开始,linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
- 中断程序并发访问,硬件中断的权利是很大的。
- SMP(多核)核间并发访问,在现代计算机体系结构中,多核CPU已经成为了常见的选择,这意味着不同核之间会进行并发访问。然而,并发访问也带来了一些问题,其中最重要的是竞争。当多个核同时访问共享数据时,就有可能出现临界区问题,即同一时间只能有一个线程访问临界区,因此必须保证临界区操作是原子性的,不能分解成更小的步骤。否则,就会导致竞争现象的发生。在编写驱动程序时,应该考虑到并发和竞争问题,并在之前进行防范,而不是编写完整个驱动程序后再解决这些问题。
2、Linux竞态
并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问容易发生竞态。竞态发生的情况:
- 对称多处理器(SMP)的多个CPU:SMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器。
- 单CPU内进程与抢占它的进程:Linux内核支持内核抢占,一个进程在内核执行的时候可能被另一个优先级高的进程打断,进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU。
- 中断(硬中断、软中断)与进程之间:中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,竞态也会发生。中断也可能被新的更高优先级的中断打断,因此,多个中断之间也可能引起并发而导致竞态发生。
3、Linux互斥锁
互斥锁是用以保护对共享资源的操作,即保护线程对共享资源的操作代码可以完整执行,而不会在访问的中途被其他线程介入对共享资源访问。通常把对共享资源操作的代码段,称之为临界区,其共享资源也可以称为临界资源。于是这种机制——互斥锁的工作原理就是对临界区进行加锁,保证处于临界区的线程不被其他线程打断,确保其临界区运行完整。
Linux内核中提供以下API来操作互斥锁:
//1)定义互斥锁lock
mutex_init(struct mutex* lock);
//2)获取互斥锁
mutex_lock(struct mutex *lock);
//3)释放互斥锁
mutex_unlock(struct mutex *lock);
4、Linux自旋锁
自旋锁是一种典型的对临界资源进行互斥访问的手段,其名称来源于他的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需要先执行一个原子操作,该操作测试并设置某个内存变量,由于是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试表明自旋锁被占用,程序将在一个小的循环中重复这个“测试并设置”的操作,即所谓的“自旋”。当自旋锁的持有者通过重置该变量释放这个自旋锁之后,某个“等待的测试并设置”操作向其调用者报告锁已释放。
Linux内核中提供以下API来操作自旋锁:
//1)定义自旋锁
spinlock_t lock;
//2)初始化自旋锁
spin_lock_init(lock);
//3)获取自旋锁
spin_lock(lock); //获得自旋锁lock
spin_trylock(lock);//尝试获取lock如果不能获得锁,返回假值,不在原地打转
//4)释放自旋锁
spin_unlock(lock); //释放自旋锁
5、Linux信号量
本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它是对临界区保护的一种常用方法,他的使用方法和自旋锁差不多。与自旋锁相同只有得到信号量的进程才能执行临界区的代码。但是与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
Linux内核中提供以下API来操作信号量:
//1)定义信号量
Struct semaphore sem;
//2)初始化信号量
void sema_init(struct semaphore *sem, int val); //初始化sem为val,当然还有系统定义的其他宏初始化,这里不列举
//3)获得信号量
void down(struct semaphore *sem); //获得信号量sem,其会导致睡眠,并不能被信号打断
int down_interruptible(struct semaphore *sem); //进入睡眠可以被信号打断
int down_trylock(struct semaphore *sem); //不会睡眠
//4)释放信号量
void up(struct semaphore *sem); //释放信号量,唤醒等待进程
延伸阅读1:竞态的解决方法——中断屏蔽
在单个CPU范围内避免竞态的一种简单省事的方法是在进入临界区之前屏蔽系统的中断。CPU一般具备屏蔽和打开中断的能力,这样可以保证正在执行的内核路径不被中断处理程序抢占,防止竞态条件的发生。具体而言,中断屏蔽使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作依赖中断来实现,内核抢占进程之间的并发得以避免。但是不能长时间屏蔽中断,因为在中断屏蔽期间,所有的中断得不到处理,有可能造成数据丢失和系统崩溃的可能。这就要求在中断屏蔽之后,当前的内核执行路径应当尽快的执行完临界区的代码。