Linux设备驱动中的并发控制

注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。

书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

参考书籍:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6.pdf》

1 并发和竞争

并发(Concurrency):指多个执行单元同时执行、并发被执行,并发执行单元对共享资源(全局变量、静态变量、硬件资源、设备结构体等)的访问容易导致竞态。

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
访问共享资源的代码区域称为临界区(Critical Sections),临界区需要被以某种互斥机制加以保护。常用的互斥机制:中断屏蔽、原子操作、自旋锁、信号量、互斥体等。

并发访问主要有以下几种产生:

1、多线程并发访问,Linux是多任务(线程)的系统,所以多线程访问是最基本的并发访问。

2、抢占式并发访问,从2.6版本内核开始,Linux内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。

3、中断程序并发访问,硬件中断的产生。

4、SMP(多核)核间并发访问,现在ARM架构的多核SOC很常见,多核 CPU 存在核间并发访问。

2 中断屏蔽

CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免了。

中断屏蔽的方法:

local_irq_disable()  //屏蔽本CPU中断
...
critical_secion  //临界区
...
local_irq_enable()  //开本CPU中断

中断的时间不应该过长,在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。

中断屏蔽仅能屏蔽本CPU的中断,这种方式不是很好。

3 原子操作

原子操作可以保证对一个整型数据的修改是排他性的。

Linux内核中提供了两类函数来实现原子操作,分别针对位和整型变量进行原子操作。

3.1 整型原子操作

定义在:include/linux/types.h

typedef struct {
	int counter;
} atomic_t;

(1)设置原子变量的值

/**
 * atomic_set - set atomic variable
 * @v: pointer of type atomic_t
 * @i: required value
 *
 * Atomically sets the value of @v to @i.
 */
#define atomic_set(v, i) (((v)->counter) = (i))
atomic_t v = ATOMIC_INIT(0);  //定义原子变量v并初始化为0

(2)获取原子变量的值

/* 返回原子变量的值 */
#define atomic_read(v)	ACCESS_ONCE((v)->counter)

(3)原子变量加/减

/* 原子变量增加i */
void atomic_add(int i, atomic_t *v);
/* 原子变量减少i */
void atomic_sub(int i, atomic_t *v);

(4)原子变量自增/自减

/* 原子变量增加1 */
void atomic_inc(atomic_t *v);
/* 原子变量减少1 */
void atomic_dec(atomic_t *v);

(5)操作并测试

/* 对原子变量执行自增、自建和减操作,测试其是否为0,为0返回true,否则返回false */
#define atomic_sub_and_test(i, v)	(atomic_sub_return((i), (v)) == 0)
#define atomic_dec_and_test(v)		(atomic_dec_return(v) == 0)
#define atomic_inc_and_test(v)		(atomic_inc_return(v) == 0)

(6)操作并返回

#define atomic_dec_return(v)		atomic_sub_return(1, (v))
#define atomic_inc_return(v)		atomic_add_return(1, (v))
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);

总结如下:

函数 描述
ATOMIC_INIT(int i) 定义原子变量并对其初始化
int atomic_read(atomic_t *v) 读取v的值,并且返回
void atomic_set(atomic_t *v, int i) 向v写入i值
void atomic_add(int i, atomic_t *v) 给v加上i值
void atomic_sub(int i, atomic_t *v) 给v减去i值
void atomic_inc(atomic_t *v) 给v加1,自增
void atomic_dec(atomic_t *v) 给v减1,自减
int atomic_inc_return(int i, atomic_t *v) 给v加1,并且返回v的值
int atomic_dec_return(int i, atomic_t *v) 给v减1,并且返回v的值
int atomic_sub_and_test(int i, atomic_t *v) 给v减i,如果结果为0就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v) 给v加1,如果结果为0就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) 给v减1,如果结果为0就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v) 给v加i,如果结果为负就返回真,否则返回假

3.2 位原子操作

(1)设置位

/* 设置addr地址的第nr位,将该位写为1 */
void set_bit(int nr, volatile unsigned long *addr);

(2)清除位

/* 清除addr地址的第nr位,该位写0 */
void clear_bit(int nr, volatile unsigned long *addr);

(3)改变位

/* addr地址的第nr位进行取反 */
void change_bit(int nr, volatile unsigned long *addr);

(4)测试位

/* 返回addr地址的第nr位 */
int test_bit(int nr, const volatile unsigned long *addr);

(5)测试并操作位

int test_and_set_bit(int nr, volatile unsigned long *addr);
int test_and_clear_bit(int nr, volatile unsigned long *addr);
int test_and_change_bit(int nr, volatile unsigned long *addr);

下面代码是使用原子操作使设备只能被一个进程打开:

static atomic_t xxx_available = ATOMIC_INIT(1); /* 定义原子变量 */

static int xxx_open(struct inode *inode, struct file *filp)
{
    ...
  	if (!atomic_dec_and_test(&xxx_available))  {
      	atomic_inc(&xxx_available);
      	return  - EBUSY;  /* 已经打开 */
  }
 	...
 	return 0;  /* 成功 */
}

static int xxx_release(struct inode *inode, struct file *filp)
{
  	atomic_inc(&xxx_available);  /* 释放设备 */
  	return 0;
}

总结如下:

函数 说明
void set_bit(int nr, volatile unsigned long *p) 将p地址的第nr位置1
void clear_bit(int nr, volatile unsigned long *p) 将p地址的第nr位清0
void change_bit(int nr, volatile unsigned long *p) 将p地址的第nr位进行翻转
int test_bit(int nr, const volatile unsigned long *p) 获取p地址的第nr的值
int test_and_set_bit(int nr, volatile unsigned long *p) 将p地址的第nr位置1,并且返回nr位原来的值。
int test_and_clear_bit(int nr, volatile unsigned long *p) 将p地址的第nr位置0,并且返回nr位原来的值。
int test_and_change_bit(int nr, volatile unsigned long *p) 将p地址的第nr位翻转,并且返回nr位原来的值。

4 自旋锁

自旋锁(Spin Lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(Test-And-Set)某个内存变量。由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。

4.1 自旋锁相关API

4.1.1 操作函数

(1)定义自旋锁

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;

spinlock_t lock;

(2)初始化自旋锁

#define spin_lock_init(_lock)				\
do {							\
	spinlock_check(_lock);				\
	raw_spin_lock_init(&(_lock)->rlock);		\
} while (0)

(3)获取自旋锁

/* 获取自旋锁,如果能够立即获得锁,则立即返回,否则在原地自旋直到获得 */
void spin_lock(spinlock_t *lock);
/* 尝试获得自旋锁,如果能够立即获得,则获得并返回true,否则立即返回false,不在原地自旋 */
int spin_trylock(spinlock_t *lock);

(4)释放自旋锁

/* 释放自旋锁,和spin_lock/spin_trylock一起使用 */
void spin_unlock(spinlock_t *lock);

总结如下:

函数 说明
DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自旋锁
int spin_lock_init(spinlock_t lock) 初始化自旋锁
void spin_lock(spinlock_t *lock) 获取指定的自旋锁,加锁
void spin_unlock(spinlock_t *lock) 释放指定的自旋锁
int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回0
int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否被获取,如果没有被获取就返回非0,否则返回0

4.1.2 自旋锁使用方式

使用方式如下:

spinlock_t lock;  		//定义一个自旋锁
spin_lock_init(&lock);	//初始化自旋锁
spin_lock(&lock);  		//获取自旋锁,保护临界区
...;  					//临界区
spin_unlock(&lock); 	//释放自旋锁

自旋锁主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。在多核SMP的情况下,任何一个核拿到了自旋锁,该核上的抢占调度也暂时禁止了,但是没有禁止另外一个核的抢占调度。

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH,稍后的章节会介绍)的影响。为了防止这种影响,就需要用到自旋锁的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断local_irq_disable()/开中断local_irq_enable()、关底半部local_bh_disable()/开底半部local_bh_enable()、关中断并保存状态字local_irq_save()/开中断并恢复状态字local_irq_restore()结合就形成了整套自旋锁机制,关系如下:

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
函数 功能
void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned flags) 保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并激活本地中断,释放自旋锁。
void spin_lock_bh(spinlock_t *lock) 关闭底半部,并获取自旋锁。
void spin_unlock_bh(spinlock_t *lock) 打开底半部,并释放自旋锁。

一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock。

DEFINE_SPINLOCL(lock);  /* 定义并初始化一个自旋锁 */
/* 线程A */
void func_a()
{
    unsigned long flags;  /* 中断状态 */
    spin_lock_irqsave(&lock, flags);  /* 获取锁 */
    /* 临界区 */
    spin_unlock_irqrestore(&lock, flags)  /* 释放锁 */
}

/* 中断服务函数 */
void irq()
{
    spin_lock(&lock);  /* 获取锁 */
    /* 临界区 */
    spin_unlock(&lock);  /* 释放锁 */
}

4.1.3 注意点

应谨慎使用自旋锁,在使用中还要特别注意如下几个问题。

1)自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。

2)自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。

3)在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核的崩溃。

4)在单核情况下编程的时候,也应该认为自己的CPU是多核的,驱动特别强调跨平台的概念。比如,在单CPU的情况下,若中断和进程可能访问同一临界区,进程里调用spin_lock_irqsave()是安全的,在中断里其实不调用spin_lock()也没有问题,因为spin_lock_irqsave()可以保证这个CPU的中断服务程序不可能执行。但是,若CPU变成多核,spin_lock_irqsave()不能屏蔽另外一个核的中断,所以另外一个核就可能造成并发问题。因此,无论如何,我们在中断服务程序里也应该调用spin_lock()。

4.1.4 使用实例代码

下面代码是使用自旋锁使设备最多只能被1个进程打开:

int xxx_count = 0;  //定义文件打开次数计数
spinlock_t xxx_lock;

static int xxx_open(struct inode *inode, struct file *filp)
{
    ...;
    spin_lock(&xxx_lock);
    if (xxx_count) {  //文件已经打开
        spin_unlock(&xxx_lock);
        return -EBUSY;
    }
    xxx_count++;  //增加使用计数
    spin_unlock(&xxx_lock);
    ...;
    return 0;
}

static int xxx_release(struct inode *inode, struct file *filp)
{
    ...;
    spin_lock(&xxx_lock);
    xxx_count--;  //减少使用计数
    spin_unlock(&xxx_lock);
    
    return 0;
}

4.2 读写自旋锁

自旋锁不关心锁定的临界区究竟在进行什么操作,不管是读还是写,它都一视同仁。即便多个执行单元同时读取临界资源也会被锁住。

实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发。读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有1个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。

4.2.1 操作函数

(1)定义和初始化读写自旋锁

typedef struct {
	arch_rwlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
	unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} rwlock_t;

rwlock_t rwlock;
rwlock_init(&rwlock);

(2)读锁定

void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);

(3)读解锁

void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

对共享资源进行读取之前,应该先调用读锁定函数,完成之后再调用读解锁函数。

(4)写锁定

void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);  //尝试获取读写自旋锁,不管成功失败都立即返回

(5)写解锁

void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

总结如下:

函数 说明
DEFINE_RWLOCK(rwlock_t lock) 定义并初始化读写锁
void rwlock_init(rwlock_t *lock) 初始化读写锁
读锁
void read_lock(rwlock_t *lock) 获取读锁
void read_unlock(rwlock_t *lock) 释放读锁
void read_lock_irq(rwlock_t *lock) 禁止本地中断,并获取读锁。
void read_unlock_irq(rwlock_t *lock) 打开本地中断,并释放读锁。
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取读锁。
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 将中断恢复到以前的状态,并且激活本地中断,释放读锁。
void read_lock_bh(rwlock_t *lock) 关闭底半部,并获取读锁。
void read_unlock_bh(rwlock_t *lock) 打开底半部,并释放读锁。
写锁
void write_lock(rwlock_t *lock) 获取写锁
void write_unlock(rwlock_t *lock) 释放写锁
void write_lock_irq(rwlock_t *lock) 禁止本地中断,并获取写锁。
void write_unlock_irq(rwlock_t *lock) 打开本地中断,并释放写锁。
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取写锁。
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 将中断恢复到以前的状态,并激活本地中断,释放写锁。
void write_lock_bh(rwlock_t *lock) 关闭底半部,并获取写锁。
void write_unlock_bh(rwlock_t *lock) 打开底半部,并释放写锁。

4.2.2 使用方式

在对共享资源进行写之前,应该先调用写锁定函数,完成之后再调用写解锁函数。

读写锁的用法:

rwlock_t lock;       /* 定义rwlock */
rwlock_init(&lock);  /* 初始化rwlock */
read_lock(&lock);	 /* 读时获取锁 */
...                  /* 临界资源 */
read_unlock(&lock);

write_lock_irqsave(&lock, flags);  /* 写时获取锁 */
...                                /* 临界资源 */
write_unlock_irqrestore(&lock, flags);

4.3 顺序锁

顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元不会被写执行单元阻塞,也就是说,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其他写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。

对于顺序锁而言,尽管读写之间不互相排斥,但是如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。所以,在这种情况下,读端可能反复读多次同样的区域才能读到有效的数据。

4.3.1 写执行单元顺序锁操作

写执行单元涉及的顺序锁操作如下:

typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

(1)获得顺序锁

void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock)

(2)释放顺序锁

void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)

写执行单元使用顺序锁的模式:

write_seqlock(&seqlock_a);
.../* 写操作代码块 */
write_sequnlock(&seqlock_a);

总结如下:

函数 说明
DEFINE_SEQLOCK(seqlock_t sl) 定义初始化顺序锁
void seqlock_init(seqlock_t sl) 初始化顺序锁
顺序锁写操作
void write_seqlock(seqlock_t *sl) 获取写顺序锁
void write_sequnlock(seqlock_t *sl) 释放写顺序锁
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并获取写顺序锁。
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并释放写顺序锁。
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取写顺序锁。
void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁。
void write_seqlock_bh(seqlock_t *sl) 关闭底半部,并获取顺序锁。
void write_sequnlock_bh(seqlock_t *sl) 打开下半部,并释放顺序锁。
顺序锁读操作
unsigned read_seqbegin(const seqlock_t *sl) 读单元访问共享资源时调用本函数,此函数会返回顺序锁的顺序号。
unsigned read_seqretry(const seqlock_t *sl, unsigned start) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有就重读。

4.3.2 读执行单元顺序锁操作

读执行单元涉及的顺序锁操作如下:

(1)读开始

读执行单元在对被顺序锁s1保护的共享资源进行访问前需要调用该函数,该函数返回顺序锁s1的当前顺序号。

unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)

(2)重读

读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。

int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)

读执行单元使用顺序锁的模式如下:

do {
    seqnum = read_seqbegin(&seqlock_a);
    /* 读操作代码块 */
    ...
} while (read_seqretry(&seqlock_a, seqnum));

4.4 读-复制-更新

RCU(Read-Copy-Update,读-复制-更新),它是基于其原理命名的。

不同于自旋锁,使用RCU的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读(只是简单地标明读开始和读结束),而RCU的写执行单元在访问它的共享资源前首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据读操作的时候。等待适当时机的这一时期称为宽限期(Grace Period)。

5 信号量

信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。
P(S):①将信号量S的值减1,即S=S-1;②如果S≥0,则该进程继续执行;否则该进程置为等待状态,陷入睡眠,排入等待队列。
V(S):①将信号量S的值加1,即S=S+1;②如果S>0,唤醒队列中等待信号量的进程。

5.1 相关操作

(1)定义信号量

struct semaphore {
	raw_spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};
struct semaphore;

(2)初始化信号量

/* 初始化信号量,并设置sem的值为val */
void sema_init(struct semaphore *sem, int val);

(3)获取信号量

/* 获取信号量,会导致睡眠,不能在中断上下文中使用 */
void down(struct semaphore *sem);
/* 和down类似,进入睡眠状态的进程能被信号打断,信号也会导致函数返回,这时候返回值非0 */
int down_interruptible(struct semaphore *sem);
/* 尝试获取信号量,能够立即获得就获得并返回0,否则返回非0值,不会导致调用者睡眠,可在中断上下文中使用 */
int down_trylock(struct semaphore *sem);

(4)释放信号量

void up(struct semaphore *sem);

总结如下:

函数 说明
DEFINE_SEMAPHORE(name) 定义一个信号量,并且设置信号量的值为1。
void sema_init(struct semaphore *sem, int val) 初始化信号量,设置信号量值为val。
void down(struct semaphore *sem) 获取信号量,会导致睡眠,不能在中断中使用。
int down_trylock(struct semaphore *sem) 尝试获取信号量,如果能获取就获取并返回0,不能获取就返回非0,并且不会睡眠。
int down_interruptible(struct semaphore *sem) 获取信号量,陷入睡眠后可以被信号打断。
void up(struct semaphore *sem) 释放信号量

作为一种可能的互斥手段,信号量可以保护临界区,只有得到信号量的进程才能执行临界区代码。当获取不到信号量时,进程进入休眠等待状态。

由于新的Linux内核倾向于直接使用mutex作为互斥手段,信号量用作互斥不再被推荐使用。

信号量也可以用于同步,一个进程A执行down()等待信号量,另外一个进程B执行up()释放信号量,这样进程A就同步地等待了进程B。其过程类似:

image-20220109211803550

使用举例:

struct semaphore sem;  /* 定义信号量 */

sema_init(&sem, 1); /* 初始化信号量值为1 */
down(&sem);  /* 申请信号量 */
/* 临界区 */
up(&sem);  /* 释放信号量 */

6 互斥体

尽管信号量已经可以实现互斥的功能,但在Linux内核中还是用mutex较为常见。

互斥访问表示一次只能有一个线程访问共享资源,不能递归申请互斥体。

(1)定义并初始化:

struct mutex {
	/* 1: unlocked, 0: locked, negative: locked, possible waiters */
	atomic_t		count;
	spinlock_t		wait_lock;
	struct list_head	wait_list;
};

struct mutex my_mutex;
mutex_init(&my_mutex);

(2)获取互斥体

void mutex_lock(struct mutex *lock);  //引起的睡眠不能被信号打断
int mutex_lock_interruptible(struct mutex *lock);  //睡眠可以被信号打断
int mutex_trylock(struct mutex *lock);  //尝试获取,不能导致睡眠

(3)释放互斥体

void mutex_unlock(struct mutex *lock);

总结如下:

函数 说明
DEFINE_MUTEX(mutexname) 定义并初始化一个mutex变量。
void mutex_init(struct mutex *mutex) 初始化mutex。
void mutex_lock(struct mutex *lock) 获取mutex,给mutex上锁,如果获取不到就休眠。
void mutex_unlock(struct mutex *lock) 释放mutex,给mutex解锁。
int mutex_trylock(struct mutex *lock) 尝试获取mutex,如果成功就返回1,失败返回0.
int mutex_is_locked(struct mutex *lock) 判断mutex是否被获取,如果是就返回1,否则返回0。
int mutex_lock_interruptible(struct mutex *lock) 获取互斥锁,获取失败进入休眠后可以被信号打断。

互斥体的使用方式:

struct mutex my_mutex;      /* 定义mutex */
mutex_init(&my_mutex);      /* 初始化mutex */
mutex_lock(&my_mutex);      /* 获取mutex */
...                         /* 临界资源 */
mutex_unlock(&my_mutex);    /* 释放 mutex */

自旋锁和互斥体都是解决互斥问题的基本手段,如何根据不同的场景使用,需要依据临界区的性质和系统的特点。

从严格意义上说,互斥体和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。在互斥体本身的实现上,为了保证互斥体结构存取的原子性,需要自旋锁来互斥。所以自旋锁属于更底层的手段。

互斥体是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用互斥体才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它可节省上下文切换的时间。但是CPU得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。

由此,可以总结出自旋锁和互斥体选用的3项原则。
1)当锁不能被获取到时,使用互斥体的开销是进程上下文切换时间,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定)。若临界区比较小,宜使用自旋锁,若临界区很大,应使用互斥体。
2)互斥体所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
3)互斥体存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在互斥体和自旋锁之间只能选择自旋锁。当然,如果一定要使用互斥体,则只能通过mutex_trylock()方式进行,不能获取就立即返回以避免阻塞。

7 完成量

Linux提供了完成量(Completion),它用于一个执行单元等待另一个执行单元执行完某事。

7.1 相关操作

(1)定义完成量

struct completion my_completion;

(2)初始化完成量

init_completion(&my_completion);
reinit_completion(&my_completion)  //重新初始化为0

(3)等待完成量

void wait_for_completion(struct completion *c);

(4)唤醒完成量

void complete(struct completion *c);  //唤醒一个等待执行的单元
void complete_all(struct completion *c);  //释放所有等待同一个完成量的执行单元

完成量的同步流程:

image-20220109213042260

8 增加并发控制后的globalmem设备驱动

在globalmem()的读写函数中,由于要调用copy_from_user()、copy_to_user()这些可能导致阻塞的函数,因此不能使用自旋锁,宜使用互斥体。

驱动工程师习惯将某设备所使用的自旋锁、互斥体等辅助手段也放在设备结构中,并在模块初始化函数中初始化这个信号量。

完整的代码如下:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

/* 直接使用立即数当作命令不合理,暂定 */
#define MEM_CLEAR           0x1
#define GLOBALMEM_MAJOR     230
#define GLOBALMEM_SIZE      0x1000

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

/* 设备结构体 */
struct  globalmem_dev {
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];
    struct mutex mutex;
};

struct globalmem_dev *globalmem_devp;

static int globalmem_open(struct inode *inode, struct file *filp)
{
    /* 使用文件的私有数据作为获取globalmem_dev的实例指针 */
    filp->private_data = globalmem_devp;
    return 0;
}

static int globalmem_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/**
 * 设备ioctl函数
 * @param[in] filp:文件结构体指针
 * @param[in] cmd: 命令,当前仅支持MEM_CLEAR
 * @param[in] arg: 命令参数
 * @return  若成功返回0,若出错返回错误码
 */
static long globalmem_ioctl(struct file *filp, unsigned int cmd,
    unsigned long arg)
{
    struct globalmem_dev *dev = filp->private_data;

    switch (cmd) {
    case MEM_CLEAR:
        mutex_lock(&dev->mutex);
        memset(dev->mem, 0, GLOBALMEM_SIZE);
        mutex_unlock(&dev->mutex);
        printk(KERN_INFO "globalmem is set to zero\n");
        break;
    
    default:
        return -EINVAL;
    }
    return 0;
}

/**
 * 读设备
 * @param[in] filp:文件结构体指针
 * @param[out] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 读取的字节数
 * @param[in/out] ppos: 读的位置相当于文件头的偏移
 * @return  若成功返回实际读的字节数,若出错返回错误码
 */
static ssize_t globalmem_read(struct file *filp,
    char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if (p >= GLOBALMEM_SIZE)
        return 0;
    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);
    /* 内核空间到用户空间缓存区的复制 */
    if (copy_to_user(buf, dev->mem + p, count)) {
        ret = -EFAULT;
    } else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "read %lu bytes(s) from %lu\n", count, p);
    }
    mutex_unlock(&dev->mutex);
    return ret;
}

/**
 * 写设备
 * @param[in] filp:文件结构体指针
 * @param[in] buf: 用户空间内存地址,不能在内核中直接读写
 * @param[in] size: 写入的字节数
 * @param[in/out] ppos: 写的位置相当于文件头的偏移
 * @return  若成功返回实际写的字节数,若出错返回错误码
 */
static ssize_t globalmem_write(struct file *filp,
    const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p = *ppos;
    unsigned long count = size;
    int ret = 0;
    struct globalmem_dev *dev = filp->private_data;

    if (p >= GLOBALMEM_SIZE)
        return 0;
    if (count > GLOBALMEM_SIZE - p)
        count = GLOBALMEM_SIZE - p;

    mutex_lock(&dev->mutex);
    /* 用户空间缓存区到内核空间缓存区的复制 */
    if (copy_from_user(dev->mem + p, buf, count))
        ret = -EFAULT;
    else {
        *ppos += count;
        ret = count;
        printk(KERN_INFO "written %lu bytes(s) from %lu\n", count, p);
    }
    mutex_unlock(&dev->mutex);
    return ret;
}

/**
 * 文件偏移设置
 * @param[in] filp:文件结构体指针
 * @param[in] offset: 偏移值大小
 * @param[in] orig: 起始偏移位置
 * @return  若成功返回文件当前位置,若出错返回错误码
 */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
    loff_t ret = 0;
    switch (orig) {
    case 0:  /* 从文件头位置设置偏移 */
        if (offset < 0) {
            ret = -EINVAL;
            break;
        }
        if ((unsigned int)offset > GLOBALMEM_SIZE) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos = (unsigned int)offset;
        ret = filp->f_pos;
        break;
    case 1:  /* 从当前位置设置偏移 */
        if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
            ret = -EINVAL;
            break;
        }
        if ((filp->f_pos + offset) < 0) {
            ret = -EINVAL;
            break;
        }
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;
    
    default:
        ret = -EINVAL;
        break;;
    }
    return ret;
}

static const struct file_operations globalmem_fops = {
	.owner = THIS_MODULE,
	.llseek = globalmem_llseek,
	.read = globalmem_read,
	.write = globalmem_write,
	.unlocked_ioctl = globalmem_ioctl,
	.open = globalmem_open,
	.release = globalmem_release,
};

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err, devno = MKDEV(globalmem_major, index);

    /* 初始化cdev */
    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    /* 注册设备 */
    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

/* 驱动模块加载函数 */
static int __init globalmem_init(void)
{
    int ret;
    dev_t devno = MKDEV(globalmem_major, 0);

    /* 获取设备号 */
    if (globalmem_major)
        ret = register_chrdev_region(devno, 1, "globalmem");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    
    if (ret < 0)
        return ret;
    
    /* 申请内存 */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
    }
    globalmem_setup_cdev(globalmem_devp, 0);
    mutex_init(&globalmem_devp->mutex);
    return 0;

fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(globalmem_init);

/* 驱动模块卸载函数 */
static void __exit globalmem_exit(void)
{
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    /* 释放设备号 */
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

MODULE_AUTHOR("MrLayfolk");
MODULE_LICENSE("GPL v2");

Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += mutex_globalmem.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

编译、测试:

$ make 
$ insmod mutex_globalmem.ko
$ mknod /dev/multex_globalmem c 230 0
$ echo "hello" > /dev/multex_globalmem
$ cat /dev/multex_globalmem

9 总结

并发和竞态广泛存在,中断屏蔽、原子操作、自旋锁和互斥体都是解决并发问题的机制。中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和互斥体应用最为广泛。

自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小。互斥体允许临界区阻塞,可以适用于临界区大的情况。

posted @ 2022-01-13 20:48  zhengcixi  阅读(425)  评论(0编辑  收藏  举报
回到顶部