Linux——互斥和同步(二)

news2024/11/30 8:47:19

目录

信号量

读写信号量

互斥量

RCU机制

虚拟串口驱动加入互斥

完成量

习题


信号量


        前面所讨论的锁机制都有一个限制,那就是在锁获得期间不能调用调度器,即不能引起进程切换。但是内核中有很多函数都可能会触发对调度器的调用(在中断的章节列举过一些),这给驱动开发带来了一些麻烦。另外,我们也知道,对于忙等锁来说,当临界代码段执行的时间比较长的时候,会降低系统的效率。为此内核提供了一种叫信号量的机制来取消这一限制,它的数据类型定义如下。


 

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


        可以看到,它有一个 count 成员,这是用来记录信号量资源的情况的,当 count 的值不为0时是可以获得信号量的,当 count 的值为0时信号量就不能被获取,这也说明信号量可以同时被多个进程所持有。我们还看到了一个 wait_list 成员,不难猜想,当信号量不能获取时,当前的进程就应该休眠了。最后,lock 成员在提示我们,信号量在底层其实使用了自旋锁的机制。信号量最常用的 API接口如下。


 

void sema_init(struct semaphore *sem,int val);
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout (struct semaphore *sem, long jiffies);
void up(struct semaphore *sem);


        sema_init:用于初始化信号量,val是赋给 count 成员的初值,这样就可以有 val个进程同时获得信号量。
        down:获取信号量(信号量的值减 1),当信号量的值不为0时,可以立即获取信号量,否则进程休眠。
        down_interruptible:同 down,但是能够被信号唤醒.

        down_trylock:只是尝试获取信号量,如果不能获取立即返回,返回0表示成功获取返回 1表示获取失败。
        down_timeout;同 down,但是在 jiffies 个时钟周期后如果还没有获取信号量,则超时返回,返回0表示成功获取信号量,返回负值表示超时。
        up:释放信号量(信号量的值加 1),如果有进程等待信号量,则唤醒这些进程。为了能更好地理解信号量,我们不妨来看看 down 函数的实现代码,下面的代码略做了改写。

/*
 * Copyright (c) 2008 Intel Corporation
 * Author: Matthew Wilcox <willy@linux.intel.com>
 *
 * Distributed under the terms of the GNU GPL, version 2
 *
 * This file implements counting semaphores.
 * A counting semaphore may be acquired 'n' times before sleeping.
 * See mutex.c for single-acquisition sleeping locks which enforce
 * rules which allow code to be debugged more easily.
 */

/*
 * Some notes on the implementation:
 *
 * The spinlock controls access to the other members of the semaphore.
 * down_trylock() and up() can be called from interrupt context, so we
 * have to disable interrupts when taking the lock.  It turns out various
 * parts of the kernel expect to be able to use down() on a semaphore in
 * interrupt context when they know it will succeed, so we have to use
 * irqsave variants for down(), down_interruptible() and down_killable()
 * too.
 *
 * The ->count variable represents how many more tasks can acquire this
 * semaphore.  If it's zero, there may be tasks waiting on the wait_list.
 */

#include <linux/compiler.h>
#include <linux/kernel.h>
#include <linux/export.h>
#include <linux/sched.h>
#include <linux/semaphore.h>
#include <linux/spinlock.h>
#include <linux/ftrace.h>

static noinline void __down(struct semaphore *sem);
static noinline int __down_interruptible(struct semaphore *sem);
static noinline int __down_killable(struct semaphore *sem);
static noinline int __down_timeout(struct semaphore *sem, long jiffies);
static noinline void __up(struct semaphore *sem);

/**
 * down - acquire the semaphore
 * @sem: the semaphore to be acquired
 *
 * Acquires the semaphore.  If no more tasks are allowed to acquire the
 * semaphore, calling this function will put the task to sleep until the
 * semaphore is released.
 *
 * Use of this function is deprecated, please use down_interruptible() or
 * down_killable() instead.
 */
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);
}
EXPORT_SYMBOL(down);

/**
 * down_interruptible - acquire the semaphore unless interrupted
 * @sem: the semaphore to be acquired
 *
 * Attempts to acquire the semaphore.  If no more tasks are allowed to
 * acquire the semaphore, calling this function will put the task to sleep.
 * If the sleep is interrupted by a signal, this function will return -EINTR.
 * If the semaphore is successfully acquired, this function returns 0.
 */
int down_interruptible(struct semaphore *sem)
{
	unsigned long flags;
	int result = 0;

	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		result = __down_interruptible(sem);
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}
EXPORT_SYMBOL(down_interruptible);

/**
 * down_killable - acquire the semaphore unless killed
 * @sem: the semaphore to be acquired
 *
 * Attempts to acquire the semaphore.  If no more tasks are allowed to
 * acquire the semaphore, calling this function will put the task to sleep.
 * If the sleep is interrupted by a fatal signal, this function will return
 * -EINTR.  If the semaphore is successfully acquired, this function returns
 * 0.
 */
int down_killable(struct semaphore *sem)
{
	unsigned long flags;
	int result = 0;

	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		result = __down_killable(sem);
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}
EXPORT_SYMBOL(down_killable);

/**
 * down_trylock - try to acquire the semaphore, without waiting
 * @sem: the semaphore to be acquired
 *
 * Try to acquire the semaphore atomically.  Returns 0 if the semaphore has
 * been acquired successfully or 1 if it it cannot be acquired.
 *
 * NOTE: This return value is inverted from both spin_trylock and
 * mutex_trylock!  Be careful about this when converting code.
 *
 * Unlike mutex_trylock, this function can be used from interrupt context,
 * and the semaphore can be released by any task or interrupt.
 */
int down_trylock(struct semaphore *sem)
{
	unsigned long flags;
	int count;

	raw_spin_lock_irqsave(&sem->lock, flags);
	count = sem->count - 1;
	if (likely(count >= 0))
		sem->count = count;
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return (count < 0);
}
EXPORT_SYMBOL(down_trylock);

/**
 * down_timeout - acquire the semaphore within a specified time
 * @sem: the semaphore to be acquired
 * @jiffies: how long to wait before failing
 *
 * Attempts to acquire the semaphore.  If no more tasks are allowed to
 * acquire the semaphore, calling this function will put the task to sleep.
 * If the semaphore is not released within the specified number of jiffies,
 * this function returns -ETIME.  It returns 0 if the semaphore was acquired.
 */
int down_timeout(struct semaphore *sem, long jiffies)
{
	unsigned long flags;
	int result = 0;

	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		result = __down_timeout(sem, jiffies);
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}
EXPORT_SYMBOL(down_timeout);

/**
 * up - release the semaphore
 * @sem: the semaphore to release
 *
 * Release the semaphore.  Unlike mutexes, up() may be called from any
 * context and even by tasks which have never called down().
 */
void up(struct semaphore *sem)
{
	unsigned long flags;

	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(list_empty(&sem->wait_list)))
		sem->count++;
	else
		__up(sem);
	raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);

/* Functions for the contended case */

struct semaphore_waiter {
	struct list_head list;
	struct task_struct *task;
	bool up;
};

/*
 * Because this function is inlined, the 'state' parameter will be
 * constant, and thus optimised away by the compiler.  Likewise the
 * 'timeout' parameter for the cases without timeouts.
 */
static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
	struct task_struct *task = current;
	struct semaphore_waiter waiter;

	list_add_tail(&waiter.list, &sem->wait_list);
	waiter.task = task;
	waiter.up = false;

	for (;;) {
		if (signal_pending_state(state, task))
			goto interrupted;
		if (unlikely(timeout <= 0))
			goto timed_out;
		__set_task_state(task, state);
		raw_spin_unlock_irq(&sem->lock);
		timeout = schedule_timeout(timeout);
		raw_spin_lock_irq(&sem->lock);
		if (waiter.up)
			return 0;
	}

 timed_out:
	list_del(&waiter.list);
	return -ETIME;

 interrupted:
	list_del(&waiter.list);
	return -EINTR;
}

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

static noinline int __sched __down_interruptible(struct semaphore *sem)
{
	return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

static noinline int __sched __down_killable(struct semaphore *sem)
{
	return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);
}

static noinline int __sched __down_timeout(struct semaphore *sem, long jiffies)
{
	return __down_common(sem, TASK_UNINTERRUPTIBLE, jiffies);
}

static noinline void __sched __up(struct semaphore *sem)
{
	struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
						struct semaphore_waiter, list);
	list_del(&waiter->list);
	waiter->up = true;
	wake_up_process(waiter->task);
}



        代码第 57行首先获得了保护 count 成员的自旋锁,如果 count 的值大于0,则将cout的值自减 1,然后释放自旋锁立即返回。否则调用 __down, __down 又调用了__down_common,准备将进程的状态切换为TASK_UNINTERRUPTIBLE,表示不能被信号唤醒。代码第 210 行, __down_common 调用 list_add_tail 将进程放到信号量的等待链表中,然后在代码第 221 行调用调度器,进程主动放弃 CPU,调度其他进程执行。很显然,进程在不能获得信号量的情况下会休眠,不会忙等待,从而适用于临界代码段运行时间比较长的情况。
        当给信号量赋初值 1 时,则表示在同一时刻只能有一个进程获得信号量,这种信号量叫二值信号量,利用这种信号量可以实现互斥,典型的应用代码如下。

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


        对于信号量的特点及其他的一些使用注意事项总结如下。

        (1)信号量可以被多个进程同时持有,当给信号量赋初值1时,信号量成为二值信号量,也称为互斥信号量,可以用来互斥。

        (2)如果不能获得信号量,则进程休眠,调度其他的进程执行,不会进行忙等待。

        (3)因为获取信号量可能会引起进程切换,所以不能用在中断上下文中,如果必须要用,只能使用 down_trylock。不过在中断上下文中可以使用 up 释信号量,从而唤配其他进程。
        (4)持有信号量期间可以调用调度器,但需要特别注意是否会产生死锁。

        (5)信号量的开销比较大,在不违背自旋锁的使用规则的情况下,应该优先使用自旋锁。



读写信号量


        和相对于自旋锁的读写锁类似,也有相对于信号量的读写信号量,它和信号量的本质是一样的,只是将读和写分开了,从而能获取更好的并发性。读写信号量的结构类型是structrw_semaphore,针对它的主要操作如下。

init rwsem(sem)

void down_read(struct rw semaphore *sem);

int down read_trylock(struct rw semaphore *sem);

void down write(struct rw'semaphore *sem);

int down write trylock(struct rw semaphore *sem);

void up read(struct rw semaphore *sem);

void up write(struct rw semaphore *sem);


其使用方法和读写锁类似,在此不再细述。



互斥量


        信号量除了不能用于中断上下文,还有一个缺点就是不是很智能。在获取信号量的代码中,只要信号量的值为0,进程马上就休眠了。但是更一般的情况是,在不会等待太长的时间后,信号量就可以马上获得,那么信号量的操作就要经历使进程先休眠再被唤醒的一个漫长过程。可以在信号量不能获取的时候,稍微耐心地等待一小段时间,如果在这段时间能够获取信号量,那么获取信号量的操作就可以立即返回,否则再将进程休眠也不迟。为了实现这种比较智能化的信号量,内核提供了另外一种专门用于互斥的高效率信号量,也就是互斥量,也叫互斥体,类型为

struct mutex,相关的API如下。
 


mutex init (mutex)

void mutox_lock(struct mutex *lock);

int mutex_Iock_interruptible(atruct mutex *lock);

int mutex_trylock(struct mutex *lock);
void mutex_unlock(struct mutex *Iock);

有了前而互斥操作的基础,使用互斥量来做互斥也就很容易实现了,示例代码如下


 

int i = 5;
/*定义互斥量*/
struct mutex lock;

/* 使用之前初始化互斥量 */
mutex_init (&block);
/* 访问共享资源之前获得互斥量 */
mutex_lock(&lock);
i++;
/*访问完共享资源后释放互斥量 */
mutex_unlock(&lock);


互斥量的使用比较简单,不过它还有一些更多的限制和特性,现将关键点总结如下:

(1)要在同一上下文对互斥量上锁和解锁,比如不能在读进程中上锁,也不能在进程中解锁。
(2)和自旋锁一样,互斥量的上锁是不能递归的。
(3)当持有互斥量时,不能退出进程。
(4)不能用于中断上下文,即使 mutex_trylock 也不行。
(5)持有互斥量期间,可以调用可能会引起进程切换的函数。
(6)在不违背自旋锁的使用规则时,应该优先使用自旋锁。
(7)在不能使用自旋锁但不违背互斥量的使用规则时,应该优先使用互斥量,而不是信号量。


RCU机制


        RCU(Read-Copy Update)机制即读一复制一更新。RCU 机制对共享资源的访问都是通过指针来进行的,读者(对共享资源发起读访问操作的代码)通过对该指针进行解引用,来获取想要的数据。写者在发起写访问操作的时候,并不是去写以前的共享资内存,而是另起炉灶,重新分配一片内存空间,复制以前的数据到新开辟的内存空间(有时不用复制),然后修改新分配的内存空间里面的内容。当写结束后,等待所有的读者完成了对原有内存空间的读取后,将读的指针更新,指向新的内存空间,之后的读操将会得到更新后的数据。这非常适合于读访问多、写访问少的情况,它尽可能地减少对锁的使用。
 


        内核使用 RCU 机制实现了对数组、链表和 NMI(不可屏中断)操作的大量 API,不过要能理解 RCU,通过下面几个最简单的 API 即可。
 


 

void rcu_read_lock(void);
rcu_dereference(p)
void rcu_read_unlock(void);
rcu_assign_pointer(p, v)
void synchronize_rcu(void);


rcu_read_lock:读者进入临界区。
rcu_dereference: 读者用于获取共享资源的内存区指针。
rcu_read_unlock: 读者退出临界区。
rcu_assign_pointer:用新指针更新老指针。
synchronize_rcu:等待之前的读者完成读操作


使用 RCU 机制最简单的示例代码如下。
 

struct foo {
    int a;
    char b;
    long c;
};
DEFINE_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;
}


        代码第1 行到第 5 行是共享资源的数据类型定义。代码第 6 行定义了一个用于写保护的自旋锁。代码第8 行定义了一个指向共享资源数据的全局指针。

        代码第15行,写者分配了一片新的内存。代码第 17 行保存原来的指针,第18行进行数据复制,即将原来的内存中的数据复制到新的内存中。代码第 19行完成对新内存中数据的修改。代码第20行则用新的指针更新原来的指针。在这之后不能立即释放原来指针所指向的内存,因为可能还有读者在使用原来的指针访问共享资源的数据,所以在代码第22行等待使用原来指针的读者,当所有使用原来的指针的读者都读完数据后,代码第23 行释放原来的指针所指向的内存。在数据更新和指针更新时使用了自旋锁进行保护。

        代码第 30 行是读者进入临界区,代码第 31 行使用 rcu_dereference 取共享资源的内存指针后进行解引用,获取相关的数据。访问完成后,代码第 32 行退出临界区。


虚拟串口驱动加入互斥


        严格来说,前面讲解的虚拟串口驱动都是错误的,因为驱动中根本没有考虑并发能导致的竞态。但是程序运行又基本正确,这说明竞态出现的概率很小,但是不代表态就不会产生。驱动开发者应该对这个问题保持高度的警惕,因为竞态所造成的后果有时是非常严重的,并且错误的原因也是很难发现的。所以在驱动设计的初期就应该考虑这些因素的影响,并采取对应的措施。
        为了方便说明问题,我们将之前的虚拟串口的运行机制稍加修改。首先,一个类似于串口的设备应该具有排他访问属性,即不能同时有多个进程都能打开串口并操作串口.在一段时间内,只允许一个进程操作串口,直到该进程关闭该串口为止,在这段时间内其他的进程都不能打开该串口,这也是实际的串口常规属性。其次,写给串口的数据不再环回,为简化问题,我们仅仅是把用户发来的数据简单地丢弃,认为数据是无等待地发送成功,那么写方向上也就不再需要等待队列。最后,串口接收中断还是通过网卡中断来产生,并将随机产生的接收数据放入接收 FIFO 中。
        针对串口功能的重新定义,我们来考虑驱动中的并发问题。首先,应该安排一个变量来表示当前串口是否可用的状态,当有一个进程已经成功打开串口,那么这将阻止其他的进程再打开串口,很显然,这个反映状态的变量是一个共享资源,当多个进程同时打开这个串口设备时,可能会产生竞态,应该对该共享资源做互斥处理。其次,写入的数据是简单丢弃(复制到一个局部的缓冲区中,非共享资源),所以不考虑并发。但是,当接收中断产生时,在中断或中断下半部处理中需要将数据写入接收 FIFO,这就存在着针对接收 FIFO 的读一写并发,即用户进程读取接收 FIFO,同时在中断或中断的下半部写数据到接收 FIFO,共享资源就是这个全局的接收 FIFO,针对该共享资源应该提供互斥的访问。根据上面的分析,相应的驱动代码如下

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kfifo.h>

#include <linux/ioctl.h>
#include <linux/uaccess.h>

#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/aio.h>

#include <linux/interrupt.h>
#include <linux/random.h>

#include "vser.h"

#define VSER_MAJOR	256
#define VSER_MINOR	0
#define VSER_DEV_CNT	1
#define VSER_DEV_NAME	"vser"
#define VSER_FIFO_SIZE	32

struct vser_dev {
	wait_queue_head_t rwqh;
	struct fasync_struct *fapp;
	atomic_t available;
	unsigned int baud;
	struct option opt;
	struct cdev cdev;
};

DEFINE_KFIFO(vsfifo, char, VSER_FIFO_SIZE);
static struct vser_dev vsdev;

static void vser_work(struct work_struct *work);
DECLARE_WORK(vswork, vser_work);

static int vser_fasync(int fd, struct file *filp, int on);

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 long vser_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	if (_IOC_TYPE(cmd) != VS_MAGIC)
		return -ENOTTY;

	switch (cmd) {
	case VS_SET_BAUD:
		vsdev.baud = arg;
		break;
	case VS_GET_BAUD:
		arg = vsdev.baud;
		break;
	case VS_SET_FFMT:
		if (copy_from_user(&vsdev.opt, (struct option __user *)arg, sizeof(struct option)))
			return -EFAULT;
		break;
	case VS_GET_FFMT:
		if (copy_to_user((struct option __user *)arg, &vsdev.opt, sizeof(struct option)))
			return -EFAULT;
		break;
	default:
		return -ENOTTY;
	}

	return 0;
}

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 ssize_t vser_aio_read(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos)
{
	size_t read = 0;
	unsigned long i;
	ssize_t ret;

	for (i = 0; i < nr_segs; i++) {
		ret = vser_read(iocb->ki_filp, iov[i].iov_base, iov[i].iov_len, &pos);
		if (ret < 0)
			break;
		read += ret;
	}

	return read ? read : -EFAULT;
}

static ssize_t vser_aio_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos)
{
	size_t written = 0;
	unsigned long i;
	ssize_t ret;

	for (i = 0; i < nr_segs; i++) {
		ret = vser_write(iocb->ki_filp, iov[i].iov_base, iov[i].iov_len, &pos);
		if (ret < 0)
			break;
		written += ret;
	}

	return written ? written : -EFAULT;
}

static int vser_fasync(int fd, struct file *filp, int on)
{
	return fasync_helper(fd, filp, on, &vsdev.fapp);
}

static irqreturn_t vser_handler(int irq, void *dev_id)
{
	schedule_work(&vswork);

	return IRQ_HANDLED;
}

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 struct file_operations vser_ops = {
	.owner = THIS_MODULE,
	.open = vser_open,
	.release = vser_release,
	.read = vser_read,
	.write = vser_write,
	.unlocked_ioctl = vser_ioctl,
	.poll = vser_poll,
	.aio_read = vser_aio_read,
	.aio_write = vser_aio_write,
	.fasync = vser_fasync,
};

static int __init vser_init(void)
{
	int ret;
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);
	ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
	if (ret)
		goto reg_err;

	cdev_init(&vsdev.cdev, &vser_ops);
	vsdev.cdev.owner = THIS_MODULE;
	vsdev.baud = 115200;
	vsdev.opt.datab = 8;
	vsdev.opt.parity = 0;
	vsdev.opt.stopb = 1;

	ret = cdev_add(&vsdev.cdev, dev, VSER_DEV_CNT);
	if (ret)
		goto add_err;

	init_waitqueue_head(&vsdev.rwqh);

	ret = request_irq(167, vser_handler, IRQF_TRIGGER_HIGH | IRQF_SHARED, "vser", &vsdev);
	if (ret)
		goto irq_err;

	atomic_set(&vsdev.available, 1);

	return 0;

irq_err:
	cdev_del(&vsdev.cdev);
add_err:
	unregister_chrdev_region(dev, VSER_DEV_CNT);
reg_err:
	return ret;
}

static void __exit vser_exit(void)
{
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);

	free_irq(167, &vsdev);
	cdev_del(&vsdev.cdev);
	unregister_chrdev_region(dev, VSER_DEV_CNT);
}

module_init(vser_init);
module_exit(vser_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");


        代码第 31 行添加了一个 available 原子变量成员,用于表示当前设备是否可用。代码第 248 行使用 atomic_set 将原子变量赋值为 1,表示可用。代码第 47 行使用atomic_dec_and_test 先将 available 的值减 1,然后判断减1后的结果是否为0。如果结果是0,函数返回真,表示设备是首次被打开,所以代码第 48 行返回 0,表示打开成功。atomic_dec_and_test 将减和测试结果按原子操作,即不会被打断,所以即便有另外一个进程想打开串口,二者也只能有一个竞争成功。代码第 50 行表示竞争失败的进程应该将available 的值加回来,否则以后串口将永远无法打开。代码第 58 行表示竞争成功的进程在使用完串口后,将 available 的值加回来,使串口又可用。

        代码第 62 行至第 87 行是用户读操作的驱动实现,代码第 68 行首先修正了读取的字节数,然后第 69 行使用了读等待队列头中自带的自旋锁,进行加锁操作,因为之后都要对共享的接收 FIFO 进行操作(包括判空)。代码第 72 行,如果接收 FIFO为空,并且是非阻塞操作,那么应该释放自旋锁,然后返回-EAGAIN,如果不释放自旋锁将会导致重复获取自旋锁的错误,这是比较容易遗忘的内容,应该注意。如果接收 FIFO 为空,并且是阻塞操作,那么调用 wait_event_interruptible_locked 来使进程休眠,当接收 FIFO不为空时,进程被唤醒。之前我们说过,在自旋锁获得期间,不能调用可能会引起进程切换的函数,但是这里用的是 wait_event_interruptible_locked,在进程休眠前,会自动释放自旋锁,醒来后将重新获得自旋锁,这就非常巧妙地对接收 FIFO 的判空实现原子化操作,从而避免了竞态。代码第 77 行是进程被信号唤醒应该释放自旋锁,也容易被遗忘,需要注意。代码第 82 行将 FIFO中的数据读出放入一个临时的缓冲区中,然后释放自旋锁。这里没有使用 kfifo_to_user,是因为该函数可能会导致进程切换,在自旋锁持有期间不能被调用。所以直到代码第 85行才使用 copy_to_user 将读取到的数据复制到用户空间。代码第 135行至第 138 行是对 FIFO的判空操作做,避免在此期间产生的竞态代码第 187行至第 206行是中断下半部的工作队列实现,同样也是在操作接收 FIFO的整个过程中,通过自旋锁来保护。代码第 201 行在接收 FIFO 访问完成后立即释放了自旋锁,尽量避免长时间持有自旋锁。
        我们来讨论一下上面的并发情况,如果在用户读期间下半部开始执行了,那么下半部会因为不能获取自旋锁而忙等待,直到 wait_event_inierruptible_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) 使用何种手段互斥。互斥的手段有很多种,我们应该尽量选用一些简单的、开销小的互斥手段,当该互斥手段受到某条使用规则的制约后再考虑其他的互斥手段。

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


完成量


        讨论完内核中的互斥后,接下来我们来看看内核中的同步。同步是指内核中的执行路径需要按照一定的顺序来进行,例如执行路径A 要继续往下执行则必须要保证执行路径B 执行到某一个点才行。以一个 ADC 设备来说,假设一个驱动中的一个执行路径是将ADC 采集到的数据做某些转换操作(比如将干次采样结果做平均),而另一个执行路径专门负责 ADC 采样,那么做转换操作的执行路径要等待做采样的执行路径。


        同步可以用信号量来实现,就以上面的 ADC 驱动来说,可以先初始化一个值为0的信号量,做转换操作的执行路径先用 down 来获取这个信号量,如果在这之前没有采集到数据,那么做转换操作的路径将会体眠等待。当做采样的路径完成采样后,调用即释放信号量,那么做转换操作的执行路径将会被唤醒,这就保证了采样发生在转换之前,也就完成了采样和转换之间的同步。

        不过内核专门提供了一个完成量来实现该操作,完成最的结构类型定义如下.

struct completion {
    unsigned int done;
    wait_queue_head_t wait;
}



        done 是是否完成的状态,是一个计数值,为0表示未完成。wait 是一个等待队列头,回想前面阻塞操作的知识,不难想到完成量的工作原理。当 done为0时进程阻塞,当内核的其他执行路径使 done 的值大于 0时,负贵唤醒被阻塞在这个完成量上的进程。完成量的主要 API如下。


 

void init_completion(struct completion *x);
wait_for_completion(struct completion *);
wait_for_completion_interruptible(struct completion *x);
unsigned long wait_for_completion_timeout (struct completion *x, unsigned long timeout);
long wait_for_completion_interruptible_timeout (struct completion *x, unsigne long timeout);
bool try_wait_for_completion(struct completion *x);
void complete(struct completion *);
void complete_all(struct completion *);


        有了前面知识的积累,上面函数的作用及参数的意义都能见名知意,在这里就不再细述。只是需要说明一点,complete 只唤醒一个进程,而complete_all 唤醒所有休眠的进程。完成量的使用例子如下。


 

/*定义完成量 */
struct completion comp;
/* 使用之前初始化完成量*/
init completion(&comp);
/* 等待其他任务完成某个操作 */
wait_for_completion(&comp);

/*某个操作完成后,唤醒等待的任务 */
complete(&comp);


习题


1.内核中的并发情况有(ABCDE )。

[A] 硬件中断[C] 抢占内核的多进程环境
[B] 软中断和 tasklet[D]普通的多进程环境

[E] 多处理器或多核CPU

2、local_irg_save 的作用是 ( C)。
[A]禁止全局中断[C] 止本CPU中断并将之前的中断使能状态保存下来
[B]禁止本CPU中断


3、可以对原子变量进行的操作有( ABC)。
[B] 加上一个整数值并返回结果
[D] 变量中指定的比特位交换
[A]自减并测试结果是否为0

[C] 进行位清除


4、关于自旋锁的使用说法错误的是( D)。

[A] 获得自旋锁的临界代码段执行时间不宜过长
[B]在获得锁的期间不能够调用可能会引起进程切换的函数
[C]自旋锁可以用于中断上下文中

[D]在所有系统中,即不管是否抢占、是否是多核,自旋锁都是忙等待

5.关于信号量的使用说法错误的是 (A )。
[A] 如果不能获得信号量,则进程休眠
[B]在中断上下文中不能调用 down 函数来获取信号量
[C]在获得信号量期间,进程可以睡眠
[D] 相比于自旋锁,优先使用信号量


6.关于RCU 说法错误的是 ( A)。
[A] 写者完成写后立即更新指针
[B] 适合于读访问多、写访问少的情况
[C] 对共享资源的访问都是通过指针来进行的
[D] 尽可能减少了对锁的使用


7.完成量的complete 函数可以唤醒( A)进程
[A]一个
[B] 所有
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/527896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Win10笔记本开机黑屏出现白色错误英文无法启动怎么办?

Win10笔记本开机黑屏出现白色错误英文无法启动怎么办&#xff1f;有用户电脑正常开机之后&#xff0c;出现了问题&#xff0c;系统无法正常的启动&#xff0c;出现一些英文错误代码。那么遇到这个情况怎么去进行解决呢&#xff1f;一起来看看以下的解决方法分享吧。 准备工作&a…

C语言数据结构注意点-线性表

目录 关于指针 LinkList L和LinkList *L的区别 初始化注意点 scanf()的操作 顺序表相关操作符号的确定 关于指针 ①指针和指针变量是两个不同的概念&#xff0c;但要注意的是&#xff0c;通常我们叙述时会把指针变量简称为指针。 ②指针变量其实是一个变量&…

FL Studio21中文完整版All Plugins Edition及切换教程

说到制作电音的软件&#xff0c;coco玛奇朵一定会把FL Studio放到第一个来讲。水果是一款为了电子音乐而生的的宿主软件。水果&#xff0c;独特的节拍音序器组件和通道机架与混音台模块打造的编曲“块”的思路。是极为适合于电子音乐的编排。而且随着水果版本不断地升级&#x…

Vite的基本介绍以及优劣势(一文读懂vite)?

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、Vite是什么&#xff1f;二、为什么选Vite?1、现实的问题2、缓慢的服务器启动3、缓慢的更新 三、Vite的优势&#xff1f;四、Vite的劣势&#xff1f;五、Vite以…

深入理解双亲委派机制

一、双亲委派机制委派流程 双亲委派机制&#xff0c;就是JVM虚拟机加载类的时候&#xff0c;会优先委派上级类加载器进行类加载。 1、如果上级类加载器能找到这个类&#xff0c;那就由上级类加载器加载&#xff0c;并且对下级共享&#xff0c;反之不共享。 2、如果上级类加载…

【#ifndef, #define, 和 #endif】

前言 学习AFNetWoring源码的时候&#xff0c;在AFN的h借接口文件又看到了这几个宏定义&#xff0c;学习记录一下。 作用 #ifndef, #define, 和 #endif是C/CPP的预处理指令&#xff0c;常常用来条件编译和防止头文件重复包含。 简介 #ifndef 它是if not define的简写&…

SpringBoot 使用 Sa-Token 完成注解鉴权功能

注解鉴权 —— 优雅的将鉴权与业务代码分离。本篇我们将介绍在 Sa-Token 中如何通过注解完成权限校验。 Sa-Token 是一个轻量级 java 权限认证框架&#xff0c;主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。 Gitee 开源地址&#xff1…

生信步骤|EffectorP批量预测病原物效应子

EffectorP软件利用机器学习原理&#xff0c;通过事先收集已知的效应子制备训练集&#xff0c;从而实现病原真菌和卵菌的效应子预测[1]。 EffectorP发展史[2]&#xff1a; 1.0版本最初在16年发表于NEW PHYTOLOGIST&#xff0c;实现了机器学习初步预测效应子。 2.0版本在18年发表…

OPPO官宣:哲库解散,哲库是 OPPO 旗下的芯片厂,类似华为海思的角色,有近 3000 名员工

大家好&#xff0c;我是二哥呀。 这两天&#xff0c;互联网最大的声音之一就是&#xff0c;OPPO 将终止芯片业务&#xff0c;相信大多数小伙伴和二哥一样&#xff0c;第一眼看到这则消息的时候&#xff0c;震惊的同时并惋惜&#xff01; ZEKU 是 OPPO 旗下的芯片厂&#xff0…

Java面试知识点(全)-JVM面试知识点

Java面试知识点(全) 导航&#xff1a; https://nanxiang.blog.csdn.net/article/details/130640392 注&#xff1a;随时更新 JVM内存结构 内存结构 Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域&#xff0c;每个区域都有的用途以及创建销毁的时机&#xf…

【JavaScript】手写Promise

&#x1f431; 个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️ 作者简介&#xff1a;2022年度博客之星前端领域TOP 2&#xff0c;前端领域优质作者、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &am…

五. AMS实践,Hook启动未注册的Activity

Activity任务栈解析 正常情况下我们的app中的所有Activity都是运行在同一个任务栈(ActivityStack)里面的,也就是我们app的build.gradle文件中的applicationId那个任务栈. 如何实现Activity运行在不同的任务栈呢? 需要在Intent启动这个Activity的时候,给这个intent赋值,设置代…

【KVM虚拟化】· 存储池、存储卷

目录 &#x1f341;虚拟磁盘文件 &#x1f342;基于文件系统的KVM存储 &#x1f342;基于设备的KVM存储 &#x1f341;使用KVM存储池 &#x1f342;存储池概念 &#x1f341;virsh中存储池命令 &#x1f341;virsh中存储卷命令 &#x1f341;命令实例 &#x1f342;创建存储池 …

一个开源的即时通讯应用 Tailchat

今天给大家介绍一款即时通讯应用&#xff0c;这个开源项目是&#xff1a;Tailchat&#xff0c;它是一个基于 React Typescript 的现代开源 noIM 应用程序。 简单介绍 相信大家都或多或少了解过 Discord / Slack 这样大火的即时通讯应用。两者分别在各自的领域有很大的成就。…

http强缓存和协商缓存的介绍和应用案例,简介明了

http强缓存和协商缓存的介绍和应用案例&#xff0c;简介明了 http缓存方式简介缓存机制案例1. Expires老版本的方式&#xff1a;2. cache-control新版本的方式&#xff1a;3.Etag和If-None-Match http缓存方式简介 强缓存&#xff1a;强缓存使用Expires&#xff08;老版本&…

el-table实现可编辑表格的思路;el-table删除不正确:表格中的el-select下拉数据项有值,但输入框中value值不显示

目录 一、问题 二、原因及解决方法 三、总结 tips:如嫌繁琐&#xff0c;直接看总结即可&#xff01; 一、问题 el-table实现可编辑表格的思路&#xff1a; 1.要写一个可编辑的表格&#xff1a;表格中的一列是下拉框。实现方法很简单&#xff1a;在el-table-column(elemen提…

PMP课堂模拟题目及解析(第9期)

81. 一位经验丰富的项目经理在到达一个重大开发里程碑之前识别到一个问题&#xff0c;项目经理采取相应的行动&#xff0c;并能够按时解决问题。两周后&#xff0c;发起人通知项目经理&#xff0c;客户发出了处罚费。若要避免这个问题&#xff0c;项目经理应该事先做什么&…

FGX Native 1.4.1.1 For Delphi Crack

FGX Native 1.4.1.1 For Delphi Crack FGX Native Network Frame是制造跨平台和现代移动设备的强大工具。FG开发团队的主要目标是简化移动应用程序的开发&#xff0c;使大多数人都能以各种技能开发应用程序。此外&#xff0c;这种形式的网络最重要的功能可以在100%的用户界面中…

CorelDRAW2020工作室版下载及新增功能介绍

CorelDRAW Graphics Suite 2020将基于人工智能的图形设计提升到一个全新水平。CorelDRAW 中增强了“查找和替换”\“对齐和分布”、阴影效果等功能。您喜欢的 Corel PHOTO-PAINT 功能 — 从遮罩、效果和透镜到“替换颜色”— 均已优化&#xff01; 针对操作系统的功能优化 Core…

微信之小程序授权登录

首先我要怒骂微信的后台开发 真的还是乱七八糟。 首先我们登录微信开发平台 选择要开发的类型 然后小程序登录&#xff1a;选择 微信小程序实现微信登录详解&#xff08;JAVA后台&#xff09;官方文档&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/framework…