Linux之并发竞争管理

Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。

产生并发的主要原因有:

①多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因

②抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。

③中断程序并发访问,硬件中断的权利可以是很大的。

④SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。

并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。微观串行,宏观并行:理解成把时间轴放大以后,各个任务是串行执行,然后每个任务执行一定的时间片,执行完后由调度系统调度到另一个任务去执行。因为CPU的速度很快,所以宏观看来是并行执行。 

竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。

临界资源: 多个进程访问的资源,共享数据段

临界区:多个进程访问的代码段

 

一、原子操作

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

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

typedef struct {
    int counter;
} atomic_t;

如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,

atomic_t a; //定义 a
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

原子变量有了,接下来就是对原子变量进行操作,读、写、增加、减少。

原子操作API函数

 如果使用 64 位的 SOC 的话,就使用64 位的原子变量

typedef struct {
    long long counter;
} atomic64_t;

 相应的操作函数把“atomic_”前缀换为“atomic64_”,将 int 换为 long long即可。

原子位操作:即位操作,在Linux 内核也提供了一系列的原子位操作 API 函数,原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作

 

atomic_t lock;			/* 原子变量 */

/* 初始化原子变量 */
atomic_set(&lock, 1);	/* 原子变量初始值为1 */

atomic_inc(&lock); /*释放原子变量*/


/* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:1 */
//若为1 则1-1为0 ,返回真,取反为假
//若为0 ,则0-1不为0,返回假,取反为真,执行下面
if (!atomic_dec_and_test(&gpioled.lock)) {
	atomic_inc(&gpioled.lock);	/* 小于0的话就加1,使其原子变量等于0 */
	return -EBUSY;				/* 共享资源被使用,返回忙 */
}



/* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:2 */
if(atomic_read(&lock) <= 0)
{
    return -EBUSY;
}
else
{
    atomic_dec(&lock);
}

二、自旋锁 

原子操作只能对整形变量或者位进行保护,不能运用到太复杂的环境中。如若共享数据为设备结构体,对于结构体中成员变量的操作要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,此时可以使用自旋锁解决。

当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁
 

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;

在使用自旋锁之前,肯定要先定义一个自旋锁变量

spinlock_t lock; //定义自旋锁

定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁,加锁或者释放锁。

自旋锁API函数


 

自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则可能会导致死锁现象自旋锁会自动禁止抢占,当线程 A得到锁以后会暂时禁止内核抢占

如果线程 A 在持有锁期间进入了休眠状态,线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止。线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,产生死锁现象。

如果在自旋锁加锁的临界区中使用中断,并且中断也会去访问共享资源,也可能会发送死锁现象。



在中断里面是可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。

获取锁之前关闭本地中断,相关自旋锁API

 使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,很难确定某个时刻的中断状态,因此不推荐使用。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,这一组函数会保存中断状态,在释放锁的时候会恢复中断状态

 一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore

中断中使用spin_lock/spin_unlock

 

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */

/* 线程 A */
void functionA ()
{
	unsigned long flags; /* 中断状态 */
	spin_lock_irqsave(&lock, flags) /* 获取锁 */
	/* 临界区 */
	spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}

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

 注意自旋锁使用事项:

锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式:信号量、互斥体。

②自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则可能导致死锁。

③不能递归申请自旋锁,否则可能导致死锁。

三、信号量

Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

信号量的特点

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

②信号量会引起休眠,中断不能休眠,所以信号量不能用于中断

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

示例:假设一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。可以通过信号量来控制访问共享资源的访问数量。如果要想进房间,需要先获取一把钥匙,信号量值减 1,直到 10 把钥匙被拿走,信号量值为 0,这时不允许任何人进入房间。如果有人从房间出来,并归还钥匙,信号量值加 1,现在可以允许进去一个人。

通过信号量控制访问资源的线程数,在初始化的时将信号量值设置的大于 1信号量是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源

如果要互斥的访问共享资源,则信号量的值小于等于1,此时信号量称为二值信号量
 

Linux 内核使用 semaphore 结构体表示信号量
 

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

使用信号量①先定义信号量②初始化信号量

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

信号量常用API

四、互斥体 

将信号量的值设置为 1 就可以使用信号量进行互斥访问。但是 Linux 提供了专门的互斥体mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。

Linux 内核使用 mutex 结构体表示互斥体

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

互斥体使用注意事项:

①mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁
②mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)

③因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。
且 mutex 不能递归上锁和解锁

互斥体常用API

 

struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */

 



 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值