系统相关
首页 > 系统相关> > Linux设备驱动--异步通知

Linux设备驱动--异步通知

作者:互联网

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

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

Linux设备驱动中的异步通知和异步I/O

在设备驱动中使用异步通知可以使得在对设备访问时,由驱动主动通知应用程序进行访问

使用非阻塞I/O的应用程序不需要轮训设备是否可以访问,而阻塞访问可以被类似“中断”的异步通知所取代。

除了异步通知以外,应用还可以在发起I/O请求后,立即返回。之后,再查询I/O完成情况,或者I/O完成后被调回。这个过程叫作异步I/O。

1.1 异步通知简介

异步通知:一旦设备就绪,则主动通知应用程序,应用程序不需要查询设备状态,较为准确的称呼为“信号驱动的异步I/O”。

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达。

阻塞I/O意味着一直等待设备可访问后再访问。非阻塞I/O使用poll()意味着查询设备是否可访问。

下图呈现了阻塞I/O,结合轮询的非阻塞I/O,以及基于SIGIO的异步通知在时间先后顺序上的不同。

image-20220130202942548

1.2 Linux异步通知编程

1.2.1 Linux信号

Linux中异步通知使用信号来实现,Linux中可用的信号及其定义如下:

image-20220130210010063

image-20220130210133027

除了SIGSTOP和SIGKILL两个信号外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理它。如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。

1.2.2 信号的接收

在用户程序中,捕获信号可以使用signal()函数来设置对应信号的处理函数,函数原型为:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数signum:指定信号的值

参数handler:指定针对信号值的处理函数,若为SIG_IGN,表示忽略该信号;若为SIG_DFL,表示采用系统默认方式处理信号;若为用户自定义的函数,则信号捕获后,该函数被执行。

返回值:如果调用成功,返回最后一次为信号signum绑定的处理函数的handler值,失败则返回SIG_ERR。

在进程执行时,按下Ctrl+C将向其发出SIGINT信号,正在运行kill的进程将向其发出SIGTERM信号,捕获这两个信号并输出信号值的代码如下:

void sigterm_handler(int signo)
{
	printf("Have caught sig N.0.%d\n", signo);
    exit(0);
}

int main(void)
{
    signal(SIGINT, sigterm_handler);
    signal(SIGTERM, sigterm_handler);
    while(1);
    
    return 0;
}

编译、测试:

$ ./a.out 
^C^CHave caught sig N.0.2

sigaction()函数:可用于改变进程接收到特定信号后的行为,函数原型为:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数signum:信号的值,可以为SIGKILL以及SIGSTOP外的任何一个特定有效的信号。

参数act:指向结构体sigaction的一个实例的指针,在结构体sigaction的实例中指定了对特定信号的处理函数,若为空,则进程会以缺省方式对信号处理。

参数oldact:指向的对象用来保存原来对相应信号的处理函数,可指定为NULL。

如果把第二、第三个参数设置为NULL,该函数可用于检查信号的有效性。

返回值:成功返回0,失败返回-1。

1.2.3 异步通知的应用程序实例

通过signal(SIGIO, input_handler)对标准输入文件描述符STDIN_FIFLNO启动信号机制。用户输入后,应用程序将接收到SIGIO信号,其处理函数input_handler()将被调用。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define MAX_LEN  100

void input_handler(int num)
{
    char data[MAX_LEN];
    int len;

    /* 读取并输出STDIN_FIFLNO */
    len = read(STDIN_FILENO, &data, MAX_LEN);
    data[len] = 0;
    printf("input data:%s\n", data);
}

int main()
{
    int oflags;

    /* 启动信号驱动机制 */
    signal(SIGIO, input_handler); //SIGIO信号安装input_handler()作为处理函数
    fcntl(STDIN_FILENO, F_SETOWN, getpid());  //设置本进程为STDIN_FIFENO文件的拥有者
    oflags = fcntl(STDIN_FILENO, F_GETFL);  
    fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);  //设置FASYNC

    /* 最后进入一个死循环,保持进程不终止 */
    while(1);
}

编译、测试:

$ gcc sigio_handler.c 
$ ./a.out 
i am chinese.  
input data:i am chinese.

i love linux driver    
input data:i love linux driver

可以看出,用户输入一串字符之后,标准输入设备释放SIGIO信号,这个信号“中断”驱使应用程序中的input_handler()得以执行,并在用户输入显示出来。

因此,在用户空间中处理一个设备释放的信号,必须完成以下三项内容:

1)通过F_SETOWN I/O控制命令设置设备文件的拥有者为本进程,这样从设备驱动发出的信号才能被本进程接收到。

2)通过F_SETFL I/O控制命令设备设备文件以支持FASYNC,即异步通知模式。

3)通过signal()函数连接信号和信号处理函数。

1.2.4 设备驱动中异步通知

在设备驱动和应用程序的交互中,信号的源头在设备驱动端,捕获在应用程序端。因此,信号的释放应该在设备驱动中进行。

设备支持异步通知机制,驱动程序需要支持以下三项工作:

1)支持F_SETOWN命令,能在这个控制命令中处理file->f_owner为对应进程的ID,这个已经由内核完成,设备驱动无须处理。

2)支持F_SETFL命令,每当FASYNC标志改变时,驱动程序中的fasync()函数得到执行,设备驱动中应该实现fasync()函数。

3)在设备资源可获得时,调用kill_fasync()函数激发对应的信号。

驱动中的三项工作和应用程序中的三项工作是一一对应的,设备驱动异步通知处理和用户空间交互过程如下:

image-20220201111407146

设备驱动中异步编程的函数涉及一个数据结构fasync_struct和两个函数fasync_helper()和kill_fasync():

(1)处理FASYNC标志变更的函数

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp);

(2)释放信号函数

void kill_fasync(struct fasync_struct **fp, int sig, int band);

在设备资源可以获得时,应该调用kill_fasync()释放SIGIO信号。在可读时,第三个参数band设置为POLL_IN;在可写时,第三个参数band设置为POLL_OUT。

fasync_struct数据结构体指针一般放在设备结构体中,支持异步通知的设备结构体模板如下:

struct  xxx_dev {
    struct cdev cdev;
    ... ....;
    struct fasync_struct *async_queue;  /* 异步结构体指针 */
    ... ...;
};

设备驱动的faync()函数:

int (*fasync) (int, struct file *, int);

支持异步通知的设备驱动程序fasync()函数的模板:

static int xxx_fasync(int fd, struct file *filp, int mode)
{
    struct xxx_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

支持异步通知的设备驱动信号释放模板:

在资源可获得时,调用kill_fasync()函数释放SIGIO信号。

static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct xxx_dev *dev = filp->private_data;
    ... ...;
    
    /* 产生异步读信号 */
    if (dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}

最后在文件关闭时,在设备的release()函数中,调用设备驱动的fasync()函数将文件从异步通知的列表中删除。支持异步通知的设备驱动release()函数模板如下:

static int xxx_release(struct inode *inode, struct file *filp)
{
	/* 将文件从异步通知的列表中删除 */
    xxx_fasync(-1, filp, 0);
    ... ...;
    return 0;
}

1.3 支持异步通知的globalfifo驱动

1.3.1 设备驱动globalfifo的改写

首先,需要将异步数据结构体指针添加到globalfifo_dev设备结构体中:

/* 设备结构体 */
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 fasync_struct *async_queue;  /* 异步结构体指针 */
};

然后,编写异步通知的globalfifo设备驱动的fasync()函数:

static int globalfifo_fasync(int fd, struct file *filp, int mode)
{
    struct globalfifo_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

然后,改写globalfifo设备驱动的写函数,在globalfifo设备被正确写入之后,可以进行读取,需要支持释放SIGIO信号,通过给应用程序捕获。

/**
 * 写设备
 * @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 count, loff_t *ppos)
{
    int ret = 0;
    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);
        if (dev->async_queue) {
            kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
            printk(KERN_INFO "%s kill SIGIO\n", __func__);
        }
        ret = count;
    }

out:
    mutex_unlock(&dev->mutex);

out2:
    remove_wait_queue(&dev->w_wait, &wait);
    set_current_state(TASK_RUNNING);
    return ret;
}

globalfifo设备驱动的release()函数需要调用globalfifo_fasync()函数将文件从异步通知列表中删除:

static int globalfifo_release(struct inode *inode, struct file *filp)
{
	/* 将文件从异步通知的列表中删除 */
    globalfifo_fasync(-1, filp, 0);
    return 0;
}

完整的设备驱动代码:

#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 fasync_struct *async_queue;  /* 异步结构体指针 */
};

struct globalfifo_dev *globalfifo_devp;

static int globalfifo_fasync(int fd, struct file *filp, int mode)
{
    struct globalfifo_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

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)
{
    /* 将文件从异步通知的列表中删除 */
    globalfifo_fasync(-1, filp, 0);
    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 count, loff_t *ppos)
{
    int ret = 0;
    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);
        if (dev->async_queue) {
            kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
            printk(KERN_INFO "%s kill SIGIO\n", __func__);
        }
        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,
    .fasync = globalfifo_fasync,
};

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");

1.3.2 用户空间globalfifo驱动验证

用户空间应用程序实现在接收到globalfifo发出的信号后输出信号值。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define MAX_LEN  100

static void signalio_handler(int signum)
{
    printf("receive a signal, signal number:%d\n", signum);
}

int main()
{
    int fd, oflags;

    /* 以非阻塞方式打开设备文件 */
    fd = open("/dev/globalfifo", O_RDONLY | S_IRUSR | S_IWUSR);
    if (fd != -1) {
        /* 启动信号驱动机制 */
        signal(SIGIO, signalio_handler); //SIGIO信号安装input_handler()作为处理函数
        fcntl(fd, F_SETOWN, getpid());  //设置本进程为STDIN_FIFENO文件的拥有者
        oflags = fcntl(fd, F_GETFL);  
        fcntl(fd, F_SETFL, oflags | FASYNC);  //设置FASYNC    
        while (1) {
            sleep(100);
        }
    } else {
        printf("device open failure\n");
    }
    
    return 0;
}

1.3.3 编译测试

(1)编译设备驱动ko,并插入ko

$ make
$ insmod globalfifo.ko 

(2)创建设备节点

$ mknod /dev/globalfifo c 230 0 

(3)编译用户程序,并且运行

$ gcc globalfifo_app.c 
$ ./a.out

(4)向设备驱动写入数据,signalio_handler()会被调用

$ echo "hello" > /dev/globalfifo
$ echo "hello" > /dev/globalfifo
$ echo "hello" > /dev/globalfifo
$ ./a.out              
receive a signal, signal number:29
receive a signal, signal number:29
receive a signal, signal number:29

标签:异步,filp,struct,--,globalfifo,dev,int,Linux,wait
来源: https://www.cnblogs.com/mrlayfolk/p/15858989.html