Linux下的并发与竞争


前言

Linux系统是个多任务操作系统,并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,要保证临界区是原子访问的。主要方法有四种:原子操作,自旋锁,信号量,互斥体。本文主要介绍内核下各方式的常用API及基本用法,帮助大家快速上手。


一、原子操作

原子操作就是指不能在进一步分割的操作,原子操作只能对整形变量或者位进行保护。
1.原子整形操作:
typedef struct {
int counter;
} atomic_t;

操作API如下,对于32位的系统,把接口名的"atomic64_"换成"atomic_"即可。

ATOMIC64_INIT(int i) 定义原子变量的时候对其初始化。
int atomic64_read(atomic64_t *v) 			读取 v 的值,并且返回。
void atomic64_set(atomic64_t *v, int i) 	向 v 写入 i 值。
void atomic64_add(int i, atomic64_t *v) 	给 v 加上 i 值。
void atomic64_sub(int i, atomic64_t *v) 	从 v 减去 i 值。
void atomic64_inc(atomic64_t *v) 			给 v 加 1,也就是自增。
void atomic64_dec(atomic64_t *v) 			从 v 减 1,也就是自减
int atomic64_dec_return(atomic64_t*v) 	从 v 减 1,并且返回 v 的值。
int atomic64_inc_return(atomic64_t *v) 	给 v 加 1,并且返回 v 的值。
int atomic64_sub_and_test(int i,atomic64_t *v)	从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic64_dec_and_test(atomic64_t*v)			从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic64_inc_and_test(atomic64_t*v)			给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic64_add_negative(int i,atomic64_t *v)	给 v 加 i,如果结果为负就返回真,否则返回假

示例:
atomic_t a = ATOMIC_INT(3);
atomic64_set(&a,10);	a赋值为10
atomic64_read(&a);		读出a的值

原子位操作API:
void set_bit(int nr, void *p) 		将 p 地址的第 nr 位置 1void 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 <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/of_address.h>
#include <linux/of.h>
#include <linux/of_gpio.h>

#define LED_NAME	"gpio_init"
#define LED_MAJOR	202
#define LED_MINOR	0

struct led_dev {
	struct class * fclass;
	struct device * fdevice;
	struct cdev * cdev;
	dev_t dev_num;
	struct device_node * nd; 
	int major;
	int minor;
	int gpio_led;
	atomic64_t lock;
};

struct led_dev led = {0};

int led_open (struct inode *a, struct file *b)
{
	int val = 0;
	printk("lzw led open !!!\n");
	if(atomic64_sub_and_test(1,&led.lock) == 0)
	{
		val = atomic64_inc_return(&led.lock);	//减了1要加回来
		printk("lzw led atomic has been open, now failed, val = %d !\n",val);
		
		return -EBUSY;
	}
	else
		printk("lzw led atomic open sueccess !\n");
	
	b->private_data = &led;
	return 0;
}

int led_close (struct inode *a, struct file *b)
{
	atomic64_inc(&led.lock);
	printk("lzw led atomic close, atomic lock +1 sueccess !\n");
	return 0;
}

struct file_operations fop = 
{
	.owner = THIS_MODULE,
	.open = led_open,
	.release = led_close,
};


int __init led_init(void)
{
	led.cdev = cdev_alloc();
	led.dev_num = MKDEV(LED_MAJOR,LED_MINOR);
	register_chrdev_region(led.dev_num, 1, LED_NAME);
	cdev_init(led.cdev,&fop);
	cdev_add(led.cdev,led.dev_num,1);
	
	led.fclass = class_create(THIS_MODULE, "led_class");
	led.fdevice = device_create(led.fclass, NULL, led.dev_num, NULL, "led_device");
	
	led.major = LED_MAJOR;
	led.minor = LED_MINOR;

	atomic64_set(&led.lock,1);

	printk("lzw led init success, atomic lock = %lld ~~~\n",atomic64_read(&led.lock));

	return 0;
}

void __exit led_exit(void)
{
	device_destroy(led.fclass,led.dev_num);
	class_destroy(led.fclass);
	cdev_del(led.cdev);
	unregister_chrdev(LED_MAJOR, LED_NAME);

	return;
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

应用程序代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#define LED_IOCTL   	_IO('T',0)
#define LED_UNDEFINE   	_IO('T',1)

struct led_dev {
	struct class * fclass;
	struct device * fdevice;
	struct cdev * cdev;
	dev_t dev_num;
	struct device_node * nd; 
	int major;
	int minor;
};

int main(void)
{
	int fd = open("/dev/led_device",O_RDWR);
	if(fd > 0)
		printf("open 1 success \n");
	else
		printf("open 1 failed \n");
	
	int fd2 = open("/dev/led_device",O_RDWR);
	if(fd2 > 0)
		printf("open 2 success \n");
	else
		printf("open 2 failed \n");
		
	if(fd>0)
		close(fd);

	if(fd2>0)
		close(fd2);
	
	return 0;
}

加载驱动及运行结果如下:
在这里插入图片描述
这样就能保证该节点只被一个应用程序打开使用,后面的方法效果及原理也和此类似,就不讲得那么详细了。

二、自旋锁

自旋锁适用于短时期的轻量级加锁,等待锁的线程会一直处于自旋状态(会浪费处理器时间)。

用结构体spinlock_t表示自旋锁
spinlock_t lock;

基本操作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

注意,当线程获得锁,执行中被中断抢走cpu使用权,中断获取不到锁一直自旋,线程A执行不了,放不了锁,死锁发生

线程A 					中断
spin_lock(&lock)	|-->spin_lock(&lock)
spin_unlock(&lock)--|	functionB
functionA()				spin_unlock(&lock)

最好解决方法就是关闭本地中断:
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)	将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

我们是很难确定某个时刻的中断状态,因此不推荐使用 spin_lock_irq/spin_unlock_irq
一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock。自旋锁使用注意事项:
1.锁的持有时间不能太长,否则会降低系统性能
2.不能调用任何可能导致线程休眠的API,否则可能死锁
3.不能递归申请自旋锁
4.必须考虑可移植性

驱动如下,真正实现设备互斥访问的是变量 dev_stats,但是要使用自旋锁对 dev_stats 来做保护:

struct led_dev {
	...
	int dev_stat;	//以此作为计数值判断打开程序有几个
	spinlock_t lock;
};

int led_open (struct inode *a, struct file *b)
{
	unsigned long  flags = 0;
	int ret = 0;
	printk("lzw led spinlock open !!!\n");
	
	spin_lock_irqsave(&led.lock,flags);
	
	if(led.dev_stat != 0)
	{
		printk("lzw led spin_lock has been open, now failed !\n");
		ret = -1;
	}
	else
	{
		printk("lzw led spin_lock open sueccess !\n");
		led.dev_stat++;
		b->private_data = &led;
	}
	
	spin_unlock_irqrestore(&led.lock,flags);
	
	return ret;
}

int led_close (struct inode *a, struct file *b)
{
	unsigned long flags = 0;
	spin_lock_irqsave(&led.lock,flags);
	if(led.dev_stat)
		led.dev_stat--;
	spin_unlock_irqrestore(&led.lock,flags);
	
	printk("lzw led spin_lock close +++ sueccess !\n");
	return 0;
}

int __init led_init(void)
{
	...
	led.dev_stat = 0;
	spin_lock_init(&led.lock);

	printk("lzw led init spinlock success ~~~\n");

	return 0;
}

测试用例:

int main(void)
{
	int fd = open("/dev/led_device",O_RDWR);
	if(fd > 0)
		printf("open 1 success \n");
	else
		printf("open 1 failed \n");
	
	int fd2 = open("/dev/led_device",O_RDWR);
	if(fd2 > 0)
		printf("open 2 success \n");
	else
		printf("open 2 failed \n");

	if(fd>0)
		close(fd);

	if(fd2>0)
		close(fd2);
	
	return 0;
}

运行结果如下:
在这里插入图片描述

三、读写自旋锁

一次只允许一个写操作,不能有读;读的时候可以多人并发的读

typedef struct {
	arch_rwlock_t raw_lock;
} rwlock_t;

API参考上面自旋锁:
void read_lock(rwlock_t *lock)	获取读锁
void write_lock(rwlock_t *lock) 获取写锁。

顺序锁:可以写操作时进行读操作,保护的资源不能是指针(同时读写可能崩溃)
typedef struct {
struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

void write_seqlock(seqlock_t *sl) 获取写顺序锁。

四、信号量

同步的一种方式
1.可以使等待线程进入休眠状态,适合占用资源久的场合
2.不能用于中断中,因为会引起休眠
3.共享资源持有时间短,不适合
结构体如下:

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

DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为 1void 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, 1); 	/* 初始化信号量 */
down(&sem);			 	/* 申请信号量 */
/* 临界区 */
up(&sem); 				/* 释放信号量 */

驱动:

int led_open (struct inode *a, struct file *b)
{

	printk("lzw led semphore open !!!\n");
	down_interruptible(&led.sem);	//如果获取不到进入休眠状态,sem大于等于1时会被唤醒

#if 0
	down(&sem);
#endif
	
	return 0;
}

int led_close (struct inode *a, struct file *b)
{
	up(&led.sem);
	
	printk("lzw led semphore close +++ sueccess !\n");
	return 0;
}

int __init led_init(void)
{
	...
	sema_init(&led.sem,1);
}

测试用例test:

	int fd = open("/dev/led_device",O_RDWR);
	if(fd > 0)
		printf("open 1 success \n");
	else
		printf("open 1 failed \n");
	
	for(int i=0;i<10;i++)
	{
		sleep(1);
		printf("led 1 on ~\n");
	}

	if(fd>0)
		close(fd);

测试用例test2:

	int fd2 = open("/dev/led_device",O_RDWR);
	if(fd2 > 0)
		printf("open 2 success \n");
	else
		printf("open 2 failed \n");

	for(int i=0;i<5;i++)
	{
		sleep(1);
		printf("led 2 on ~\n");
	}

	if(fd2>0)
		close(fd2);

先运行test,再运行test2,可发现因为test已经占用打开了/dev/led_device,导致test2只能阻塞在open,等test关闭之后才能继续运行。
在这里插入图片描述

五、互斥体

互斥体,也叫互斥锁。虽然可以通过信号量为1实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥。和二值信号量类似,只不过互斥体专门用于互斥访问。
注意:
1.mutex会导致休眠,所以不能在中断用,中断只能用自旋锁
2.和信号量一样,mutex保护的临界区可以调用引起阻塞的API
3.一次只有一个线程可以持有mutex,必须由mutex持有者释放mutex

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

DEFINE_MUTEX(name) 						定义并初始化一个 mutex 变量。
void mutex_init(struct 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,如果失败就返回 0int mutex_is_locked(struct mutex*lock)	判断 mutex 是否被获取,如果是的话就返回 1,否则返回 0int mutex_lock_interruptible(structmutex *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断。

struct mutex lock;
mutex_init(&lock);
mutex_lock(&lock);
/* 临界区 */
mutex_unlock(&lock);

驱动:


struct led_dev {
	...
	struct mutex lock;
};

int led_open (struct inode *a, struct file *b)
{
	printk("lzw led mutex open !!!\n");
	if(mutex_lock_interruptible(&led.lock))//可被信号打断,失败进入休眠状态
		return -1;

#if 0
	mutex_lock(&led.lock);	
#endif

	return 0;
}
int led_close (struct inode *a, struct file *b)
{
	mutex_unlock(&led.lock);
	
	printk("lzw led mutex close +++ sueccess !\n");
	return 0;
}
int __init led_init(void)
{
	...
	mutex_init(&led.lock);
}

测试用例及效果与第四节信号量的一致。


总结

以上就是今天要讲的内容,本文简单介绍了Linux内核下的并发与竞争,常用的API,用法以及测试案例。制作不易,多多包涵。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值