Linux 驱动框架---驱动中的阻塞

描述和API       

    阻塞IO和非阻塞IO的应用编程时的处理机制是不同的,如果是非阻塞IO在访问资源未就绪时就直接返回-EAGAIN,反之阻塞IO则会使当前用户进程睡眠直到资源可用。从应用场景来说两种方式分别适应不同的使用场景。而驱动开发不可避免的需要支持两种访问方式。如果不是采用现成的子框架而自己实现文件操作底层接口部分时就需要自己实现这一机制。文件的访问方式除了在打开文件时指定外还可以在打开以后通过fcnt和ioctl进行修改和获取。

     阻塞IO 在实现过程依赖两个重要的数据结构等待队列头(wait_queue_head_t)和等待队列成员(wait_queue_t)具体的定义如下:

struct __wait_queue_head {
    spinlock_t        lock;
    struct list_head    task_list;
};

typedef struct __wait_queue wait_queue_t;

struct __wait_queue {
    unsigned int        flags;
#define WQ_FLAG_EXCLUSIVE    0x01
    void            *private;
    wait_queue_func_t    func;
    struct list_head    task_list;
};

实际上都是Linux内核关于链表管理的通用方式再加上互斥资源的封装操作。使用也特别简单直接看API和具体的使用方法。

定义

使用等待队列前需要先定义一个等待队列头,可以使用动态和静态的方式进行定义。等待队列成员相同也可以使用两种方式创建,不过内核都提供了方便的宏用来完成对等待成员的定义和初始化。

初始化

等待队列头的初始化使用init_waitqueue_head(wait_queue_head_t* head)进行初始化。而等待队列成员常使用__WAITQUEUE_INITIALIZER如下宏进行定义和初始化。

//初始化队列头
#define init_waitqueue_head(q)                
    do {                        
        static struct lock_class_key __key;    
                            
        __init_waitqueue_head((q), #q, &__key);    
    } while (0)


//初始化队列成员
#define __WAITQUEUE_INITIALIZER(name, tsk) {                
    .private    = tsk,                        
    .func        = default_wake_function,            
    .task_list    = { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk)                    
    wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

添加和移除等待队列

//将wait 加入到 head 队列中
void add_wait_queue(wait_queue_head_t *head,wait_queue_t *wait )
//将wait 从 head 队列中移除
void remove_wait_queue(wait_queue_head_t *head,wait_queue_t *wait )

在队列上等待事件

wait_event(wq, condition)  注意传入的参数wq为队列实体而不是地址,condition 为“并”逻辑条件。

#define wait_event(wq, condition)                    
do {                                    
    if (condition)                            
        break;                            
    __wait_event(wq, condition);                    
} while (0)

#define __wait_event(wq, condition)                 
    (void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0,  
                schedule())

#define ___wait_event(wq, condition, state, exclusive, ret, cmd)    
({                                    
    __label__ __out;                        
    wait_queue_t __wait;                        
    long __ret = ret;    /* explicit shadow */            
                                    
    INIT_LIST_HEAD(&__wait.task_list);                
    if (exclusive)                            
        __wait.flags = WQ_FLAG_EXCLUSIVE;            
    else                                
        __wait.flags = 0;                    
                                    
    for (;;) {                            
        long __int = prepare_to_wait_event(&wq, &__wait, state);
                                    
        if (condition)                        
            break;                        
                                    
        if (___wait_is_interruptible(state) && __int) {        
            __ret = __int;                    
            if (exclusive) {                
                abort_exclusive_wait(&wq, &__wait,    
                             state, NULL);    
                goto __out;                
            }                        
            break;                        
        }                            
                                    
        cmd;                            
    }                                
    finish_wait(&wq, &__wait);                    
__out:    __ret;                                
})

可以中断的接口 wait_event_interruptible(wq, condition) 

#define wait_event_interruptible(wq, condition)                
({                                    
    int __ret = 0;                            
    if (!(condition))                        
        __ret = __wait_event_interruptible(wq, condition);    
    __ret;                                
})

#define __wait_event_interruptible(wq, condition)            
    ___wait_event(wq, condition, TASK_INTERRUPTIBLE, 0, 0,        
              schedule())

支持超时的接口 wait_event_timeout(wq, condition, timeout)  

#define wait_event_timeout(wq, condition, timeout)            
({                                    
    long __ret = timeout;                        
    if (!___wait_cond_timeout(condition))                
        __ret = __wait_event_timeout(wq, condition, timeout);    
    __ret;                                
})

#define ___wait_cond_timeout(condition)                    
({                                    
    bool __cond = (condition);                    
    if (__cond && !__ret)                        
        __ret = 1;                        
    __cond || !__ret;                        
})

#define __wait_event_timeout(wq, condition, timeout)            
    ___wait_event(wq, ___wait_cond_timeout(condition),        
              TASK_UNINTERRUPTIBLE, 0, timeout,            
              __ret = schedule_timeout(__ret))

支持中断和超时wait_event_interruptible_timeout(wq, condition, timeout)

#define wait_event_interruptible_timeout(wq, condition, timeout)    
({                                    
    long __ret = timeout;                        
    if (!___wait_cond_timeout(condition))                
        __ret = __wait_event_interruptible_timeout(wq,        
                        condition, timeout);    
    __ret;                                
})


#define __wait_event_interruptible_timeout(wq, condition, timeout)    
    ___wait_event(wq, ___wait_cond_timeout(condition),        
              TASK_INTERRUPTIBLE, 0, timeout,            
              __ret = schedule_timeout(__ret))

通过观察以上接口最终都是通过调用___wait_event(wq, condition, state, exclusive, ret, cmd) 接口来实现的等待事件。

唤醒队列

void wake_up(wait_queue_head_t* queue);
void wake_up_interruptible(wait_queue_head_t* queue);

需要注意的是两个接口都用来唤醒一个队列上的所有等待进程,而wake_up可以唤醒TASK_INTERRUPTIBLR和TASK_UNINTERRUPTIBLE 两种状态的进程。而wake_up_interruptible仅能用来唤醒TASK_INTERRUPTIBLR状态的进程即调用wait_event_interruptible和wait_event_interruptible_timeout()接口进入等待的进程。除此之外还有两个接口用于快速在一个访问接口中睡眠;

sleep_on(wait_queue_head_t* queue);
interruptible_sleep_on(wait_queue_head_t* queue);

两个接口都是会定义一个等待成员并将其添加到等待队列上设置进程为对应状态后睡眠直到资源可用。

使用示例

在驱动私有数据中先定义一个等待队列头并在模块安装时初始化。

int xxx_init(void)
{
    ...
    init_waitqueue_head(&xxx_wait_head);
    ...
}

阻塞

static ssize_t xxx_write(struct file* file ,const char* buf,size_t cnt,lofft_t *ppos)
{
    //从私有数据中或全局的方式拿到等待队列头xxx_queue
    
    //访问接口中
    DECLARE_WAITQUEUE(xx_wait,currrent);//currrent 为当前进程PCB
    add_wait_queue(&xxx_queue,&xx_wait);
    ...
    if(file->flags & O_NONBLOCK)
    {
        return -EAGAIN;
    }else
    {
        __set_current_state(TASK_INTERRUPTIABLE);
        schedule();
        if(signal_pending(currrent)){
            //因为此处是可信号中断睡眠的所以,有可能因为信号唤醒
            //所以需要判断是否是因为信号唤醒,如是则返回请重新调用系统调用
            return -ERESTARTSYS;
        }
    }
    ...
}

唤醒

static ssize_t xxx_read(struct file* file ,const char* buf,size_t cnt,lofft_t *ppos)
{
    //从私有数据中或全局的方式拿到等待队列头xxx_queue
    //访问接口中
    ....
    //或wake_up(&xxx_queue) 与前面阻塞相对应
    wake_up_interruptible(&xxx_queue)
    ....
    
}

这里示例未使用wait_event相关的接口,但实际上这些接口的实现类似上面使用过程的。如 __wait_event_interruptible的旧版内核实现,这里看旧版是因为旧版的宏接口封装更加容易理解。

#define __wait_event_interruptible(wq, condition, ret)            
do {                                    
    DEFINE_WAIT(__wait);                        
                                    
    for (;;) {                            
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    
        if (condition)                        
            break;                        
        if (!signal_pending(current)) {                
            schedule();                    
            continue;                    
        }                            
        ret = -ERESTARTSYS;                    
        break;                            
    }                                
    finish_wait(&wq, &__wait);                    
} while (0)

驱动的轮询支持

      应用编程过程中常常也会使用非阻塞的IO操作,因为可能应用的逻辑是判断是否有数据可用如果有则执行A处理,没有则执行B处理。如果在IO操作时阻塞则就无法执行B处理过程,最重要的是如果需要同时操作多个IO时如果非阻塞的方式打开则会降低程序的性能,因为频繁的调用系统接口然后返回 -EAGAIN 且需要同时处理多个IO的情况下就需要编写很多的读取判断函数接口,过多的系统调用会降低程序的执行性能所以需要IO轮询接口从而实现IO多路复用系统提供了select()和poll()调用,在内核内部最后都会调用驱动提供的poll()接口。poll机制的实现基本思路是

1、设备驱动定义一个等待队列

2、poll接口调用将调用进程挂接到这个队列上(poll_wait(file,&xxx_queue,wait))

3、由中断或其他机制唤醒这个队列

4、返回对应事件类型的掩码(POLLRDNORM 、POLLIN数据可读、POLLRDNORM 、POLLOUT数据可写)

示例:

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

static unsigned drivers_poll(struct file *file, poll_table *wait)
{
    unsigned int mask = 0;
    poll_wait(file, &button_waitq, wait); /* 将进程挂接到button_waitq等待队列下 */

    /* 根据实际情况,标记事件类型 */
    if (ev_press)
        mask |= POLLIN | POLLRDNORM;

    /* 如果mask为0,那么证明没有请求事件发生;如果非零说明有时间发生 */
    return mask;
}
poll_wait接口详情:
/*
 * Do not touch the structure directly, use the access functions
 * poll_does_not_wait() and poll_requested_events() instead.
 */
typedef struct poll_table_struct {
    poll_queue_proc _qproc;
    unsigned long _key;
} poll_table;

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address)
        p->_qproc(filp, wait_address, p);
}

它是将当前进程添加到由系统提供poll_table中,实际的作用就是在驱动唤醒wait_address 指定的等待队列头时可以同时唤醒因为调用轮叙接口select或这poll接口而进入睡眠的进程。至此就是驱动中阻塞相关使用的简单学习归纳了,后续在实际使用中提炼总结。

原文地址:https://www.cnblogs.com/w-smile/p/13418235.html