了解linux中的“原子整形数据”操作、“原子位数据”操作、自旋锁、读写锁、顺序锁、信号量和互斥体,以及相关函数。
并发就是多个“用户”同时访问同一个共享资源。如:多个线程同时要求读写同一个EEPROM芯片,这个EEPROM就是共享资源,为了保证读写的正确性,其它线程必须等待“持有者”释放使用权限,才可以使用该EEPROM。并发带来的问题就是竞争。Linux采用“原子整形数据”操作、“原子位数据”操作、自旋锁、读写锁、顺序锁、信号量和互斥体的函数来解决并发与竞争。
1、Linux系统并发产生的原因:
1)、多线程并发访问。
2)、抢占式并发访问,从2.6版本内核开始,Linux内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
3)、中断程序并发访问。
4)、SMP(多核)核间并发访问。
注意:SOC称为系统级芯片,也有称片上系统。
如果线程A和线程B修改同一个存储单元,如果没有竞争,就会按照下面的流程执行,都会得到正确的结果。
线程A:
ldr r0, =0X30000000 /* r0=0X30000000 */
ldr r1, =10 /* r1=10 */
str r1, [r0] /*将地址0X30000000的存储单元设置为10*/
线程B:
ldr r0, =0X30000000 /* r0=0X30000000 */
ldr r1, =20 /* r1=20 */
str r1, [r0] /*将地址0X30000000的存储单元设置为20*/
如果线程A和线程B发生竞争,地址0X30000000的存储单元设置的值可能是错误的。
2、原子操作:
原子操作是指不能再进行分割的操作。主要指整型变量操作或者位变量操作。
1)、“原子整形数据”操作:
①、在“include/linux/types.h”文件中,atomic_t的结构体如下
如果是32位的系统级芯片,Linux内核定义的32位原子结构体如下:
typedef struct {
int counter;
}atomic_t;
如果是64位的系统级芯片,Linux内核也定义了64位原子结构体如下:
typedef struct {
s64 counter;
} atomic64_t;
②、“原子整形数据”操作函数:
ATOMIC INIT(int i)
定义原子变量的时候对其初始化;
int atomic read(atomic_t *v)
读取v->counter的值,并且返回;
void atomic_set(atomic_t *v, int i)
向v->counter写入i值;
void atomic_add(int i. atomic_t *v)
给v->counter加上i值;
void atomic_sub(int i, atomic_t *v)
从v->counter减去i值;
void atomic_inc(atomic_t *v)
给v->counter加1,也就是自增;
void atomic_dec(atomic_t *v)
从v->counter减1,也就是自减;
int atomic_dec_return(atomic_t *v)
从v->counter减1,并且返回v的值;
int atomic_inc_return(atomic_t *v)
给v->counter加1,并且返回v的值;
int atomic_sub_and_test(int i.atomic_t *v)
从v->counter减i,如果结果为0就返回真,否则返回假;
int atomic_dec_and_test(atomic_t *v)
从v->counter减1,如果结果为0就返回真,否则返回假;
int atomic_inc_and_test(atomic_t *v)
给v->counter加1,如果结果为0就返回真,否则返回假;
int atomic_add_negative(int i, atomic_t *v)
给v->counter加i,如果结果为负就返回真,否则返回假;
举例:
atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v,并初始化原子变零v=0 */
atomic_set(&v, 10); /* 设置v->counter=10 */
atomic_read(&v); /* 读取v->counter的值,肯定是10 */
atomic_inc(&v); /* v->counter的值加1,v->counter=11 */
atomic_t Mylock; /* 原子变量 */
Mylock = (atomic_t)ATOMIC_INIT(0); //初始化原子变量
atomic_set(&Mylock, 1); //原子变量初始值为Mylock->counter=1
if (!atomic_dec_and_test(&Mylock)) {
//当Mylock->counter=1时,atomic_dec_and_test()返回1
//从Mylock->counter减1,如果结果为0就返回1,否则返回0;
atomic_inc(&Mylock);
return -EBUSY; /* Mylock被使用,返回忙 */
}
2)、“原子位数据”操作:
需要包含“#include <bitops.h>”
void set_bit(int nr, void *p)
将p地址的第nr位置1;
void clear_bit(int nr,void *p)
将p地址的第nr位清零;
void change_bit(int nr, void *p)
将p地址的第nr位进行翻转;
int test_bit(int nr, void *p)
获取p地址的第nr位的值;
int test_and_set_bit(int nr, void *p)
将p地址的第nr位置 1,并且返回第nr位原来的值;
int test_and_clear_bit(int nr, void *p)
将p地址的第nr位清零,并且返回第nr位原来的值;
int test_and_change_bit(int nr, void *p)
将p地址的第nr位翻转,并且返回第nr位原来的值;
举例:
#include <bitops.h>
unsigned long mig_status;
#define NFS_MIG_FAILED (2)
int ret;
if ( test_bit(NFS_MIG_FAILED, &mig_status) )
{//mig_status的bit2=1,则执行
ret = -EIO;
return ret;
}
3)、原子操作只对整型变量和位变量数据进行保护。
3、自旋锁
1)、自旋锁结构体spinlock_t
需要包含头文件“#include < spinlock_types.h >”;
Linux内核使用结构体spinlock_t表示自旋锁,定义如下:
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;
2)、采用“自旋锁”,实现线程之间的并发访问函数:
需要包含头文件“#include <spinlock.h>”;
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
声明x为自旋锁结构,并初始化;
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;
注意:
线程之间的并发访问函数,适用于线程之间的并发访问,但不适合中断程序并发访问。
3)、采用“自旋锁”,实现线程与中断之间的并发访问函数:
void spin_lock_irq(spinlock_t *lock)
禁止本地中断,并获取自旋锁;
void spin_unlock_irq(spinlock_t *lock)
激活本地中断,并释放自旋锁;
void spin_lock_irqsave(spinlock_t *lockunsigned long flags)
保存中断状态,禁止本地中断,并获取自旋锁;
void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags)
将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。
注意:在线程中使用spin_lock_irqsave()和spin_unlock_irqrestore();在中断中使用spin_lock()和spin_unlock()来获取自旋锁和释放自旋锁。不推荐使用spin_lock_irq()和spin_unlock_irq函数。
4)、举例:
DEFINE_SPINLOCK(MyLock) /*声明MyLock为自旋锁结构,并初始化*/
/* 线程A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&MyLock, flags); /* 获取锁 */
//保存中断状态,禁止本地中断,并获取自旋锁;
/* 临界区 */
spin_unlock_irqrestore(&MyLock, flags); /* 释放锁 */
//将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁;
}
/* 中断服务函数 */
void irq() {
spin_lock(&MyLock); /* 获取锁 */
//获取MyLock自旋锁,也叫做加锁;
/* 临界区 */
spin_unlock(&MyLock); /* 释放锁 */
//释放MyLock自旋锁;
}
5)、“下半部”并发访问函数
需要包含文件“#include <spinlock.h>”
下半部(BH)也会竟争共享资源,有些资料也会将下半部叫做底半部。如果要在“下半部”里面使用自旋锁,则需要用到下面的函数:
void spin_lock_bh(spinlock_t*lock)
关闭下半部,并获取自旋锁;
void spin_unlock_bh(spinlock_t*lock)
打开下半部,并释放自旋锁;
6)、自旋锁使用注意事项:
①、“其它线程”在等待自旋锁的时处于“自旋”状态,因此“持有锁的线程”不能长时间持用这个自旋锁,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的API函数,否则的话可能导致死锁。
③、不能递归申请白旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的SOC,都将其当做多核SOC来编写驱动程序。
4、读写锁函数
需要包含文件“#include <rwlock_types.h>”
typedef struct {
arch_rwlock_t raw_lock;
#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;
#define DEFINE_RWLOCK(x) rwlock_t x = __RW_LOCK_UNLOCKED(x)
声明“读写锁结构变量x”,并初始化读写锁;
读写锁函数需要包含文件“#include <rwlock.h>”
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_irg(rwlock_t *lock)
打开本地中断,并且释放读锁;
void read_lock_irgsave(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_irg(rwlock_t *lock)
禁止本地中断,并且获取写锁;
void write_unlock_irg(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)
打开下半部,并释放读锁;
5、顺序锁
顺序锁结构需要包含文件“#include <seqlock.h>”
顺序锁结构体seqlock_t
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
}seqlock_t;
使用顺序锁,实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。
注意:
顺序锁“保护的资源”不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。
#define DEFINE_SEQLOCK(x) \
seqlock_t x = __SEQLOCK_UNLOCKED(x)
声明“顺序锁结构变量x”,并初始化
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 *sl,unsigned long flags)
保存中断状态,禁止本地中断,并获取“写顺序锁”;
void write_sequnlock_irqrestore(seqlock_t *sl,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)
读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读;
6、信号量
信号量有一个“信号量值”,用来控制访问共享资源的访问数量,相当于通过“信号量值”控制访问资源的线程数。
如果将“信号量值”设置大于 1,那么这个信号量就是“计数型信号量”,它允许多个线程同时访问共享资源。
如果将“信号量值”设置为0,那么这个信号量就是“二值信号量”,它具有互斥访问共享资源的作用。
需要包含文件“#include <semaphore.h>”
1)、信号量结构体semaphore
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
并将“name.count”设置为n;
2)、信号量函数
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
声明信号量结构变量为name,并将信号量的值“name. count”设置为1;这是一个“计数型信号量”。
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
初始化信号量结构变量sem,并将“sem->count”设置为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)
释放信号量
int down_killable(struct semaphore *sem)
进入休眠状态的线程可以被唤醒,中断获取信号量的操作;
3)、举例:
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
4)、信号量的特点:
信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
合,因此信号量不能用于中断中,因为信号量会引起休眠,中断是不能进入休眠的。
如果共享资源的持有时间比较短,那就不适合使用信号量。因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
7、互斥体
需要包含文件“#include <mutex.h>”
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
};
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
声明互斥结构变量mutexname,并初始化;
void mutex_init(struct mutex *lock);
并初始化互斥结构变量lock
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 lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */
使用“互斥体”时,需要注意如下几点:
①、互斥体可以导致休眠,因此不能在中断中使用互斥体,在中断中只能使用自旋锁。
②、和信号量一样,“互斥体”保护的临界区可以调用引起阻塞的API函数。
因为一次只有一个线程可以持有“互斥体”,因此,必须由“互斥体”的持有者释放“互斥体”。
③、“互斥体”不能递归上锁和解锁。