内核中的互斥与同步

文章链接:
从零开始学Linux驱动–(9)内核中的互斥与同步

原子变量补充:

原子变量操作的API:函数如下:
罗列一些:

atomic_read(v)
atomic_set(v, i)
int atomic_add_return(int i, atomic_t *v);
int atomic sub_ return(int i, atomic_t*v);
int atomic add negative(int i, atomic_t*v)
void atomic_add (int i, atomic_t *v);
void atomic_sub (int i, atomic_t *v);
void atomic_inc (atomie_t*v);
void atomic_dec (atomic_t *v);
atomic_dec_return(v)
atomic_inc_return(v)
atomic_sub_and_test(i,v)
atomic_dec_and_test(v)
atomic_inc_and_test(v)
atomic_xchg(ptr, v)
atomic_cmpxchg(v, old, new)
void atomic_clear_mask (unsigned long mask,atomic_t *v);
void atomic_set_mask (unsigned int mask, atomic_t*v);
void set_bit(int nr, volatile unsigned long *addr);
void clear_bit(int nr, volatile unsigned long *addr);
void change_bit(int nr, volatile unsigned long *addr);
int test_and_set_bit(int nr, volatile unsigned long *addr);
int test_and_clear_bit(int nr,volatile unsigned long *addr);
int test_and_change_bit(int nr,volatile unsigned long *addr);
  • atomic_read: 读取原子变量v的值
  • atomic_set(v,i):设置原子变量v的值为i
  • atomic_add、atomic_sub: 将原子变量加上i或减去i,加“_return”表示还要返回修改后的值,加“_negative”表示当结果为负返回真。
  • atomic_inc、atomic_dec: 将原子变量自加1或自减1,加“_return”表示还要返回修改后的值,加“_test”表示结果为0返回真。
  • atomic_xchg: 交换v和ptr指针指向的数据。
  • atomic_cmpxchg: 如果v的值和 old相等,则将v的值设为new,并返回原来的值。
  • atomic_clear_mask: 将v中mask为1的对应位清零。
  • atomic_set_mask: 将v中 mask为1的对应位置一。
  • set_bit、clear_bit、change_bit: 将nr位置一、清零或翻转,有test前缀的还要返回原来的值。

在i++的那个例子中,我们可以使用下面的代码来保证对它访问的原子性操作,第一行使用ATOMIC_INIT对原子变量赋初值:

atomic_t i =ATOMIC_INIT(5);
atomic_inc(&i);

需要注意的一点是,虽然原子变量的本质是一个整型变量,对于非整型变量就不能使用这一套方法来操作,而需要使用另外的方法。一般在能够使用原子变量的时候,尽可能的使用原子变量,而不使用复杂的锁机制。因为原子变量的开销更小。

自旋锁补充

理解自旋锁可以这样理解:

有这样一个不太优雅的例子: 一个公共的卫生间,很多人排队去方便,但是要进去就要先打开卫生间的门,进去后将门反锁,出来后再开锁。

很显然,在门被反锁的期间,其他人是进不去的,只有干着急,越等越急后,就急得团团转,于是就原地自旋了。

这个例子很能说明在内核中的另一种互斥手段一自 旋锁的特性,在访问共享资源(卫生间)之前,首先要获得自旋锁(卫生间门上的锁),访问完共享资源后解锁。其他内核执行路径(其他人)如果没有竞争到锁,只能忙等待,所以自旋锁是一种忙等锁。

它的类型和函数可以见上面链接文档

信号量补充

为了更好的了解信号量,看看down函数的实现代码:

/* kernel/locking/semaphore.c*/
void down(struct semaphore *sem)
{
	unsigned long flags;

	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		__down(sem);
	raw_spin_unlock_irqrestore(&sem->lock, flags);
}
.....
static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
list_add_tail(&waiter.list, &sem->wait_list);
.....
	timeout = schedule_timeout(timeout);
.....
}

static noinline void __sched __down(struct semaphore *sem)
{
	__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

代码

raw_spin_lock_irqsave(&sem->lock, flags);

首先获取了保护count成员的自旋锁

	if (likely(sem->count > 0))
		sem->count--;
	else
		__down(sem);

,如果count的值大于0.则将count的值自减1.然后释放自旋锁立即返回。

否则调用_down,_down 又调用了_down_common

static noinline void __sched__down(struct semaphore *sem)
{
	__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

准备将进程的状态切换为TASK_UNINTERRUPTIBLE, 表示不能被信号唤醒。

代码

static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
list_add_tail(&waiter.list, &sem->wait_list);
.....
	timeout = schedule_timeout(timeout);
.....
}

_down_common 调用list_add_tail 将进程放到信号量的等待链表中

然后在代码

timeout = schedule_timeout(timeout);

调用调度器,进程主动放弃CPU,调度其他进程执行。

很显然,进程在不能获得信号量的情况下会休眠,不会忙等待,从而适用于临界代码段运行时间比较长的情况。

下面来讲一种特殊的情况,当信号量初值为1的时候,则表示在同一时刻只能有一个进程获得信号量,这种信号量叫二值信号量

显然,根据这个性质我们就可以用它来做互斥。

其实现也很简单,就是调用dema_init函数的时候指定val为1。

典型的应用代码如下:

/*定义信号量*/
struct semaphore sem;
/*初始化信号量,赋初值为1,用于互斥*/
sema_init(&sem,1);
/*获取信号量,如果是被信号唤醒,则返回-ERESTARTSYS*/
if(down_interruptible(&sem);
return -ERESTARTSYS;
/*对共享资源进行访问,执行一些耗时的引起进程调度的操作*/
xxxx;
/*共享资源完成后,释放信号量*/
up(&sem);

RCU机制

在这里插入图片描述
下面使用简单的代码来完成一个RCU机制的示例

struct foo{
    int a;
    char b;
    long c;
};

DEFIEN_SPINLOCK(foo_mutex);

struct foo *gbl_foo;

void foo_update_a(int new_a)
{
    struct foo *new_fp;
    struct foo *old_fp;
    
    new_fp = kmalloc(sizeof(*new_fp),GFP_KERNEL);	//在内存的其他位置重新分配
    spin_lock(&foo_mutex);
    old_fp = gbl_foo;
    *new_fp = *old_fp;	//赋值原共享资源到新的内存空间
    new_fp->a = new_a;
    rcu_assign_pointer(gbl_foo,new_fp);	//新指针更新老指针
    spin_unlock(&foo_mutex);
    synchronize_rcu();
    kfree(old_fp);	
}

int foo_get_a(void)
{
    int retval;
    
    rcu_read_lock();
    retval = rcu_dereference(gbl_foo)->a;	//读者获取共享资源
    rcu_read_unlock();
    return retval;
}

代码

struct foo{
    int a;
    char b;
    long c;
};

是一个共享资源的数据类型定义。

代码

DEFIEN_SPINLOCK(foo_mutex);

定义了一个用于写保护的自旋锁。

代码

struct foo *gbl_foo;

定义了一个指向共享资源数据的全局指针

代码

  new_fp = kmalloc(sizeof(*new_fp),GFP_KERNEL);	

分配一片新的内存

代码

 old_fp = gbl_foo;

保存原来的指针

代码

   *new_fp = *old_fp;	

数据复制,即将原来内存中的数据复制到新的内存中

代码

 new_fp->a = new_a;

完成对新内存中数据的修改(a)

代码

 rcu_assign_pointer(gbl_foo,new_fp);	

用新的指针new_fp更新了原来的指针gbl_foo

在这之后不能立即释放原来的指针所指向的内存,因为可能还有读者在使用原来的指针访问共享资源的数据

所以在代码

 synchronize_rcu();

等待使用原来指针的读者,当所有使用原来的指针的读者都读完数据后

代码

  kfree(old_fp);	

释放原来的指针所指向的内存。在数据更新和指针更新时使用了自旋锁进行保护。

代码

 rcu_read_lock();

是读者进入临界区

代码

 rcu_read_lock();
    retval = rcu_dereference(gbl_foo)->a;	//读者获取共享资源
}

使用rcu_dereference获取共享资源的内存指针后进行解引用,获取相关的数据。

访问完成后,代码

  rcu_read_unlock();

退出临界区。

虚拟串口驱动加入互斥

前面的虚拟串口驱动没有考虑并发可能导致的竞态。

为了方便说明问题,我们将之前的虚拟串口的运行机制稍加修改。

首先,一个类似串口的设备应该具有排他访问属性,即不能同时有多个进程都能打开串口并操作串口。

在一段时间内,只允许一个进程操作串口,直到该进程关闭该串口为止,在这段时间内,其他的进程都不能打开该串口,这也是实际的串口常规属性。

其次,写给串口的数据不再环回,为简化问题,我们仅仅是把用户发来的数据简单地丢弃,认为数据是无等待地发送成功,那么写方向上也就不再需要等待队列。

最后,串口接收中断还是通过网卡中断来产生,并将随机产生的接收数据放入接收FIFO中。

针对串口功能的重新定义,我们来考虑驱动中的并发问题。
首先,应该安排一个变量来表示当前串口是否可用的状态

当有一个进程已经成功打开串口,那么这将阻止其他的进程再打开串口,很显然,这个反映状态的变量是一个共享资源,当多个进程同时打开这个串口设备时,可能会产生竞态,应该对该共享资源做互斥处理

其次,写入的数据是简单丢弃(复制到一个局部的缓冲区中,非共享资源),所以不考虑并发。但是,当接收中断产生时,在中断或中断下半部处理中需要将数据写入接收FIFO,这就存在着针对接收FIFO的读一写并发,即用户进程读取接收FIFO,同时在中断或中断的下半部写数据到接收FIFO共享资源就是这个全局的接收FIFO,针对该共享资源应该提供互斥的访问。

根据上面的分析,相应的驱动代码如下:

struct vser_dev {
.....
	atomic_t available;	
.....
};

static int vser_open(struct inode *inode, struct file *filp)
{
	if (atomic_dec_and_test(&vsdev.available))
		return 0;
	else {
		atomic_inc(&vsdev.available);
		return -EBUSY;
	}
}
static int vser_release(struct inode *inode, struct file *filp)
{
	vser_fasync(-1, filp, 0);
	atomic_inc(&vsdev.available);
	return 0;
}
static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	int ret;
	int len;
	char tbuf[VSER_FIFO_SIZE];

	len = count > sizeof(tbuf) ? sizeof(tbuf) : count;
	spin_lock(&vsdev.rwqh.lock);
	if (kfifo_is_empty(&vsfifo)) {
		if (filp->f_flags & O_NONBLOCK) {
			spin_unlock(&vsdev.rwqh.lock);
			return -EAGAIN;
		}
if (wait_event_interruptible_locked(vsdev.rwqh, !kfifo_is_empty(&vsfifo))) {
			spin_unlock(&vsdev.rwqh.lock);
			return -ERESTARTSYS;
		}
	}
	len = kfifo_out(&vsfifo, tbuf, len);
	spin_unlock(&vsdev.rwqh.lock);

	ret = copy_to_user(buf, tbuf, len);
	return len - ret;
}
static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{

	int ret;
	int len;
	char *tbuf[VSER_FIFO_SIZE];

	len = count > sizeof(tbuf) ? sizeof(tbuf) : count;
	ret = copy_from_user(tbuf, buf, len);

	return len - ret;
}
static unsigned int vser_poll(struct file *filp, struct poll_table_struct *p)
{
	int mask = POLLOUT | POLLWRNORM;

	poll_wait(filp, &vsdev.rwqh, p);

	spin_lock(&vsdev.rwqh.lock);
	if (!kfifo_is_empty(&vsfifo))
		mask |= POLLIN | POLLRDNORM;
	spin_unlock(&vsdev.rwqh.lock);

	return mask;
}
static void vser_work(struct work_struct *work)
{
	char data;

	get_random_bytes(&data, sizeof(data));
	data %= 26;
	data += 'A';

	spin_lock(&vsdev.rwqh.lock);
	if (!kfifo_is_full(&vsfifo))
		if(!kfifo_in(&vsfifo, &data, sizeof(data)))
			printk(KERN_ERR "vser: kfifo_in failure\n");

	if (!kfifo_is_empty(&vsfifo)) {
		spin_unlock(&vsdev.rwqh.lock);
		wake_up_interruptible(&vsdev.rwqh);
		kill_fasync(&vsdev.fapp, SIGIO, POLL_IN);
	} else
		spin_unlock(&vsdev.rwqh.lock);
}

static int __init vser_init(void)
{
.....
atomic_set(&vsdev.available, 1);
.....
}

代码atomic_t available;添加了一个available原子变量成员,用于表示当前设备是否可用。

代码atomic_set(&vsdev.available, 1); 使用atomic_set将原子变量设置为1,表示可用

代码

static int vser_open(struct inode *inode, struct file *filp)
{
	if (atomic_dec_and_test(&vsdev.available))
		return 0;

代码使用atomic_dec_and_test,先将available的值减1,然后判断减1后的结果是否为0。如果是0,表示真,表示设备是首次被打开,返回0表示打开成功
这个函数是原子操作,一直到最后,即不会被打断。所以即使有另外一个进程想打开串口,二者之中只有一个能竞争成功。

代码

else {
		atomic_inc(&vsdev.available);
		return -EBUSY;
	}

表示竞争失败的进程应该把available的值加回来,否则以后串口将永远无法打开。

代码

static int vser_release(struct inode *inode, struct file *filp)
{
	vser_fasync(-1, filp, 0);
	atomic_inc(&vsdev.available);
	return 0;
}

竞争成功,在使用完串口后,将available加回来,使串口又可用。

代码

static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	int ret;
	int len;
	char tbuf[VSER_FIFO_SIZE];

	len = count > sizeof(tbuf) ? sizeof(tbuf) : count;
	spin_lock(&vsdev.rwqh.lock);
	if (kfifo_is_empty(&vsfifo)) {
		if (filp->f_flags & O_NONBLOCK) {
			spin_unlock(&vsdev.rwqh.lock);
			return -EAGAIN;
		}

		if (wait_event_interruptible_locked(vsdev.rwqh, !kfifo_is_empty(&vsfifo))) {
			spin_unlock(&vsdev.rwqh.lock);
			return -ERESTARTSYS;
		}
	}

	len = kfifo_out(&vsfifo, tbuf, len);
	spin_unlock(&vsdev.rwqh.lock);

	ret = copy_to_user(buf, tbuf, len);
	return len - ret;
}

是用户读操作的驱动实现。

其中代码

	len = count > sizeof(tbuf) ? sizeof(tbuf) : count;

首先修正了读取的字节数,然后

	spin_lock(&vsdev.rwqh.lock);

使用了读等待队列头中的自带的自旋锁,进行加锁操作。因为之后都要对共享的接收FIFO进行操作(包括判空)。

代码

if (kfifo_is_empty(&vsfifo)) {
		if (filp->f_flags & O_NONBLOCK) {
			spin_unlock(&vsdev.rwqh.lock);
			return -EAGAIN;
		}

		if (wait_event_interruptible_locked(vsdev.rwqh, !kfifo_is_empty(&vsfifo))) {
			spin_unlock(&vsdev.rwqh.lock);
			return -ERESTARTSYS;
		}
	}

如果接收FIFO为空,并且是非阻塞操作,那么应该释放自旋锁,然后返回-EAGAIN,如果不释放自旋锁将会导致重复获取自旋锁的错误。

应该注意:如果接收FIFO为空,并且是阻塞操作,那么调用wait event interruptible_ locked 来使进程休眠,当接收FIFO不为空时,进程被唤醒。

之前我们说过,在自旋锁获得期间,不能调用可能会引起进程切换的函数,但是这里用的是wait_event interruptible_ locked, 在进程休眠前,会自动释放自旋锁,醒来后将重新获得自旋锁,这就非常巧妙地对接收FIFO的判空实现原子化操作,从而避免了竞态。

需要注意:代码

len = kfifo_out(&vsfifo, tbuf, len);
spin_unlock(&vsdev.rwqh.lock);

将FIFO中的数据读出放入一个临时的缓冲区中,然后释放自旋锁。

这里没有使用kifo_to_user,是因为该函数可能会导致进程切换,在自旋锁持有期间不能被调用。

所以直到代码

ret = copy_to_user(buf, tbuf, len);
	return len - ret;

才使用copy_to_user 将读取到的数据复制到用户空间。

接下来

代码

spin_lock(&vsdev.rwqh.lock);
	if (!kfifo_is_empty(&vsfifo))
		mask |= POLLIN | POLLRDNORM;
	spin_unlock(&vsdev.rwqh.lock);

是对FIFO的判断空做互斥,避免这段时间的竞态。

代码

static void vser_work(struct work_struct *work)
{
	char data;

	get_random_bytes(&data, sizeof(data));
	data %= 26;
	data += 'A';

	spin_lock(&vsdev.rwqh.lock);
	if (!kfifo_is_full(&vsfifo))
		if(!kfifo_in(&vsfifo, &data, sizeof(data)))
			printk(KERN_ERR "vser: kfifo_in failure\n");

	if (!kfifo_is_empty(&vsfifo)) {
		spin_unlock(&vsdev.rwqh.lock);
		wake_up_interruptible(&vsdev.rwqh);
		kill_fasync(&vsdev.fapp, SIGIO, POLL_IN);
	} else
		spin_unlock(&vsdev.rwqh.lock);
}

这里中断下半部的工作队列的实现,同样也是在操作接受FIFO的整个过程中,通过自旋锁来保护。

其中代码 spin_unlock(&vsdev.rwqh.lock);
在接受FIFO访问成功后立即释放了自旋锁,尽量避免长时间持有自旋锁。

我们来讨论一下上面的并发情况,如果在用户读期间下半部开始执行了,那么下半部会因为不能获取自旋锁而忙等待。直到wait_event interruptible_ locked 被调用,在真正的进程切换之前,释放了自旋锁。

下半部将会立即获得自旋锁,从而完成对接收FIFO的完整操作,操作完成后,释放自旋锁并唤醒读进程,读进程醒来后马上获取自旋锁,在确定接收FIFO不为空的情况下,在自旋锁持有的过程中将接收FIFO的数据读出,即便这时中断下半部又执行,也会因为不能获得自旋锁而忙等待,所以竞态不会产生。

读取了接收FIFO中的数据后,再释放自旋锁,之后将数据复制到用户空间,因为不涉及接收FIFO共享资源的操作,所以不需要持有自旋锁。其他可能的情况请读者自行分析。

下面是用于测试的命令。

在这里插入图片描述

总结一下驱动中解决竞态的问题

  1. 找出驱动中的共享资源,比如available和接收FIFO。
  2. 考虑在驱动中的什么地方有并发的可能、是何种并发,以及由此引起的竞态。
    比如例子中的vser_open、 vser_ release、vser_ read, vser_ poll 和vser_ work.其中, vser_ open、vser_ release 在打开和关闭设备的时候可能会产生竞态,而其他的则发生在对接收FIFO的访问上。
  3. 使用何种手段互斥。
    互斥的手段有很多种,我们应该尽量选用一些简单的、开销小的互斥手段,当该互斥手段受到某条使用规则的制约后再考虑其他的互斥手段。

当然,对于一个复杂的驱动,刚开始时我们不一定 就能有一个全局的认识,很多认识是逐渐产生的。但是我们应该有这个意识,当问题刚引入的时候就应该考虑它的解决方案,事后再进行处理往往会比较麻烦。

剩余内容见顶部链接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值