Linux并发与竞争

前言

        Linux系统是个多任务的操作系统,会存在多个任务同时访问同一片内存区域的情况,造成内存数据混乱,严重的话可能会导致系统崩溃;因此我们需要针对这一现象做处理。

        产生并发的主要原因有:①最基本的原因是多线程并发访问;②抢占式并发访问;③中断程序并发访问;④多核CPU间并发访问。

        并发访问产生对临界区(即共享数据段)的竞争关系,所以对于临界区要保证只能有一个线程访问。

1.原子操作

1.1 概念

        原子操作就是指不能再进一步分割的操作 ,一般原子操作用于整形变量或者位的操作。

Linux 内核定义了叫做atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h 文件中,

typedef struct {

    int counter;

} atomic_t;

64位的SOC的话,就要用到 64位的原子变量, Linux内核也定义了64位原子结构体:

#ifdef CONFIG_64BIT

typedef struct {

    long counter;

} atomic64_t;

#endif

1.2 常用API函数

1.2.1 整形原子变量API

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_dec_return(atomic_t *v)从v减 1,并且返回 v的值。
int atomic_inc_return(atomic_t *v)给v加 1,并且返回 v的值。
int atomic_sub_and_test(int i, atomic_t *v)从v减 i,如果结果为 0就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v)从v减 1,如果结果为 0就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)给v加 1,如果结果为 0就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v)

给v加 i,如果结果为负就返回真,否则返回假。

示例:

atomic_t  var = ATOMIC_INIT(1);  //初始化为1

atomic_set(var, 2);                              //var设置为2

atomic_add(1, var);                             //var值加1

1.2.2  原子位操作API

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位原来的值。

2. 自旋锁

2.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;

2.2  自旋锁API函数

自旋锁API 函数适用于多核并发 或支持抢占的单CPU 下线程之间的并发访问,也就是用于线程与线程之间;被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。

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。

使用示例:

spinlock_t lock;

spin_lock_init(&lock);

spin_lock(&lock);

spin_unlock(&lock);

2.2.1  中断场景中使用

为了避免死锁在中断函数中,每次获取自旋锁的时候,要先关闭本地中断。

一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore;

在中断中使用 spin_lock/spin_unlock。

void spin_lock_irq(spinlock_t *lock)禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock)激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long 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)打开下半部,并释放自旋锁。

2.2.2  读、写自旋锁

读写自旋锁为读和写操作提供了不同的锁;

一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。

但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。

typedef struct raw_spinlock {

    arch_spinlock_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

} raw_spinlock_t;

 2.2.3  顺序锁

        顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。

 3. 信号量

3.1  概念

信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开 销要远大于信号量带来的那点优势。

信号量有一个信号量值,信号量值为1的称为二值信号量。

信号量结构体定义在linux/spinlock.h

struct semaphore {

    raw_spinlock_t      lock;

    unsigned int        count;

    struct list_head    wait_list;

};

3.2  信号量API

DEFINE_SEAMPHORE(name)定义一个信号量,并且设置信号量的值为1。
void sema_init(struct semaphore *sem, int val)初始化信号量sem,设置信号量值为 val。
void down(struct semaphore *sem)获取信号量,因为会导致休眠,因此不能在中断中使用。
int down_trylock(struct semaphore *sem)尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。
int down_interruptible(struct semaphore *sem)获取信号量,和down类似,只是使用 down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem)释放信号量。

示例:

struct semaphore sem;

sema_init(&sem, 10);

down(&sem);

up(&sem);

4. 互斥体

4.1 概念

将信号量的值设置为1就可以达到互斥体的效果,Linux中有个专门的互斥体操作。

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

互斥体可以导致休眠,因此不能在中断中使用 互斥体,中断中只能使用自旋锁。

互斥体的结构体定义在linux/mutex.h

struct mutex {

    /* 1: unlocked, 0: locked, negative: locked, possible waiters */

    atomic_t        count;

    spinlock_t      wait_lock;

    struct list_head    wait_list;

};

4.2 互斥体API

DEFINE_MUTEX(name)定义并初始化一个mutex变量。
void mutex_init(mutex *lock)初始化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)使用此函数获取信号量失败进入休眠以后可
以被信号打断。

总结

并发的问题在多线程应用中很常见,需要做好共享数据段的访问处理。另外该文章是对正点原子的Linux驱动开发相关章节的归纳总结。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值