Linux教程

Linux设备驱动--轮询操作

本文主要是介绍Linux设备驱动--轮询操作,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。

书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。对于用户而言,使用文件系统的操作接口open()、close()、read()、write()等进行访问。

Linux设备驱动-轮询操作

1.1 简介

在用户态程序中,可使用select()和poll()系统调用来查询是否可对设备进行无阻塞的访问。通过select()和poll()实现的是I/O多路复用功能。

I/O 多路复用允许我们同时检查多个文件描述符,看其中任意一个是否可执行I/O操作。我们可以在普通文件、终端、伪终端、管道、FIFO、套接字以及一些其他类型的字符型设备上使用 select()和 poll()来检查文件描述符。这两个系统调用都允许进程要么一直等待文件描述符成为就绪态,要么在调用中指定一个超时时间。

在内核中,设备驱动中的poll()会被用户态的select()和poll()调用。

1.2 应用程序中的轮询编程

1.2.1 select()函数

应用程序中使用最广泛的系统调用是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()多路复用如下:

image-20220130140213539

第一次进行读写时,若任何一个文件满足读写要求,select()就直接返回;

第二次进行select()时,没有文件满足读写要求,select()的进程阻塞且睡眠。

1.2.2 poll()函数

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结构体数量。

1.2.3 epoll()函数

当多路复用的文件数量庞大、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,则表示已经超时。

1.3 设备驱动中的轮询编程

1.3.1 poll()函数原型

设备驱动中file_operations数据结构中的poll()函数原型:

unsigned int(*poll)(struct file *filp, struct poll_table *wait);

参数filp:file结构体指针

参数wait:轮询表指针

返回值:表示是否能对设备进行无阻塞读、写访问的掩码。

1.3.2 poll_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)意味着设备可以无阻塞地写。

1.3.3 poll()函数使用调用模板

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; 
}	

1.4 支持轮询操作的globalfifo驱动

1.4.1 内核设备驱动globalfifo

在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

1.4.2 用户空间验证globalfifo设备轮询

在用户空间编写一个应用程序调用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;
}

1.4.3 编译测试

编译设备驱动程序并加载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!
这篇关于Linux设备驱动--轮询操作的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!