注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。
书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)
字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。对于用户而言,使用文件系统的操作接口open()、close()、read()、write()等进行访问。
在用户态程序中,可使用select()和poll()系统调用来查询是否可对设备进行无阻塞的访问。通过select()和poll()实现的是I/O多路复用功能。
I/O 多路复用允许我们同时检查多个文件描述符,看其中任意一个是否可执行I/O操作。我们可以在普通文件、终端、伪终端、管道、FIFO、套接字以及一些其他类型的字符型设备上使用 select()和 poll()来检查文件描述符。这两个系统调用都允许进程要么一直等待文件描述符成为就绪态,要么在调用中指定一个超时时间。
在内核中,设备驱动中的poll()会被用户态的select()和poll()调用。
应用程序中使用最广泛的系统调用是select()函数,原型为:
/* According to POSIX.1-2001 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
系统调用select()会一直阻塞,直到一个或多个文件描述符集合成为就绪态。
参数nfds、readfds、writefds、exceptfds指定了select()要检查的文件描述符集合。
参数readfds:是用来检测输入是否就绪的文件描述符集合。
参数writefds:是用来检测输出是否就绪的文件描述符集合。
参数exceptfds:是用来检测异常情况是否发生的文件描述符集合。
参数nfds:必须设为比3个文件描述符集合中所包含的最大文件描述符号还要大1。
所有关于文件描述符集合的操作都是通过四个宏来完成的:FD_ZERO(),FD_SET(),FD_CLR()以及FD_ISSET()。
void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);
FD_ZERO():将fdset所指向的集合初始化为空。
FD_SET():将文件描述符fd添加到由fdset所指向的集合中。
FD_CLR():将文件描述符fd从fdset所指向的集合中移除。
如果文件描述符fd是fdset所指向的集合中的成员,FD_ISSET()返回 true。
参数timeout:用来设定select()阻塞的时间上限,struct timeval数据结构定义如下:
struct timeval { int tv_sec; int tv_usec; }
timeout的两个域都设置为0,此时select()不会阻塞,只是简单的轮询指定的文件描述符集合,看其中是否有就绪的文件描述符并立即返回。
select()多路复用如下:
第一次进行读写时,若任何一个文件满足读写要求,select()就直接返回;
第二次进行select()时,没有文件满足读写要求,select()的进程阻塞且睡眠。
poll()函数的功能和select()相似,其函数原型为:
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll()中提供一列文件描述符,并在每个文件描述符上标明感兴趣的事件。
参数fds:列出需要poll()检查的文件描述符,该参数为pollfd结构体数组,其定义为:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events bit mask */ short revents; /* returned events bit mask */ };
参数nfds:指定了数组fds中元素的个数。
参数timeout :参数timeout 决定了poll()的阻塞行为,具体如下。
如果timeout等于−1,poll()会一直阻塞直到 fds 数组中列出的文件描述符有一个达到就绪态(定义在对应的 events字段中)或者捕获到一个信号。
如果timeout等于 0,poll()不会阻塞—只是执行一次检查看看哪个文件描述符处于就绪态。
如果timeout大于0,poll()至多阻塞timeout毫秒,直到 fds 列出的文件描述符中有一个达到就绪态,或者直到捕获到一个信号为止。
poll()的返回值:作为函数的返回值,poll()会返回如下几种情况中的一种。
返回−1:表示有错误发生。一种可能的错误是 EINTR,表示该调用被一个信号处理例程中断。(如果被信号处理例程中断,poll()绝不会自动恢复。)
返回0:表示该调用在任意一个文件描述符成为就绪态之前就超时了。
返回正整数:表示有1个或多个文件描述符处于就绪态了。返回值表示数组fds中拥有非零revents字段的pollfd结构体数量。
当多路复用的文件数量庞大、I/O流量频繁的时候,一般不太适合使用select()和poll()),此种情况下,
select()和poll()的性能表现较差,宜使用epoll。
与epoll相关的用户编程接口:
epoll_create()用于创建一个epoll句柄,size指定要监听多少个fd。
#include <sys/epoll.h> int epoll_create(int size);
当创建好epoll句柄后,它本身也会占用一个fd值,因此在使用完epoll后,需要调用close()关闭。
epoll_ctl()用于告诉内核监听的事件类型。
#include <sys/epoll.h>s int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数epfd:为epoll_create()函数的返回值。
参数op:表示动作,包括:
EPOLL_CTL_ADD:注册新的fd到epfd中。 EPOLL_CTL_MOD:修改已经注册的fd的监听事件。 EPOLL_CTL_DEL:从epfd中删除一个fd。
参数fd:是需要监听的fd
参数event:是告诉内核需要监听的事件类型。struct epoll_event结构如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }
events可以是以下几个宏的“或”:
EPOLLIN:表示对应的文件描述符可以读。
EPOLLOUT:表示对应的文件描述符可以写。
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示的是有socket带外数据到来)。
EPOLLERR:表示对应的文件描述符发生错误。
EPOLLHUP:表示对应的文件描述符被挂断。
EPOLLET:将epoll设为边缘触发(Edge Triggered)模式,这 是相对于水平触发(Level Triggered)来说的。LT(Level Triggered)是缺省的工作方式,在LT情况下,内核告诉用户一个fd是否就绪了,之后用户可以对这个就绪的fd进行I/O操作。但是如果用户不进行任何操作,该事件并不会丢失,而ET(Edge-Triggered)是高速工作方式,在这种模式下,当fd从未就绪变为就绪时,内核通过epoll告诉用户,然后它会假设用户知道fd已经就绪,并且不会再为那个fd发送更多的就绪通知。
EPOLLONESHOT:意味着一次性监听,当监听完这次事件之后,如果还需要继续监听这个fd的话,需要再次把这个fd加入到epoll队列里。
epoll_wait()函数用于等待事件的产生:
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数events:输出参数,用来从内核得到事件的集合。
参数maxevents:告诉内核本次最多收多少个事件,maxevents不能大于创建epoll_creat()时的size。
参数timeout:超时时间(以毫秒为单位,0:立即返回;-1:永久等待)
函数返回值:需要处理的事件数目,如返回0,则表示已经超时。
设备驱动中file_operations数据结构中的poll()函数原型:
unsigned int(*poll)(struct file *filp, struct poll_table *wait);
参数filp:file结构体指针
参数wait:轮询表指针
返回值:表示是否能对设备进行无阻塞读、写访问的掩码。
poll_wait()函数用于向poll_table注册等待队列,函数的原型为:
void poll_wait(struct file * filp, wait_queue_head_t * queue, poll_table *wait);
poll_wait函数不会阻塞的等待某件事发生,仅把当前进程添加到wait参数指定的等待列表(poll_table)中,实际作用是让唤醒参数queue所对应的等待队列可以唤醒因select()而睡眠的进程。
驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如POLLIN(定义为0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为0x0004)意味着设备可以无阻塞地写。
poll()函数的典型模板:
static unsigned int xxx_poll(struct file *filp, poll_table *wait) { unsigned int mask = 0; struct xxx_dev *dev = filp->private_data; /* 获得设备结构体指针 */ ... poll_wait(filp, &dev->r_wait, wait); /* 加入读等待队列 */ poll_wait(filp, &dev->w_wait, wait); /* 加入写等待队列*/ if (...) /* 可读 */ mask |= POLLIN | POLLRDNORM; /* 标示数据可获得(对用户可读) */ if (...) /* 可写 */ mask |= POLLOUT | POLLWRNORM; /* 标示数据可写入 */ ... return mask; }
在globalfifo的代码中增加poll()函数。
完整代码如下:
#include <linux/module.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/poll.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[in] wait: 轮询表指针 * @return 返回位掩码指示是否非阻塞的读或写是可能的 */ static unsigned int globalfifo_poll(struct file *filp, struct poll_table_struct *wait) { unsigned int mask = 0; struct globalfifo_dev *dev = filp->private_data; mutex_lock(&dev->mutex); /* 调用select而阻塞的进程可以被r_wait和w_wait唤醒 */ poll_wait(filp, &dev->r_wait, wait); poll_wait(filp, &dev->w_wait, wait); if (dev->current_len != 0) { /* 设备可以无阻塞的读,正常数据可用来读 */ mask |= POLLIN | POLLRDNORM; } if (dev->current_len != GLOBALFIFO_SIZE) { /* 设备可以无阻塞的写 */ mask |= POLLOUT | POLLWRNORM; } mutex_unlock(&dev->mutex); return mask; } /** * 读设备 * @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, .poll = globalfifo_poll, }; 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_poll.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
在用户空间编写一个应用程序调用select()来监控globalfifo的可读写状态。
完整代码如下:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #define FIFO_CLEAR 0x1 #define BUFFER_LEN 20 int main(void) { int fd, num; char rd_ch[BUFFER_LEN]; fd_set rfds, wfds; /* 读/写文件描述符集 */ /* 以非阻塞方式打开设备文件 */ fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK); if (fd != -1) { /* FIFO清0 */ if (ioctl(fd, FIFO_CLEAR) < 0) printf("ioctl command failed!\n"); while (1) { sleep(2); FD_ZERO(&rfds); //将rfds所指向的集合初始化为空 FD_ZERO(&wfds); //将wfds所指向的集合初始化为空 FD_SET(fd, &rfds); //将文件描述符fd添加到由rfds所指向的集合 FD_SET(fd, &wfds); //将文件描述符fd添加到由wfds所指向的集合 select(fd + 1, &rfds, &wfds, NULL, NULL); /* 数据可获得 */ if (FD_ISSET(fd, &rfds)) printf("Poll monitor: can be read!\n"); /* 数据可写入 */ if (FD_ISSET(fd, &wfds)) printf("Poll monitor: can be written!\n"); } } else { printf("Device open failure\n"); } return 0; }
编译设备驱动程序并加载ko,然后创建一个字符设备节点:
$ make $ insmod globalfifo_poll.ko $ mknod /dev/globalfifo c 230 0
编译用户态应用程序,并且运行:
$ gcc app_poll.c $ ./a.out Poll monitor: can be written! Poll monitor: can be written! Poll monitor: can be written!
刚开始运行时,设备只能进行写操作,然后让设备空间写一些字符,设备变为可读可写,然后读取设备空间字符,设备变为只能进行写操作。
$ ./a.out Poll monitor: can be written! Poll monitor: can be written! Poll monitor: can be written! $ echo "hello" > /dev/globalfifo Poll monitor: can be written! Poll monitor: can be read! Poll monitor: can be written! Poll monitor: can be read! $ cat /dev/globalfifo hello Poll monitor: can be written! Poll monitor: can be written!