Linux驱动中的poll和fasync

一、poll(异步阻塞)

这种模式的I/O操作并不是阻塞在设备的读写操作本身,而是阻塞在同一组设备文件的描述符上,当其中的某些描述符上代表的设备对读写操作已经就绪时,阻塞状态将被解除,用户程序随后可以对这些描述符代表的设备进行读写操作。

具体到Linux的字符设备驱动程序上就是需要实现file_operations中的poll函数以支持I/O模式。相对于驱动程序用户空间除了原生态的poll调用外,还有select和epoll。

但是对于驱动来说,这些应用层调用最终到驱动程序里只由poll函数来实现。

/*
第一个参数表示要打开的设备文件(文件描述符)。
第二个参数由应用程序传递进来的,一般将此参数传递给 poll_wait 函数。
*/
__poll_t (*poll) (struct file *, struct poll_table_struct *);

设备驱动中的poll会在一个或多个等待队列中调用poll_wait函数,这里需要注意poll_wait 函数不会引起阻塞,只是把当前进程添加到指定的等待列表(poll_table)中,当请求数据准备好之后,会唤醒这些睡眠的进程;最后返回监听事件,也就是POLLIN或POLLOUT等掩码。

typedef struct poll_table_struct {
    poll_queue_proc _qproc;
    __poll_t _key;
} poll_table;

/*
参数 wait_address 是要添加到 poll_table 中的等待队列头,
参数 p 就是 poll_table,就是 file_operations 中 poll 函数的第二个参数。
*/
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

1)示例一:

由此可见poll方法就是让应用程序同时等待多个数据流,在设备驱动中使用轮询的典型模板:

static unsigned int xxx_poll(struct file *filp, struct poll_table_struct *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;
}

2)示例二:

/*定义一个用于读取的等待队列xxx_inq*/
static DECLARE_WAIT_QUEUE_HEAD(xxx_inq)  
/*驱动程序实现的poll例程*/
unsigned int xxx_poll(struct file *filp, struct poll_table_struct *wait)
{
    struct xxx_buf_list *list = filp->private_data;
    unsigned int mask = 0; //初始化mask为0,表面目前关于设备的数据状态没有发生任何变化
    ...
    poll_wait(filp, &xxx_inq, wait); //将来自内核的一个等待节点加入xxx_inq队列
    if(list->head != list->tail)  //判断缓冲区是否可读
        mask |= POLLIN | POLLRDNORM;

    return mask;
}
/*设备驱动程序实现的中断处理例程*/
irqreturn_t xxx_isr(int irq, void *dev)
{
    ...
    wake_up_interrupt(&xxx_inq); //如果缓冲区可读,唤醒阻塞在poll上的进程
    ...
}

二、fasync(异步通知)

在异步阻塞型I/O中,基于驱动程序poll例程之上的应用层的三个函数poll、select、epoll,它们在与驱动程序沟通数据是否就绪时,本质是采用了轮询的方式:应用程序在一组由设备文件描述符的集合上调用poll,由此获得设备可否进行无阻塞操作的信息。

其实除了轮询的方式,应用程序与设备驱动的沟通还有一种类似中断的方式;当设备中的数据就绪时,作为通知的方式,驱动程序给应用程序发送一个signal。

异步通知使用了系统调用的signal()和sigcation()函数。signal()函数会让一个信号和一个函数对应,每当接收到这个信号时就会调用相应的函数来处理。异步通知处理过程中用户空间和设备驱动的交互如下:

Linux驱动中的poll和fasync_第1张图片

应用层的处理

1)设置信号处理函数:

我们使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数。在应用程序中使用 signal 函数来设置指定信号的处理函数:

//signum:要设置处理函数的信号。
//handler: 信号的处理函数。
//返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。
sighandler_t signal(int signum, sighandler_t handler);

2)fcntl函数需要做二件事:

将本应用程序的进程号告诉给内核使用:

/*通过fcntl函数的F_SETOWN命令将进程的ID告诉驱动程序,这样当驱动发现设备的数据就绪时才
直到要通知哪个进程*/
fcntl(fd, F_SETOWN, getpid());

使用如下两行程序开启异步通知:

/*获取当前的进程状态*/
flags = fcntl(fd, F_GETFL);
/*
通过F_SETFL命令设置FASYNC标志让驱动程序启动异步通知机制(开启当前进程异步通知功能),
经过这一步,驱动程序中的 fasync 函数就会执行。
*/                     
fcntl(fd, F_SETFL, flags | FASYNC);        

下面是内核的系统调用接口,从中可以看到会调用驱动程序提供的fasync例程:

static int setfl(int fd, struct file * filp, unsigned long arg)
{
    ...
    if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
        error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
         ...
    }
    ...
}

static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
        struct file *filp)
{
    long err = -EINVAL;
    ...
    switch (cmd) {
    ...
    case F_SETFL:
        /*会在内部直接调用驱动程序提供的fasync例程*/
        err = setfl(fd, filp, arg);
        break;
    ...
    case F_SETOWN:
        /*将要通知进程的ID相关信息记录在filp->f_owner中,驱动程序无需对此做出回应*/
        err = f_setown(filp, arg, 1);
        break;
     ...
    default:
        break;
    }
    return err;
}

内核层的处理

在设备驱动和应用程序的异步通知交互中, 仅仅在应用程序端捕获信号是不够的, 因为信号的源头在设备驱动端。 因此, 应该在合适的时机让设备驱动释放信号, 在设备驱动程序中增加信号释放的相关代码。

设备驱动中异步通知编程比较简单, 主要用到一项数据结构和两个函数。使用时,首先需要定义一个struct fasync_struct类型的指针,当用户程序调用fcntl用F_SETFL命令来设置或者清除FASYNC标志时,驱动程序应该在其fasync例程中调用内核提供的fasync_helper函数在struct fasync_struct指针所指向的链表中增加或者删除一个节点,每个节点代表一个需要通知的进程。其次,当进程所需的数据就绪或者关注的某个事件发生时,驱动程序使用内核提供的kill_fasync函数负责向维护的struct fasync_struct链表中的每个进程发送通知信号。

数据结构是fasync_struct结构体:

支持异步通知的设备结构体模板如下:

struct fasync_struct {
    rwlock_t        fa_lock;
    int            magic;
    int            fa_fd;
    struct fasync_struct    *fa_next; /* singly linked list */
    struct file        *fa_file;
    struct rcu_head        fa_rcu;
};

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

两个函数分别是:

1)需要在设备驱动中实现 file_operations 操作集中的 fasync 函数:

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

该函数一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct结构体指针。fasync_helper主要将当前要通知的进程加入一个链表或者从链表中移除,这取决于应用程序调用fcntl时是否设置了FASYNC标志。

/*
1)第二个参数on:上面提到的setfl()函数中,传递给它的是一个条件表达式(arg & FASYNC) != 0,
这意味着如果应用程序在调用fcntl时,对于F_SETFL命令使用的参数arg设置了FASYNC,那么
(arg & FASYNC) != 0结果为1,所以参数on将为1,这表明应用程序正在启用驱动程序的异步通知
机制;反之,当对fcntl函数使用F_SETFL命令时清除了FASYNC标志,将导致驱动程序的fasync例程
关闭异步通知特性。
2)所以fasync_helper的主要作用是维护一个需要通知的进程链表fapp,如果应用程序需要获得异步通知
的能力,那么需要通过fcntl的F_SETFL命令设置FASYNC标志,如果设置了该标志,驱动程序的fasync例程
在调用fasync_helper时将用fasync_add_entry将需要通知的进程加入到驱动程序维护的一个链表中,
否则使用fasync_remove_entry将其从链表中移除。
3)链表的类型为struct fasync_struct,链表中的每个节点对象代表着一个需要通知的进程,进程ID
信息存放再节点对象的fa_file->f_owner中。例如当有二个用户进程需要得到设备驱动程序的异步通知时,
内核在实现fasync_helper函数时,对于一个新加入的struct fasync_struct对象节点,会将其放到
链表的头部,这就意味着应用层后调用fcntl(fd, F_SETFL, FASYNC)的应用程序反而先得到通知。
*/
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
    if (!on)
        return fasync_remove_entry(filp, fapp);
    return fasync_add_entry(fd, filp, fapp);
}

使用模板:

/*在设备驱动的fasync函数中, 只需要简单地将该函数的3个参数以及fasync_struct结构体指针
的指针作为第4个参数传入fasync_helper函数即可。*/
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);
}
/*在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放fasync_struct*/
static int xxx_release(struct inode *inode, struct file *filp)
{
    xxx_fasync(-1, filp, 0);  //将文件从异步通知列表中删除
    ...
    return 0;
}

2)kill_fasync则在设备中的某一事件发生时负责通知链表中的所有相关的进程。

现在,驱动程序已经将需要通知的进程所在的节点加入了fasync链表。当需要的条件满足时,比如进程所请求的数据已经就绪,驱动程序需要使用kill_fasync来向fasync链表中的每个等待通知的进程发送通知信号。

/*通过while循环遍历fasync链表,定义每个进程调用send_sigio来向其发送信号SIGIO以通知进程*/
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
    while (fa) {
        struct fown_struct *fown;
        unsigned long flags;

        if (fa->magic != FASYNC_MAGIC) {
            printk(KERN_ERR "kill_fasync: bad magic number in "
                   "fasync_struct!\n");
            return;
        }
        read_lock_irqsave(&fa->fa_lock, flags);
        if (fa->fa_file) {
            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);
        }
        read_unlock_irqrestore(&fa->fa_lock, flags);
        fa = rcu_dereference(fa->fa_next);
    }
}

void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
    if (*fp) {
        rcu_read_lock();
        kill_fasync_rcu(rcu_dereference(*fp), sig, band);
        rcu_read_unlock();
    }
}

使用模板:

static ssize_t xxx_write(struct file *filp, const char__user *buf, size_t count,
                        loff_t *f_ops)
{
    struct xxx_dev *dev = filp->private_data;
    ...
    if (dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}

三、总结

异步阻塞和异步通知是字符设备中二种非常重要的设备操作方式。

poll可以让应用程序通过select等API睡眠等待在一组fd上,当前的字符设备驱动程序所在的设备节点就对应其中的一个fd。进程醒来的条件是:或者指定时间到期,或者fd_set中的某一fd就绪。字符设备在实现自己的poll例程时需要维护一个自己的等待队列,将来自内核的等待节点通过poll_wait加入到自己的等待队列上,当数据就绪时唤醒等待队列上的进程,这使得应用程序的select等函数返回。

fasync用来实现一个异步通知机制,用户程序通过fcntl函数来向设备驱动程序表面是否希望在某一事件出现时得到通知。设备驱动程序在实现fasync例程时主要依赖二个内核提供的函数:fasync_helper和kill_fasync,前者将需要通知的进程加入一个链表,后者在应用程序关注的事件发送时通过信号发送的方式来通知应用程序。

你可能感兴趣的:(Linux,linux)