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

wait_event

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

为了能够找到休眠的进程,需要维护一个称为等待队列的数据结构,等待队列是一个进程链表,其中包含了等待某个特定事件的所有进程。Linux中一个等待队列通过一个等待队列头来管理,等待队列头的结构类型是wait_queue_head_t,定义在<linux/wait.h>。初始化方式如下:
静态初始化: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 <linux/poll.h>

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;
}
以下是一个简单的使用 epoll字符设备驱动代码示例: ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/types.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/poll.h> #include <linux/cdev.h> #include <linux/device.h> #include <asm/uaccess.h> #define DEVICE_NAME "mychardev" #define BUFFER_SIZE 1024 MODULE_LICENSE("Dual BSD/GPL"); // 设备结构体 struct mychardev_dev { struct cdev cdev; // 字符设备结构体 char buffer[BUFFER_SIZE]; // 数据缓冲区 atomic_t length; // 缓冲区中当前数据长度 wait_queue_head_t wq; // 等待队列头 struct mutex mutex; // 互斥锁 struct device *device; // 设备指针 }; // 定义设备结构体变量 static struct mychardev_dev mychardev; // 打开设备 static int mychardev_open(struct inode *inode, struct file *file) { file->private_data = container_of(inode->i_cdev, struct mychardev_dev, cdev); return 0; } // 释放设备 static int mychardev_release(struct inode *inode, struct file *file) { return 0; } // 读取设备 static ssize_t mychardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { struct mychardev_dev *dev = file->private_data; ssize_t ret = 0; // 等待缓冲区中有数据 if (wait_event_interruptible(dev->wq, atomic_read(&dev->length) > 0)) { return -ERESTARTSYS; } // 读取数据 if (mutex_lock_interruptible(&dev->mutex)) { return -ERESTARTSYS; } if (count > atomic_read(&dev->length)) { count = atomic_read(&dev->length); } if (copy_to_user(buf, dev->buffer, count)) { ret = -EFAULT; } else { memmove(dev->buffer, dev->buffer + count, atomic_read(&dev->length) - count); atomic_sub(count, &dev->length); ret = count; } mutex_unlock(&dev->mutex); return ret; } // 写入设备 static ssize_t mychardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { struct mychardev_dev *dev = file->private_data; ssize_t ret = 0; // 写入数据 if (mutex_lock_interruptible(&dev->mutex)) { return -ERESTARTSYS; } if (count > BUFFER_SIZE - atomic_read(&dev->length)) { count = BUFFER_SIZE - atomic_read(&dev->length); } if (copy_from_user(dev->buffer + atomic_read(&dev->length), buf, count)) { ret = -EFAULT; } else { atomic_add(count, &dev->length); ret = count; } mutex_unlock(&dev->mutex); // 唤醒等待队列中的进程 wake_up_interruptible(&dev->wq); return ret; } // 轮询设备 static unsigned int mychardev_poll(struct file *file, poll_table *wait) { struct mychardev_dev *dev = file->private_data; unsigned int mask = 0; poll_wait(file, &dev->wq, wait); if (atomic_read(&dev->length) > 0) { mask |= POLLIN | POLLRDNORM; } if (atomic_read(&dev->length) < BUFFER_SIZE) { mask |= POLLOUT | POLLWRNORM; } return mask; } // 设备操作结构体 static struct file_operations mychardev_fops = { .owner = THIS_MODULE, .open = mychardev_open, .release = mychardev_release, .read = mychardev_read, .write = mychardev_write, .poll = mychardev_poll, }; // 初始化设备 static int mychardev_init(void) { int ret; // 分配设备号 if (register_chrdev_region(MKDEV(0, 0), 1, DEVICE_NAME)) { printk(KERN_ERR "mychardev: register_chrdev_region failed\n"); return -EFAULT; } // 初始化字符设备结构体 cdev_init(&mychardev.cdev, &mychardev_fops); mychardev.cdev.owner = THIS_MODULE; // 注册字符设备 ret = cdev_add(&mychardev.cdev, MKDEV(0, 0), 1); if (ret) { printk(KERN_ERR "mychardev: cdev_add failed\n"); unregister_chrdev_region(MKDEV(0, 0), 1); return ret; } // 初始化等待队列头 init_waitqueue_head(&mychardev.wq); // 初始化互斥锁 mutex_init(&mychardev.mutex); // 创建设备节点 mychardev.device = device_create( class_create(THIS_MODULE, "mychardev_class"), NULL, MKDEV(0, 0), NULL, DEVICE_NAME); if (IS_ERR(mychardev.device)) { printk(KERN_ERR "mychardev: device_create failed\n"); cdev_del(&mychardev.cdev); unregister_chrdev_region(MKDEV(0, 0), 1); return PTR_ERR(mychardev.device); } return 0; } // 卸载设备 static void mychardev_exit(void) { // 销毁设备节点 device_destroy(class_create(THIS_MODULE, "mychardev_class"), MKDEV(0, 0)); // 删除字符设备 cdev_del(&mychardev.cdev); // 释放设备号 unregister_chrdev_region(MKDEV(0, 0), 1); } module_init(mychardev_init); module_exit(mychardev_exit); ``` 在此示例中,我们使用了 epoll 实现了设备的轮询,当设备中有数据时,我们会将进程加入到等待队列中,当设备中有数据或者缓冲区未满时,我们会将等待队列中的进程唤醒。同时,我们也使用了互斥锁来保证数据的同步和并发访问的安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值