高级字符设备驱动操作(wait_event/poll/访问控制)

wait_event

阻塞指的是当执行操作的时候,如果条件未满足,则挂起线程直至条件满足之后在进行操作,被挂起的线程进入睡眠状态。当一个进程被置为休眠状态的时候,它会被标记为一种特殊状态并且从运行队列中移除。直到某些情况下修改了这个状态,进程才会继续运行。休眠的进程会被搁置到一边,等待将来某个事件的发生。
说明如何进入休眠状态前,请牢记两条规则:
1、永远不要在原子上下文中进入休眠
2、当我们被唤醒时,我们永远无法知道休眠了多少时间,或者休眠期间都发生了什么事情

为了能够找到休眠的进程,需要维护一个称为等待队列的数据结构,等待队列是一个进程链表,其中包含了等待某个特定事件的所有进程。Linux中一个等待队列通过一个等待队列头来管理,等待队列头的结构类型是wait_queue_head_t,定义在。初始化方式如下:
静态初始化:DECLARE_WAIT_QUEUE_HEAD(name);
动态初始化:wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

Linux内核中最简单的休眠方式是称为wait_event的宏;其形式如下:
wait_event(queue, condition) 非中断休眠。
wait_event_interruptible(queue, condition) 可以被信号中断,返回非零表示休眠被某个信号中断。
wait_event_timeout(queue, condition, timeout) 限时版本,超时返回0。
wait_event_interruptible_timeout(queue, condition, timeout) 限时版本,超时返回0。
上面所有的形式中,queue是等待队列头。注意,它是“通过值”传递,不是指针。condition是任意一个布尔表达式,上面的宏在休眠前后都要对该表达式求值;在条件为真之前,进程都会保持休眠。注意,该条件可能会被多次求值,因此对该表达式的求值不能带来任何副作用。

用来唤醒的函数如下:
void wake_up(wait_queue_head_t *queue); 唤醒等待在给定queue上的所有进程。
void wake_up_interruptible(wait_queue_head_t *queue); 只会唤醒那些执行可中断休眠的进程。
约定的做法是wait_event和wake_up对应,wait_event_interruptible和wake_up_interruptible对应。

简单实例如下:

ssize_t sleepy_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	printk(KERN_DEBUG "process %i (%s) going to sleep\n",
			current->pid, current->comm);
	wait_event_interruptible(wq, flag != 0);
	flag = 0;
	printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
	return 0; /* EOF */
}

ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count,
		loff_t *pos)
{
	printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
			current->pid, current->comm);
	flag = 1;
	wake_up_interruptible(&wq);
	return count; /* succeed, to avoid retrial */
}

在某些情况下,我们也需要实现非阻塞的操作。显示的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定。如果指定了O_NONBLOCK标志,如果执行操作的条件不满足,就立即返回-EAGAIN。只有read、write和open文件操作受到非阻塞标志的影响。

scull中的实例:

static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
	struct scull_pipe *dev = filp->private_data;

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;

	while (dev->rp == dev->wp) { /* 无数据可读 */
		up(&dev->sem); /* 释放锁*/
		if (filp->f_flags & O_NONBLOCK)
			return -EAGAIN;
		PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
		if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
			return -ERESTARTSYS; /* 信号,通知fs层做相应的处理 */
		/* 否则循环,但首先获取锁 */
		if (down_interruptible(&dev->sem))
			return -ERESTARTSYS;
	}
	/* 数据已经就绪,返回 */
	if (dev->wp > dev->rp)
		count = min(count, (size_t)(dev->wp - dev->rp));
	else /* 写入指针回卷,返回数据直到 dev->end */
		count = min(count, (size_t)(dev->end - dev->rp));
	if (copy_to_user(buf, dev->rp, count)) {
		up (&dev->sem);
		return -EFAULT;
	}
	dev->rp += count;
	if (dev->rp == dev->end)
		dev->rp = dev->buffer; /* 回卷 */
	up (&dev->sem);

	/* 最后,返回所有写入者并返回 */
	wake_up_interruptible(&dev->outq);
	PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
	return count;
}

代码逻辑:while循环在拥有设备信号量时测试缓冲区。如果其中有数据,则可以立即将数据返回给用户而不需要休眠,整个循环体会被跳过。相反,如果缓冲区为空,则必须休眠。但是要在休眠之前释放设备信号量,因为如果在拥有该信号量时休眠,任何写入者都没有机会来唤醒了。另外其中还有检查非阻塞标志O_NONBLOCK,用来判断是否立即返回。
 

poll

使用非阻塞型IO的应用程序也经常使用poll、select和epoll系统调用。poll、select和epoll的功能本质上是一样的:都允许进程决定是否对一个或者多个打开的文件做非阻塞的读取或写入。这些调用也会阻塞进程,直到给定的文件描述符集合中的任何一个可以读取或写入。poll、select和epoll系统调用的驱动原型是:unsigned int (*poll) (struct file *filep, struct poll_table_struct *wait);包含头文件<linux/poll.h>。
代码编写步骤如下:
1、在一个或者多个可指示poll状态变化的等待队列上调用poll_wait。如果当前没有文件描述符可用来执行I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。
2、返回一个用来描述操作是否可以立即无阻塞执行的位掩码。可用的位掩码如下:
POLLIN 如果设备无阻塞的读,就返回该值。
POLLRDNORM 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA)。
POLLRDBAND 如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中
POLLPRI 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理。
POLLHUP 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。
POLLERR 如果设备发生错误,就返回该值。
POLLOUT 如果设备可以无阻塞地些,就返回该值。
POLLWRNORM 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM)。
POLLWRBAND 于POLLRDBAND类似。
使用参考:

#include 

static DECLARE_WAIT_QUEUE_HEAD(csdn_waitq);
static int wait_signal = 0;

unsigned int csdn_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    pr_info("csdn_poll enter\n");

    poll_wait(filp, &csdn_waitq,  wait);
    pr_info("wait_signal: %d\n", wait_signal);
    if (wait_signal) {
        mask |= POLLIN | POLLRDNORM;
    }

    return mask;
}

static struct file_operations csdn_fops = {
    /* ohters code */
    .poll = csdn_poll,
};

/* 触发方式
wait_signal = 1;
wake_up_interruptible(&csdn_waitq); 
*/

用户空间参考:

int main(int argc, char const *argv[])
{
    struct pollfd fds = { 0 };

    fds.fd = open(DEVICE_NAME, O_RDWR);
    if (fds.fd < 0) {
        printf("open failed! error:%s\n", strerror(errno));
        return 0;
    }

    fds.events = POLLIN;
    poll(&fds, 1, 5000); /* 五秒后自动返回 */
    printf("fds.revent: %d\n", fds.revents);

    close(fds.fd);

    return 0;
}

 

llseek

llseek方法实现了lseek和llseek系统调用。如果操作系统未定义llseek方法,内核默认通过filp->f_ops而执行定位,filp->f_ops是文件的当前读写位置。

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
	struct scull_dev *dev = filp->private_data;
	loff_t newpos;

	switch(whence) {
	  case 0: /* SEEK_SET */
		newpos = off;
		break;

	  case 1: /* SEEK_CUR */
		newpos = filp->f_pos + off;
		break;

	  case 2: /* SEEK_END */
		newpos = dev->size + off;
		break;

	  default: /* can't happen */
		return -EINVAL;
	}
	if (newpos < 0) return -EINVAL;
	filp->f_pos = newpos;
	return newpos;
}

 

访问控制

控制可以打开设备的进程数量,样例代码如下,此设备只能允许一个进程访问。

static atomic_t csdn_available = ATOMIC_INIT(1);

static int csdn_open(struct inode *inode, struct file *filp)
{
    pr_info("csdn open enter\n");
    if (!atomic_dec_and_test(&csdn_available)) {
        atomic_inc(&csdn_available);
        return -EBUSY;
    }

    pr_info("csdn open exit!\n");
    return 0;
}

static int csdn_release(struct inode *inode, struct file *filp)
{
    atomic_inc(&csdn_available);
    return 0;
}

你可能感兴趣的:(Linux,驱动)