linux内核之并发与竞态详述

竞 态

为了详细叙述竞态的概念,我们结合一段 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(可导致休眠)**是个严重的错误。
  • 最好的方法就是:避免出现需要多个锁的情况

猜你喜欢

转载自blog.csdn.net/qq_41782149/article/details/90138847