注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。
书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)
字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。对于用户而言,使用文件系统的操作接口open()、close()、read()、write()等进行访问。
阻塞操作:执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起进程进入睡眠状态,被从调度队列中移走,直到等待的条件被满足。
非阻塞操作:进程在不能进行设备操作时,不挂起,那么轮询,要么放弃,直到可以操作为止。
阻塞与非阻塞的用户态访问的区别:
以阻塞与非阻塞方式读取串口一个字符的代码为例进行说明。
阻塞的方式在打开文件的时候没有O_NONBLOCK标记。
char buf; fd = open("/dev/ttyS1", O_RDWR); ... res = read(fd, &buf, 1); /* 串口有输入才返回 */ if (res == 1) printf("%c\n", buf);
非阻塞的读取一个串口字符:
char buf; fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK); ... while(read(fd, &buf, 1) != 1) /* 串口上无返回,循环尝试读取串口 */ continue; printf("%c\n", buf);
除了在打开文件时可以指定阻塞还是非阻塞方式外,在打开文件之后,可以通过ioctl()和fcntl()来改变读写的方式,比如从阻塞变为非阻塞或者从非阻塞变为阻塞。
在Linux驱动程序中,可以使用等待队列(Wait Queue)来实现阻塞进程的唤醒。
等待队列作为Linux内核中的一个基本单位,以队列为数据结构,与调度机制紧密结合,可以用来同步对系统资源的访问。
1)定义等待队列头部:
typedef struct __wait_queue_head wait_queue_head_t; wait_queue_head_t my_queue;
2)初始化等待队列头部:
init_waitqueue_head(&my_queue);
宏DECLARE_WAIT_QUEUE_HEAD()可以定义并初始化等待队列头部:
#define DECLARE_WAIT_QUEUE_HEAD(name) \ wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
3)定义等待队列元素
定义并初始化一个名为name的等待队列元素
#define DECLARE_WAITQUEUE(name, tsk) \ wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
4)添加/移除等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
5)等待事件
参数wq:作为等待队列头部的队列被唤醒
参数condition:condition条件必须满足
参数timeout:等待超时时间,以jifffy为单位,等待timeout到达时无论condition是否满足,均返回。
#define wait_event(wq, condition) #define wait_event_timeout(wq, condition, timeout) #define wait_event_interruptible(wq, condition) //可被信号打断 #define wait_event_interruptible_timeout(wq, condition, timeout)
6)唤醒队列
唤醒以queue作为等待队列头部的队列中的所有的线程。
#define wake_up(queue) #define wake_up_interruptible(queue)
wake_up()应该和wait_event()或wait_event_timeout()成对使用;
wake_up_interruptible()应该和wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。
wake_up()可唤醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程;wake_up_interruptible只能唤醒处于TASK_INTERRUPTIBLE的进程。
7)在等待队列上睡眠
sleep_on(wait_queue_head_t *q); interruptible_sleep_on(wait_queue_head_t *q);
sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。
interruptible_sleep_on()与sleep_on()函数类似,其作用是将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得(q指引的等待队列被唤醒)或者进程收到信号。
sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。
使用等待队列的模板,判断设备是否可写,如果不可写且为阻塞I/O,则进程睡眠并挂起等待队列。
static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos) { ... DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */ add_wait_queue(&xxx_wait, &wait); /* 添加元素到等待队列 */ /* 等待设备缓冲区可写 */ do { avail = device_writable(...); if (avail < 0) { if (file->f_flags & O_NONBLOCK) { /* 非阻塞 */ ret = -EAGAIN; goto out; } __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */ schedule(); /* 调度其他进程执行 */ if (signal_pending(current)) { /* 如果是因为信号唤醒 */ ret = -ERESTARTSYS; goto out; } } } while (avail < 0); /* 写设备缓冲区 */ device_write(...); out: remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */ set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */ return ret; }
这段代码对理解进程状态切换很重要,代码中有如下几个要点:
1)如果是非阻塞访问(O_NONBLOCK被设备),设备忙时,直接返回-EAGAIN。
2)如果是阻塞访问,调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并通过schedule()调度其它进程执行。
3)醒来的时候,由于调度出去的时候进程状态是TASK_INTERRUPTIBLE(浅度睡眠),所以唤醒的可能是信号,所以先通过signal_pending判断是否为信号唤醒,如果是,立即返回-ERESTARTSYS。
DECLARE_WAITQUEUE和add_wait_queue这两个动作的效果如下图所示:
在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,这个新元素绑定了一个task_struct数据结构(当前做xxx_write的current,也是DECLARE_WAITQUEUE使用"current"作为参数的原因)。
把globalmem的全局内存当作一个FIFO,只有FIFO中有数据的时候(有进程把数据写到FIFO而且没有读进程读空),读进程才把数据读出,而且读取数据后从globalmem的全局内存中被拿掉;只有当FIFO不是满时(有一些空间未被写或写满后读进程从这个FIFO中读出了数据),写进程才能往这个FIFO中写入数据。
在globalfifo中,读FIFO将唤醒写FIFO的进程(如果之前的FIFO正好是满的),而写FIFO也将唤醒读FIFO的进程(如果之前的FIFO是空的)。
完整代码:
#include <linux/module.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/uaccess.h> /* 直接使用立即数当作命令不合理,暂定 */ #define MEM_CLEAR 0x1 #define GLOBALFIFO_MAJOR 230 #define GLOBALFIFO_SIZE 0x1000 static int globalfifo_major = GLOBALFIFO_MAJOR; module_param(globalfifo_major, int, S_IRUGO); /* 设备结构体 */ struct globalfifo_dev { struct cdev cdev; unsigned int current_len; /* 当前FIFO中有效数据的长度 */ unsigned char mem[GLOBALFIFO_SIZE]; struct mutex mutex; wait_queue_head_t r_wait; wait_queue_head_t w_wait; }; struct globalfifo_dev *globalfifo_devp; static int globalfifo_open(struct inode *inode, struct file *filp) { /* 使用文件的私有数据作为获取globalfifo_dev的实例指针 */ filp->private_data = globalfifo_devp; return 0; } static int globalfifo_release(struct inode *inode, struct file *filp) { return 0; } /** * 设备ioctl函数 * @param[in] filp:文件结构体指针 * @param[in] cmd: 命令,当前仅支持MEM_CLEAR * @param[in] arg: 命令参数 * @return 若成功返回0,若出错返回错误码 */ static long globalfifo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct globalfifo_dev *dev = filp->private_data; switch (cmd) { case MEM_CLEAR: mutex_lock(&dev->mutex); dev->current_len = 0; memset(dev->mem, 0, GLOBALFIFO_SIZE); mutex_unlock(&dev->mutex); printk(KERN_INFO "globalfifo is set to zero\n"); break; default: return -EINVAL; } return 0; } /** * 读设备 * @param[in] filp:文件结构体指针 * @param[out] buf: 用户空间内存地址,不能在内核中直接读写 * @param[in] size: 读取的字节数 * @param[in/out] ppos: 读的位置相当于文件头的偏移 * @return 若成功返回实际读的字节数,若出错返回错误码 */ static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) { int ret = 0; unsigned long count = size; struct globalfifo_dev *dev = filp->private_data; DECLARE_WAITQUEUE(wait, current); mutex_lock(&dev->mutex); add_wait_queue(&dev->r_wait, &wait); while (dev->current_len == 0) { if (filp->f_flags & O_NONBLOCK) { ret = -EAGAIN; goto out; } __set_current_state(TASK_INTERRUPTIBLE); mutex_unlock(&dev->mutex); schedule(); if (signal_pending(current)) { ret = -ERESTARTSYS; goto out2; } mutex_lock(&dev->mutex); } if (count > dev->current_len) count = dev->current_len; /* 内核空间到用户空间缓存区的复制 */ if (copy_to_user(buf, dev->mem, count)) { ret = -EFAULT; goto out; } else { memcpy(dev->mem, dev->mem + count, dev->current_len - count); dev->current_len -= count; printk(KERN_INFO "read %lu bytes(s) from %u\n", count, dev->current_len); wake_up_interruptible(&dev->w_wait); ret = count; } out: mutex_unlock(&dev->mutex); out2: remove_wait_queue(&dev->r_wait, &wait); set_current_state(TASK_RUNNING); return ret; } /** * 写设备 * @param[in] filp:文件结构体指针 * @param[in] buf: 用户空间内存地址,不能在内核中直接读写 * @param[in] size: 写入的字节数 * @param[in/out] ppos: 写的位置相当于文件头的偏移 * @return 若成功返回实际写的字节数,若出错返回错误码 */ static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) { int ret = 0; unsigned long count = size; struct globalfifo_dev *dev = filp->private_data; DECLARE_WAITQUEUE(wait, current); mutex_lock(&dev->mutex); add_wait_queue(&dev->w_wait, &wait); while (dev->current_len == GLOBALFIFO_SIZE) { if (filp->f_flags & O_NONBLOCK) { ret = -EAGAIN; goto out; } __set_current_state(TASK_INTERRUPTIBLE); mutex_unlock(&dev->mutex); schedule(); if (signal_pending(current)) { ret = -ERESTARTSYS; goto out2; } mutex_lock(&dev->mutex); } if (count > GLOBALFIFO_SIZE - dev->current_len) count = GLOBALFIFO_SIZE - dev->current_len; /* 用户空间缓存区到内核空间缓存区的复制 */ if (copy_from_user(dev->mem + dev->current_len, buf, count)) { ret = -EFAULT; goto out; } else { dev->current_len += count; printk(KERN_INFO "written %lu bytes(s) from %u\n", count, dev->current_len); wake_up_interruptible(&dev->r_wait); ret = count; } out: mutex_unlock(&dev->mutex); out2: remove_wait_queue(&dev->w_wait, &wait); set_current_state(TASK_RUNNING); return ret; } /** * 文件偏移设置 * @param[in] filp:文件结构体指针 * @param[in] offset: 偏移值大小 * @param[in] orig: 起始偏移位置 * @return 若成功返回文件当前位置,若出错返回错误码 */ static loff_t globalfifo_llseek(struct file *filp, loff_t offset, int orig) { loff_t ret = 0; switch (orig) { case 0: /* 从文件头位置设置偏移 */ if (offset < 0) { ret = -EINVAL; break; } if ((unsigned int)offset > GLOBALFIFO_SIZE) { ret = -EINVAL; break; } filp->f_pos = (unsigned int)offset; ret = filp->f_pos; break; case 1: /* 从当前位置设置偏移 */ if ((filp->f_pos + offset) > GLOBALFIFO_SIZE) { ret = -EINVAL; break; } if ((filp->f_pos + offset) < 0) { ret = -EINVAL; break; } filp->f_pos += offset; ret = filp->f_pos; break; default: ret = -EINVAL; break;; } return ret; } static const struct file_operations globalfifo_fops = { .owner = THIS_MODULE, .llseek = globalfifo_llseek, .read = globalfifo_read, .write = globalfifo_write, .unlocked_ioctl = globalfifo_ioctl, .open = globalfifo_open, .release = globalfifo_release, }; static void globalfifo_setup_cdev(struct globalfifo_dev *dev, int index) { int err, devno = MKDEV(globalfifo_major, index); /* 初始化cdev */ cdev_init(&dev->cdev, &globalfifo_fops); dev->cdev.owner = THIS_MODULE; /* 注册设备 */ err = cdev_add(&dev->cdev, devno, 1); if (err) printk(KERN_NOTICE "Error %d adding globalfifo%d", err, index); } /* 驱动模块加载函数 */ static int __init globalfifo_init(void) { int ret; dev_t devno = MKDEV(globalfifo_major, 0); /* 获取设备号 */ if (globalfifo_major) ret = register_chrdev_region(devno, 1, "globalfifo"); else { ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo"); globalfifo_major = MAJOR(devno); } if (ret < 0) return ret; /* 申请内存 */ globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL); if (!globalfifo_devp) { ret = -ENOMEM; goto fail_malloc; } globalfifo_setup_cdev(globalfifo_devp, 0); mutex_init(&globalfifo_devp->mutex); init_waitqueue_head(&globalfifo_devp->r_wait); init_waitqueue_head(&globalfifo_devp->w_wait); return 0; fail_malloc: unregister_chrdev_region(devno, 1); return ret; } module_init(globalfifo_init); /* 驱动模块卸载函数 */ static void __exit globalfifo_exit(void) { cdev_del(&globalfifo_devp->cdev); kfree(globalfifo_devp); /* 释放设备号 */ unregister_chrdev_region(MKDEV(globalfifo_major, 0), 1); } module_exit(globalfifo_exit); MODULE_AUTHOR("MrLayfolk"); MODULE_LICENSE("GPL v2");
Makefile:
KVERS = $(shell uname -r) # Kernel modules obj-m += globalfifo.o # Specify flags for the module compilation. #EXTRA_CFLAGS=-g -O0 build: kernel_modules kernel_modules: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules clean: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
编译、插入ko,并进行测试:
$ make $ insmod globalfifo.ko $ mknod /dev/globalfifo c 230 0 //创建设备节点 $ cat /dev/globalfifo & //读进程在后台运行 $ echo "I want to be" > /dev/globalfifo //写进程对FIFO进行写数据 $ I want to be //cat读进程会立即打印