文章链接:
从零开始学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共享资源的操作,所以不需要持有自旋锁。其他可能的情况请读者自行分析。
下面是用于测试的命令。
总结一下驱动中解决竞态的问题
- 找出驱动中的共享资源,比如available和接收FIFO。
- 考虑在驱动中的什么地方有并发的可能、是何种并发,以及由此引起的竞态。
比如例子中的vser_open、 vser_ release、vser_ read, vser_ poll 和vser_ work.其中, vser_ open、vser_ release 在打开和关闭设备的时候可能会产生竞态,而其他的则发生在对接收FIFO的访问上。 - 使用何种手段互斥。
互斥的手段有很多种,我们应该尽量选用一些简单的、开销小的互斥手段,当该互斥手段受到某条使用规则的制约后再考虑其他的互斥手段。
当然,对于一个复杂的驱动,刚开始时我们不一定 就能有一个全局的认识,很多认识是逐渐产生的。但是我们应该有这个意识,当问题刚引入的时候就应该考虑它的解决方案,事后再进行处理往往会比较麻烦。
剩余内容见顶部链接