Linux驱动程序之同步互斥阻塞

Linux驱动程序之同步互斥阻塞


有时候一个驱动程序,同一时刻只允许有一个用户程序能够打开,访问设备,否则会出错,那么怎么完成这种需求呢?这其实就是个多进程同步互斥的问题。将一个例子(载自韦东山视频)。

简单例子说明多进程互斥的基本方法

在按键驱动里面,要完成同一时刻只允许有一个用户程序能够打开设备的方法,最直观的做法
是定义在驱动程序的open函数加上
static int canopen = 1;

static int sixth_drv_open(struct inode *inode, struct file *file)
{
	if( --canopen! = 0 ){
		canopen++;
		return -EBUSY;
	}  
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}
...
int sixth_drv_close(struct inode *inode, struct file *file)
{
	//atomic_inc(&canopen);
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	up(&button_lock);
	return 0;
}

看这程序,当一个用户程序首先访问open函数的时候canopen会置为0,之后正常注册中断,而另一个用户程序
访问open函数时候,由于–canopen,此时canopen为-1了,那么就直接会返回,并且open函数会返回一个负值,就能实现同一个时刻只有一个进程访问驱动。

但实际上是不对的。
因为对于条件

--canopen! = 0

看上去一句话,其实在机器里面是分成很多个基本操作的,比如:

1读canopen的值到某个寄存器
2 寄存器值减1
3寄存器值跟0进行比较,改变标志位
4 寄存器值写回到canopen内存里面。

这学了汇编代码的,大体都能猜出这句话实际的汇编指令是什么。
而多进程切换,这种基本的操作,是不能够被中断掉(实际进程,线程的切换都是用软中断实现的)的,但是每条基本操作执行的间隙是可以切换进程的。

那么假如两个进程A,B同时访问驱动,A执行完步骤1的时候,此时寄存器的值为1,被打断,CPU切换执行进程B,B顺次执行完步骤3之后,判断就为0了,认为进程B取得了驱动的访问权,此时CPU又切换会进程A,A从步骤2开始执行,此时,寄存器的值为1.那么运行完步骤3的时候,也会等于0,也会被认为获得了驱动的访问权。这样就出错了,两个进程对于驱动的访问并没有做到绝对的互斥。

我的结论,上述代码,–canopen这句代码中间可以被中断,造成最后进程互斥失败。

解决这种竞态问题的途径是保证对共享资源的互斥访问。这样是可以保证多个进(线)程访问共享资源的时候,其他执行的单元被禁止。访问共享资源的代码区域称为临界区(critical sections),临界区需要某种互斥机制加以保护。Linux内核提供的机制有

  • 原子操作
  • 中断屏蔽
  • 自旋锁
  • 信号量

原子操作

原子操作指的就是在执行过程中不会被别的代码路径所中断的操作。
Linux内核提供了很多函数来实现内核中的原子操作。这些函数分为两类,分别针对位和整形变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核可以安全地调用它们而不被打断。位和整型变量都是依赖底层cpu的原子操作来实现的,因此这些函数都与架构有关。

//整型原子操作
//设置原子变量的值
void atomic_set(atomic_t *v, int i);//设置原子变量的值为iatomic_t v = ATOMIC_INIT(0);//定义原子变量v并初始化为0
//获取原子变量的值
atomic_read(atomic_t *v);//返回原子变量的值
//原子变量加/减
void atomic_add(int i, atomic_t *v);//原子变量增加i
void atomic_sub(int i, atomic_t *v);//原子变量减少i
//原子变量自增/自减
void atomic_inc(atomic_t *v);//原子变量增加i
void atomic_dec(atomic_t *v);//原子变量减少i

//操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
//上述操作对原子变量执行自增、自减、和减操作后测试其是否为0,为0则返回true,否则返回false。

//操作并返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
//上述操作对原子变量进行加、减和自增、自减操作,并返回新的值。

//位原子操作
//设置位
void set_bit(nr, void *addr);//设置addr地址的第nr位,所谓设置位即将位写为1
//清除位
void clear_bit(nr, void *addr);//清除addr地址的第nr位,将位写为0
//改变位
void change_bit(nr, void *addr);//对addr地址的第nr位进行反置
//测试位
Test_bit(nr, void *addr);//返回addr地址的第nr位
//测试并操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);

可以看到刚才整型的原子操作

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

刚才的程序,问题就出在–canopen,如果使用原子操作,让这句话不可被打断,那么就OK了。

atomic_t canopen = ATOMIC_INIT(1);

static int sixth_drv_open(struct inode *inode, struct file *file)
{   
/*
	if( --canopen! = 0 ){
		canopen++;
		return -EBUSY;
	}
*/
	if (!atomic_dec_and_test(&canopen) ){
		atomic_inc(&canopen);
		return -EBUSY
	}
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}
...
int sixth_drv_close(struct inode *inode, struct file *file)
{
	atomic_inc(&canopen);
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	//up(&button_lock);
	return 0;
}
把canopen定义为一个原子整形变量,而自减测试,以及自增都调用原子操作,就能保证关键代码不会被打断,也就两个进程用canopen的值形成了互斥效果。

我好奇了一下,就去内核代码里面看看了这些原子操作的源代码实现:

typedef struct { volatile int counter; } atomic_t;

#define ATOMIC_INIT(i)	{ (i) }
...
static inline int atomic_add_return(int i, atomic_t *v)
{
	unsigned long tmp;
	int result;

	__asm__ __volatile__("@ atomic_add_return\n"
"1:	ldrex	%0, [%2]\n"
"	add	%0, %0, %3\n"
"	strex	%1, %0, [%2]\n"
"	teq	%1, #0\n"
"	bne	1b"
	: "=&r" (result), "=&r" (tmp)
	: "r" (&v->counter), "Ir" (i)
	: "cc");

	return result;
}

可以看到原子操作是调用最底层的指令,来实现代码不被打断的效果,跟具体指令集有关,也就是为什么说原子操作跟架构相关

信号量

这个就是听的比较多的“加锁”的操作。

  • 定义一个信号量
  • 加锁
  • 解锁

这个锁在open函数出加锁,其他的进程到这里会休眠,直到release函数(关闭文件)的解锁。
在加锁的期间,其他进程访问这个锁的时候,会休眠,直到解锁。而且还有几种不同的加锁办法

  • down 这个锁一旦加了,另一个进程访问锁的时候,会进入一个不可被中断的休眠状态,
    用ps查看进程状态的时候是D,“僵死”状态
  • down_interruptible 这个锁上了以后,另一个进程会进入一个可以被中断的休眠
  • down_trylock 这个锁上了以后,另一个进程会试图访问该锁,如果没有获得锁的访问权,则会马上返回, 不休眠

几种加锁方法各有个的用处。
我思考一下,down这种方式,比较适合保护关键代码段,因为要处于僵死状态,这个时间越短越好,
比如刚才的那个代码,还是用canopen变量

static DECLARE_MUTEX(button_lock);     //定义互斥锁
...
	down(&button_lock);
	if( --canopen! = 0 ){
		canopen++;
		return -EBUSY;
	}
	up(&button_lock);

当然,直接全程锁住也是可以的,这样会阻塞掉另一个引用程序,让它一直休眠,直到解锁,计算外部给一个ctrl+c,也无法退出。

static int sixth_drv_open(struct inode *inode, struct file *file)
{   

	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	down(&button_lock);
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}
...

int sixth_drv_close(struct inode *inode, struct file *file)
{
	/*atomic_inc(&canopen);*/
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	up(&button_lock);
	return 0;
}

还有一种加锁方法是 down_interruptible。这种方式允许加上锁的时候,另一个程序处于休眠但可以被打断的状态,但是一旦该进程收到信号,那么就会从down_interruptible函数中返回。并标记错误号为:-EINTR。
这样的话,ctrl + c 肯定可以由响应,退出程序,如果用户程序已经定义了一个可以接受处理的信号,那么down_interruptible函数也会立即返回一个-EINTR的值。

还有一种用法是down_trylock,这种方法只有发现,锁处于锁上的状态就返回,当一个加锁时,马上会返回,整个用户进程不会阻塞。这种方式,跟上面只保护一个关键变量的方式相当,用户进程不会阻塞。

static int sixth_drv_open(struct inode *inode, struct file *file)
{
	if (down_trylock(&button_lock))//down_trylock,无锁状态就返回0,上锁状态返回1.
			return -EBUSY;
	}

	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

阻塞与非阻塞

阻塞操作,是指执行设备操作的时候若不能获得资源则挂起进程,直到可以满足操作的条件再进行操作
被挂起的进程进入休眠状态,被从调度器的的运行队列中被移走,直到等待的条件被满足。
非阻塞操作
进程在不能进行设备操作的时候并不挂起,它或者放弃或者不停的查询,直到可以操作为止。

阻塞这个概念,其实看到上面的论述,应该没有什么疑问了,简单的说,上面两段论述是针对用户程序来说的,调用open函数,如果另一个用户程序也在调用它,那么它直接返回,就是非阻塞式,如果程序休眠,一直等到另一个程序访问结束,再被唤醒访问,这成为非阻塞式,我们驱动层面要支持用户的两种不同的操作方式。自然,通过文件标志来确定,用户代码:

	fd = open( "...",O_RDWR | O_NOBLOCK);

驱动代码

static int sixth_drv_open(struct inode *inode, struct file *file)
{
#if 0	
	if (!atomic_dec_and_test(&canopen))
	{
		atomic_inc(&canopen);
		return -EBUSY;
	}
#endif		

	if (file->f_flags & O_NONBLOCK)
	{
		if (down_trylock(&button_lock))
			return -EBUSY;
	}
	else
	{
		/* 获取信号量 */
		down(&button_lock);
	}

	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

open的时候测试一下文件标志,是否存在O_NONBLOCK标志,如果存在,就说明用户想要一个非阻塞的式的
操作,使用downtrylock来加锁,否则使用down阻塞式加锁。

猜你喜欢

转载自blog.csdn.net/ronaldo_hu/article/details/90764740
今日推荐