【Linux 驱动】异步通知机制

异步通知机制:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,是一种“信号驱动的异步I/O”。

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候会到达。

我们试图通过两个方面来分析异步通知机制:
从用户程序的角度考虑:
为了启动文件的异步通知机制,用户程序必须执行两个步骤。首先,她们指定一个进程作为文件的“属主(owner)”。当进程使用fcntl系统调用执行F_SETOWN命令时,属主进程的进程ID号就被保存在filp->f_owner中。这一步是必需的,目的是为了让内核知道应该通知哪个进程。
然后为了真正启动异步通知机制,用户程序还必须在设备中设置FASYNC标志,这通过fcntl的F_SETFL命令完成的。
执行完这两个步骤之后,输入文件就可以在新数据到达时请求发送一个SIGIO信号。该信号被发送到存放在filp->f_owner中的进程(如果是负值就是进程组)。

下面先贴出用户程序代码:

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

void input_handler(int signum)
{
    printf("receive a signal from io,signalnum:%d\n", signum);
}

int main(void)
{
    int fd, flag;
    fd_set r_fset, w_fset;

    fd = open("/dev/wqlkp", O_RDWR, S_IRUSR | S_IWUSR);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    //启动信号驱动机制
    signal(SIGIO, input_handler);//让input_handler()处理SIGIO信号
    fcntl(fd, F_SETOWN, getpid());//设置文件的所有权进程
    flag = fcntl(fd, F_GETFL);//获取状态标志
    fcntl(fd, F_SETFL, flag | FASYNC);//设置FASYNC标志

    while(1);
    return 0;
}

我们先通过内核源码,剖析上面的实现原理。
先看fcntl()
其调用步骤为:fcntl() -> sys_fcntl() -> do_fcntl() -> 根据cmd调用相应操作函数。
先看sys_fcntl() [linux/fcntl.c]

asmlinkage long sys_fcntl(unsigned int fd, unsigned int cmd, unsigned long arg)
{   
    struct file *filp;
    long err = -EBADF;

    filp = fget(fd);//通过文件描述符获得对应关联的文件指针
    if (!filp)
        goto out;

    err = security_file_fcntl(filp, cmd, arg);
    if (err) {
        fput(filp);
        return err;
    }

    err = do_fcntl(fd, cmd, arg, filp);//调用do_fcntl函数

    fput(filp);
out:
    return err;
}

//只保留与异步通知机制相关的部分代码
static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
        struct file *filp)
{
    long err = -EINVAL;

    switch (cmd) {
    ……
    case F_GETFL:
        err = filp->f_flags;//返回文件标志
        break;
    case F_SETFL:
        err = setfl(fd, filp, arg);//转调用setfl函数
        break;
    ……
    case F_SETOWN:
        err = f_setown(filp, arg, 1);//转调用f_setown函数
        break;
    ……
    default:
        break;
    }
    return err;
}

ok,来看看f_setown函数的内部实现:设置文件的属主进程

int f_setown(struct file *filp, unsigned long arg, int force)
{
    int err;

    err = security_file_set_fowner(filp);
    if (err)
        return err;

    f_modown(filp, arg, current->uid, current->euid, force);//调用f_modown函数
    return 0;
}


static void f_modown(struct file *filp, unsigned long pid,
                     uid_t uid, uid_t euid, int force)
{
    write_lock_irq(&filp->f_owner.lock);
    //设置对应的pid,uid,euid
    if (force || !filp->f_owner.pid) {
        filp->f_owner.pid = pid;
        filp->f_owner.uid = uid;
        filp->f_owner.euid = euid;
    }
    write_unlock_irq(&filp->f_owner.lock);
}

再来看看setfl函数的内部实现:

//只保留异步通知机制相关的代码
static int setfl(int fd, struct file * filp, unsigned long arg)
{
    struct inode * inode = filp->f_dentry->d_inode;
    int error = 0;
    ……
    lock_kernel();
    //下面这个判断语句有点意思,是一个边缘触发
    //也就是说FASYNC标志从0变为1的时候,才为真。自己验证...
    if ((arg ^ filp->f_flags) & FASYNC) {//FASYNC标志发生了变化
        if (filp->f_op && filp->f_op->fasync) {
            error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);//前面接触了这么多内核驱动接口,看到filp->f_op->fasync,应该知道这是调用我们注册的自定义fasync函数了
            if (error < 0)
                goto out;
        }
    }

    filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK);
 out:
    unlock_kernel();
    return error;
}

好,有了前面的铺垫,我们在从驱动层次考虑:
从驱动程序角度考虑:

  1. F_SETOWN被调用时对filp->f_owner赋值,此外什么也不做;
  2. 在执行F_SETFL启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标识发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其能正确响应。文件打开时,FASYNC标志被默认为是清除的。
  3. 当数据到达时,所有注册为异步通知的进程都会被发送一个SIGIO信号。

Linux的这种通用方法基于一个数据结构和两个函数:

struct fasync_struct {
    int magic;
    int fa_fd;//文件描述符
    struct  fasync_struct   *fa_next; /* singly linked list *///异步通知队列
    struct  file        *fa_file;//文件指针
};

/* SMP safe fasync helpers: */
extern int fasync_helper(int, struct file *, int, struct fasync_struct **);
/* can be called from interrupts */
extern void kill_fasync(struct fasync_struct **, int, int);

当一个打开的文件的FASYNC标志被修改时,调用fasync_helper函数以便从相关的进程表中添加或删除文件。
当数据到达时,可使用kill_fasync函数通知所有的相关进程。(在UNIX语义中,kill这个词常用来向进程发送信号,而不是杀死某个进程,raise则用于向自身发送信号)。

基本上,所有工作都有内核提供的这两个函数完成了。
现在我们来看看驱动程序:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include <linux/semaphore.h>
#include <linux/fcntl.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DEV_SIZE 20
#define WQ_MAJOR 230

#define DEBUG_SWITCH 1
#if DEBUG_SWITCH
    #define P_DEBUG(fmt, args...)  printk("<1>" "<kernel>[%s]"fmt,__FUNCTION__, ##args)
#else
    #define P_DEBUG(fmt, args...)  printk("<7>" "<kernel>[%s]"fmt,__FUNCTION__, ##args)
#endif

struct wq_dev{
    char kbuf[DEV_SIZE];//缓冲区
    dev_t devno;//设备号
    unsigned int major;
    struct cdev wq_cdev;
    unsigned int cur_size;//可读可写的数据量
    struct semaphore sem;//信号量
    wait_queue_head_t r_wait;//读等待队列
    wait_queue_head_t w_wait;//写等待队列
    struct fasync_struct *async_queue;//异步通知队列 
};

//struct wq_dev *wq_devp;

//异步通知机制驱动函数
static int wq_fasync(int fd, struct file *filp, int mode)
{
    struct wq_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);//调用内核提供的函数
}

int wq_open(struct inode *inodep, struct file *filp)
{
    struct wq_dev *dev;
    dev = container_of(inodep->i_cdev, struct wq_dev, wq_cdev);
    filp->private_data = dev;

    printk(KERN_ALERT "open is ok!\n");
    return 0;
}

int wq_release(struct inode *inodep, struct file *filp)
{
    printk(KERN_ALERT "release is ok!\n");
    wq_fasync(-1, filp, 0);//从异步通知队列中删除该filp
    return 0;
}

static ssize_t wq_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
    struct wq_dev *dev = filp->private_data;

    P_DEBUG("read data...\n");

    if(down_interruptible(&dev->sem))//获取信号量
    {
        P_DEBUG("enter read down_interruptible\n");
        return -ERESTARTSYS;
    }
    P_DEBUG("read first down\n");
    while(dev->cur_size == 0){//无数据可读,进入休眠lon
        up(&dev->sem);//释放信号量,不然写进程没有机会来唤醒(没有获得锁)
        if(filp->f_flags & O_NONBLOCK)//检查是否是阻塞型I/O
            return -EAGAIN;
        P_DEBUG("%s reading:going to sleep\n", current->comm);
        if(wait_event_interruptible(dev->r_wait, dev->cur_size != 0))//休眠等待被唤醒
        {
            P_DEBUG("read wait interruptible\n");
            return -ERESTARTSYS;
        }
        P_DEBUG("wake up r_wait\n");
        if(down_interruptible(&dev->sem))//获取信号量
            return -ERESTARTSYS;
    }

    //数据已就绪
    P_DEBUG("[2]dev->cur_size is %d\n", dev->cur_size);
    if(dev->cur_size > 0)
        count = min(count, dev->cur_size);

    //从内核缓冲区赋值数据到用户空间,复制成功返回0
    if(copy_to_user(buf, dev->kbuf, count))
    {
        up(&dev->sem);
        return -EFAULT;
    }   
    dev->cur_size -= count;//可读数据量更新
    up(&dev->sem);
    wake_up_interruptible(&dev->w_wait);//唤醒写进程
    P_DEBUG("%s did read %d bytes\n", current->comm, (unsigned int)count);
    return count;
}

static ssize_t wq_write(struct file *filp,const char __user *buf,size_t count, loff_t *offset)
{
    struct wq_dev *dev = filp->private_data;
    //wait_queue_t my_wait;
    P_DEBUG("write is doing\n");    
    if(down_interruptible(&dev->sem))//获取信号量
    {
        P_DEBUG("enter write down_interruptible\n");
        return -ERESTARTSYS;
    }

    P_DEBUG("write first down\n");
    while(dev->cur_size == DEV_SIZE){//判断空间是否已满

        up(&dev->sem);//释放信号量
        if(filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        P_DEBUG("writing going to sleep\n");
        if(wait_event_interruptible(dev->w_wait, dev->cur_size < DEV_SIZE))
            return -ERESTARTSYS;

        if(down_interruptible(&dev->sem))//获取信号量
            return -ERESTARTSYS;
    }
    if(count > DEV_SIZE - dev->cur_size)
        count = DEV_SIZE - dev->cur_size;

    if(copy_from_user(dev->kbuf, buf, count))//数据复制
        return -EFAULT;
    dev->cur_size += count;//更新数据量
    P_DEBUG("write %d bytes , cur_size:[%d]\n", count, dev->cur_size);
    P_DEBUG("kbuf is [%s]\n", dev->kbuf);
    up(&dev->sem);
    wake_up_interruptible(&dev->r_wait);//唤醒读进程队列

    if(dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);//可写时发送信号

    return count;
}

static unsigned int wq_poll(struct file *filp, poll_table *wait)
{
    struct wq_dev *dev = filp->private_data;
    unsigned int mask = 0;

    if(down_interruptible(&dev->sem))//获取信号量
        return -ERESTARTSYS;
    poll_wait(filp, &dev->w_wait, wait);//添加写等待队列
    poll_wait(filp, &dev->r_wait, wait);//添加读等待队列

    if(dev->cur_size != 0)//判断是否可读取
        mask |= POLLIN | POLLRDNORM;
    if(dev->cur_size != DEV_SIZE)//判断是否可写入
        mask |= POLLOUT | POLLWRNORM;

    up(&dev->sem);//释放信号量
    return mask;
}
struct file_operations wq_fops = {
    .open = wq_open,
    .release = wq_release,
    .write = wq_write,
    .read = wq_read,
    .poll = wq_poll,
    .fasync = wq_fasync,//函数注册
};

struct wq_dev my_dev;

static int __init wq_init(void)
{
    int result = 0;
    my_dev.cur_size = 0;
    my_dev.devno = MKDEV(WQ_MAJOR, 0);
    //设备号分配
    if(WQ_MAJOR)
        result = register_chrdev_region(my_dev.devno, 1, "wqlkp");
    else
    {
        result = alloc_chrdev_region(&my_dev.devno, 0, 1, "wqlkp");
        my_dev.major = MAJOR(my_dev.devno);
    }
    if(result < 0)
        return result;

    cdev_init(&my_dev.wq_cdev, &wq_fops);//设备初始化
    my_dev.wq_cdev.owner = THIS_MODULE;
    sema_init(&my_dev.sem, 1);//信号量初始化
    init_waitqueue_head(&my_dev.r_wait);//等待队列初始化
    init_waitqueue_head(&my_dev.w_wait);

    result = cdev_add(&my_dev.wq_cdev, my_dev.devno, 1);//设备注册
    if(result < 0)
    {
        P_DEBUG("cdev_add error!\n");
        goto err;
    }
    printk(KERN_ALERT "hello kernel\n");
    return 0;

err:
    unregister_chrdev_region(my_dev.devno,1);
    return result;
}

static void __exit wq_exit(void)
{
    cdev_del(&my_dev.wq_cdev);
    unregister_chrdev_region(my_dev.devno, 1);
}

module_init(wq_init);
module_exit(wq_exit);

我们通过剖析这两个函数的内部实现来窥探异步通知机制原理

//异步通知机制驱动函数,我们自定义的驱动程序,可以看到主体是fasync_helper函数
static int wq_fasync(int fd, struct file *filp, int mode)
{
    struct wq_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);//调用内核提供的函数
}


/*
 * fasync_helper() is used by some character device drivers (mainly mice)
 * to set up the fasync queue. It returns negative on error, 0 if it did
 * no changes and positive if it added/deleted the entry.
 */
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
    struct fasync_struct *fa, **fp;
    struct fasync_struct *new = NULL;
    int result = 0;

    if (on) {
        new = kmem_cache_alloc(fasync_cache, SLAB_KERNEL);//创建对象,slab分配器
        if (!new)
            return -ENOMEM;
    }
    write_lock_irq(&fasync_lock);
    //遍历整个异步通知队列,看是否存在对应的文件指针
    for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {
        if (fa->fa_file == filp) {//已存在
            if(on) {
                fa->fa_fd = fd;//文件描述符赋值
                kmem_cache_free(fasync_cache, new);//销毁刚创建的对象
            } else {
                *fp = fa->fa_next;//继续遍历
                kmem_cache_free(fasync_cache, fa);//删除非目标对象
                result = 1;
            }
            goto out;//找到了
        }
    }

//看到下面可以得知,所谓的把进程添加到异步通知队列中
//实则是将文件指针关联到异步结构体对象,然后将该对象挂载在异步通知队列中(等待队列也是这个原理)
//那么最后发送信号又是怎么知道是哪个进程的呢?我们看后面的kill_fasync函数。

    if (on) {//不存在
        new->magic = FASYNC_MAGIC;
        new->fa_file = filp;//指定文件指针
        new->fa_fd = fd;//指定文件描述符
        new->fa_next = *fapp;//挂载在异步通知队列中
        *fapp = new;//挂载
        result = 1;
    }
out:
    write_unlock_irq(&fasync_lock);
    return result;
}

看看kill_fasync函数是怎么将信号通知指定进程的:

//我们自定义代码
if(dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);//可写时发送信号


void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
    /* First a quick test without locking: usually
     * the list is empty.
     */
    if (*fp) {
        read_lock(&fasync_lock);
        /* reread *fp after obtaining the lock */
        __kill_fasync(*fp, sig, band);//调用
        read_unlock(&fasync_lock);
    }
}


void __kill_fasync(struct fasync_struct *fa, int sig, int band)
{
    while (fa) {
        struct fown_struct * fown;
        if (fa->magic != FASYNC_MAGIC) {
            printk(KERN_ERR "kill_fasync: bad magic number in "
                   "fasync_struct!\n");
            return;
        }

/*  
struct fown_struct {
    rwlock_t lock;          // protects pid, uid, euid fields 
    int pid;        // pid or -pgrp where SIGIO should be sent 
    uid_t uid, euid;    // uid/euid of process setting the owner 
    void *security;
    int signum;     // posix.1b rt signal to be delivered on IO 
};
*/
        fown = &fa->fa_file->f_owner;//这里便是回答上面的问题,如果知道是哪个进程的,通过异步对象的文件指针知道其属主进程
        /* Don't send SIGURG to processes which have not set a
           queued signum: SIGURG has its own default signalling
           mechanism. */
        if (!(sig == SIGURG && fown->signum == 0))
            send_sigio(fown, fa->fa_fd, band);//发送信号
        fa = fa->fa_next;
    }
}

ok,点到即止,发送信号的细节我们不深究了。

上面从用户态和驱动层两方面剖析,还深度分析了内核源码的实现,算是理清了异步通知机制的原理。

此外本文分析异步通知队列,如何将进程“添加”到异步通知队列中,对于理解等待队列原理有一定的帮助。实则是通过文件指针关联对象,将对象添加到队列中。定位进程则是反过来,根据文件指针查找,然后根据对应文件指针去找关联的进程,所以有时候会出现一个文件指针可以找到多个进程(多个进程打开了该文件)。这种情况下,应用程序仍然必须借助于poll/select来确定输入的来源。

测试:
代码都贴出来了,读者应该知道怎么测…

参考文献:
《LDD》、《Linux设备驱动开发详解》、《Linux内核设计与实现》
Linux kernel 2.6.18/3.13 SourceCode(开发内核源码树版本为3.13)

【拜csdn上的markdown编辑器所赐,写好的东西莫名其妙的后半截没了,害得我整个博文在毫无备份的情况下重新编辑了两次】

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值