竞 态
为了详细叙述竞态的概念,我们结合一段 write函数中的片段分配内存的代码来解释:
if(!dptr->data[s_pos])
{
dptr->data[s_pos] = kmalloc(quantum,GFP_KERNEL);
if(!dptr->data[s_pos])
goto out;
}
假定有两个进程(称之为 “”A“”和“”B“”)正在独立地尝试像同一个设备的相同偏移量写入数据,而且两个进程在同一时刻到达上述代码中的第一个if判断语句。如果代码涉及的指针是NULL,两个进程都会决定分配内存,而每个进程都会将结果指针赋值给 dptr->data[s_pos]。因为两个进程对同一位置赋值,显然只有一个赋值会成功。
当然,其结果是第二个完成赋值的进程会“”胜出“”。如果进程A首先赋值,则它的赋值会被进程B覆盖。这样,设备会完全忘记由A分配的内存,而只会记录由进程B分配得到的指针。因此,由A分配的内存将丢失,从而永远不会返回到系统中。
上述事件的过程就是一种 竞态 。
竞态会导致对共享数据的非控制访问。发生错误的访问模式时,会产生非预期的结果。而这里讨论的竞态,其结果是内存的泄露。
信号量和互斥体
原子操作:不可被中断的一个或一系列操作。
为了避免竞态的发生,我们需要对设备驱动添加锁定。目的是使对数据结构的操作是原子的,这就意味着在涉及到其他执行线程之前,整个操作已经结束了。对此,我们必须建立临界区:在任意给定的时刻,代码只能被一个线程执行。
所以在此引入了信号量。
在计算机科学中,信号量是一个总所周知的概念。一个信号量的本质是一个整数值,它和一对函数联合使用,这对函数通常称为P和V。希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待直到其他人释放该信号量。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同时在一个临界区中运行),信号量的值应初始化为1.这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“”互斥体(mutex)“”,它是**互斥(mutual exclusion)**的简称,Linux内核中几乎所有的信号量均用于互斥。
Linux信号量的实现
头文件<asm/semaphore.h>
相关类型为:struct semaphore
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量初始化
- void sema_init(struct semaphore *sem,int val)
其中 val 是赋予信号量的初始值。
不过信号量通常用于互斥模式,所以内核提供辅助函数和宏。所以,可以通过下面方法之一来声明和初始化一个互斥体。
#define DECLARE_MUTEX(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
信号量的值name被初始化为1.
和
DECLARE_MUTEX_LOCKED(name);
信号量的值name被初始化为0.
如果互斥体必须在运行时被初始化(例如在动态分配互斥体情况下),应使用下面的函数之一。
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
在Linux世界中,P函数被称为down ------或者这个名字的其他变种。这里有down的三个版本
extern int __must_check down_interruptible(struct semaphore *sem); //减小信号量的值,但操作是可中断的,它允许等待在某个信号量上的用户空间进程可被用户中断
extern int __must_check down_killable(struct semaphore *sem);
extern int __must_check down_trylock(struct semaphore *sem); //永远不会睡眠,如果信号量在调用时不可获得,down_trylock 立即返回一个非零值
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
当一个线程成功调用上述 down 的某一个版本后,就称该线程“”拥有“”了信号量。这样,该线程就被赋予访问由该信号量保护的临界区的权利。
同样,Linux中的V操作的函数为up
extern void up(struct semaphore *sem);
调用up之后,调用者将不再拥有该信号量。
自旋锁
信号量对互斥来说是非常有用的工具,但不是内核唯一提供的。相反,大多数锁定通过称为 自旋锁 的机制实现。和信号量不同,自旋锁可以在不能休眠的代码中实现,比如中断处理。
自旋锁不会引起调用者睡眠,称为 忙等。
一个自旋锁是一个互斥设备,它只能有两个值:锁定和解锁。它通常实现为某个整数值中的单个位。希望获取某特定锁的代码测试相关的位。如果锁可用,则锁定位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环并重复检查这个锁,知道该锁可用为止。这个循环就是自旋锁的自旋部分。
当然,其实自旋锁的实现还是比较复杂的。
- 测试和设置的操作必须是原子的。这样,即使有多个线程在给定时间自旋,也只有一个线程可以获得该锁。
- 还要避免死锁。
如果某一个获得锁的函数要调用其他同样试图获取这个锁的函数,我们的到吗就会死锁。不管是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁;如果试图这样做,系统将挂起。
自旋锁的API
头文件 linux/spinlock.h
实际的锁类型:spinlock_t
自旋锁初始化
spinlock_t my_lock = SPIN_LOCK_UNLOCKED:
或者在运行时,调用:
void spin_lock_init(spinlock_t *lock);
在进入临界区之前,代码必须调用下面的函数获得需要的锁:
void spin_lock(spinlock_t *lock)
所有的自旋锁在本质上都是不可中断的。一旦调用了 spin_lock,在获得锁之前一直处于自旋状态。
要释放获得的锁,可将锁传递给下面的函数:
void spin_unlock(spinlock_t *lock);
适用于自旋锁的核心规则是:
- 任何拥有自旋锁的代码都必须是原子的。它不能休眠,事实上,它不能因为任何原因放弃处理器,除了服务中断以外(某些情况下此时也不能放弃处理器)。
- 内核抢占的情况由自旋锁代码自身处理。任何时候,只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。
- 自旋锁必须在可能的最短时间内拥有。
使用信号量与自旋锁的时机
信号量:适合时间较长情况,可以有多个持有者、
自旋锁:不适合长时间,只能有一个持有者。
锁的顺序规则
- 如果我们必须获得一个局部锁(比如一个设备锁),以及一个属于内核更中心位置的锁,则应该首先获取自己的局部锁。
- 如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量。
- **在拥有自旋锁时调用 down(可导致休眠)**是个严重的错误。
- 最好的方法就是:避免出现需要多个锁的情况。