Linux设备驱动中的阻塞与非阻塞I/O

1 阻塞与非阻塞I/O

        阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。         非阻塞操作的进程在不能进行设备操作时, 并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。

        在阻塞访问时, 不能获取资源的进程将进入休眠, 它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态, 所以必须确保有一个地方能够唤醒休眠的进程, 否则, 进程就真的“寿终正寝”了。 唤醒进程的地方最大可能发生在中断里面, 因为在硬件资源获得的同时往往伴随着一个中断。 而非阻塞的进程则不断尝试, 直到可以进行I/O

Linux设备驱动中的阻塞与非阻塞I/O_第1张图片

 如何设置文件为阻塞/非阻塞:

// open
open("file_name", O_RDWR | O_NONBLOCK);

//或 fcntl
fcntl(fd,F_SETFL, O_NONBLOCK)

1.1 等待队列

        阻塞访问最大的好处是当设备不可操作时进程可以挂起进入睡眠,让出CPU。

        在Linux驱动程序中, 可以使用等待队列(Wait Queue) 来实现阻塞进程的唤醒。等待队列作为Linux内核中的一个基本单位,以队列为数据结构,与调度机制紧密结合,可以用来同步对系统资源的访问。

相关操作

定义在:include/linux/wait.h

1.1.1 定义队列头

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

//定义等待队列头部
wait_queue_head_t my_queue;

1.1.2 初始化等待队列头 

//初始化等待队列头部
void init_waitqueue_head(&my_queue);

或使用宏DECLARE_WAIT_QUEUE_HEAD定义+初始化:

//或使用宏 定义且初始化 等待队列头
#define DECLARE_WAIT_QUEUE_HEAD(my_queue) \
	wait_queue_head_t my_queue= __WAIT_QUEUE_HEAD_INITIALIZER(my_queue)

1.1.3 定义等待队列项(等待队列元素)

struct __wait_queue {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	task_list;
};

typedef struct __wait_queue wait_queue_t;
//定义并初始化一个名为name的等待队列元素
/* 参数name:等待队列项的名字
 * 参数tsk:表示等待队列项属于哪个任务,一般设置为current(表示当前进程)
*/
#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

1.1.4 添加、删除等待队列元素

//添加/删除等待队列中的元素
/* 参数q:等待队列项要加入的等待队列头
 * 参数wait:要加入的等待队列项
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

/* 参数q:要删除的等待队列项所在的等待队列头
 * 参数wait:要删除的等待队列项
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

1.1.5 等待事件

        除了主动唤醒,也可以设置等待队列等待某个事件,当事件满足就自动唤醒等待队列中的进程。

函数 说明
wait_event(wq, condition) 等待以wq为等待队列头的等待队列被唤醒,前提是condition条件满足(为真),否则一直阻塞。函数会将进程设置为TASK_UNINTERRUPTIBLE。
wait_event_timeout(wq, condition, timeout) 和wait_event类似,但可以添加超时时间,以jiffies为单位。返回值为0表示超时时间到而且condition为假;如果返回值为1表示condition为真,条件满足。
wait_event_interruptible(wq, condition) 和wait_event类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。
wait_event_interruptible_timeout(wq, condition, timeout) 和wait_event_timeout类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。

1.1.6 唤醒队列

        唤醒以queue作为等待队列头部的队列中所有的进程。

void wake_up(wait_queue_head_t *queue);

void wake_up_interruptible(wait_queue_head_t *queue);
能唤醒的进程类型 配对使用
wake_up()

TASK_INTERRUPTIBLE

TASK_UNINTERRUPTIBLE

wait_event()

wait_event_timeout()

wake_up_interruptible() TASK_INTERRUPTIBLE

wait_event_interruptible()

wait_event_interruptible_timeout()


 

1.1.7 在等待队列上睡眠

sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);

        sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。
        interruptible_sleep_on()与sleep_on()函数类似,其作用是将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得(q指引的等待队列被唤醒)或者进程收到信号。sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。

1.1.8 使用模板

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
    ...
    DECLARE_WAITQUEUE(wait, current);   /* 定义等待队列元素 */
    add_wait_queue(&xxx_wait, &wait);   /* 添加元素到等待队列 */

    /* 等待设备缓冲区可写 */
    do {
        avail = device_writable(...);
        if (avail < 0) {
            if (file->f_flags & O_NONBLOCK) {  /* 非阻塞 */
                ret = -EAGAIN;
                goto out;
            }
            __set_current_state(TASK_INTERRUPTIBLE);  /* 改变进程状态 */
            schedule();                               /* 调度其他进程执行 */
            if (signal_pending(current)) {            /* 如果是因为信号唤醒 */
                ret = -ERESTARTSYS;
                goto out;
            }
        }
    } while (avail < 0);

    /* 写设备缓冲区 */
    device_write(...);
out:
    remove_wait_queue(&xxx_wait, &wait);  /* 将元素移出xxx_wait指引的队列 */
    set_current_state(TASK_RUNNING);      /* 设置进程状态为TASK_RUNNING */
    return ret;
}

1)如果是非阻塞访问(O_NONBLOCK被设备),设备忙时,直接返回-EAGAIN。

2)如果是阻塞访问,调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并通过schedule()调度其它进程执行。

3)醒来的时候,由于调度出去的时候进程状态是TASK_INTERRUPTIBLE(浅度睡眠),所以唤醒的可能是信号,所以先通过signal_pending判断是否为信号唤醒,如果是,立即返回-ERESTARTSYS。

DECLARE_WAITQUEUE和add_wait_queue这两个动作的效果如下图所示:

Linux设备驱动中的阻塞与非阻塞I/O_第2张图片

 

        在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,这个新元素绑定了一个task_struct数据结构(当前做xxx_write的current,也是DECLARE_WAITQUEUE使用"current"作为参数的原因)。

2  轮询操作

2.1 轮询的概念与作用

        在用户程序中, select() 和 poll() 也是与设备阻塞与非阻塞访问息息相关的论题。 使用非阻塞I/O的应用程序通常会使用select() 和poll() 系统调用查询是否可对设备进行无阻塞的访问。 select() 和 poll() 系统调用最终会使设备驱动中的poll() 函数被执行, 在Linux2.5.45内核中还引入了epoll() , 即扩展的poll() 。

        select() 和poll() 系统调用的本质一样, 前者在BSD UNIX中引入, 后者在System V中引入。

2.2 应用程序中的轮询编程

2.2.1 select()

/* 参数numfds:等待队列项要加入的等待队列头
 * 参数readfds:要监视的读事件
 * 参数writefds:要监视的写事件
 * 参数exceptfds:要监视的异常事件
 * 参数timeout:超时时间
 */
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
        struct timeval *timeout);

       readfds文件集中的任何一个文件变得可读, select() 返回;同理, writefds文件集中的任何一个文件变得可写, select也返回。

        如图8.3所示, 第一次对n个文件进行select() 的时候, 若任何一个文件满足要求, select() 就直接返回; 第2次再进行select() 的时候, 没有文件满足读写要求, select() 的进程阻塞且睡眠。 由于调用select() 的时候, 每个驱动的poll() 接口都会被调用到, 实际上执行select() 的进程被挂到了每个驱动的等待队列上, 可以被任何一个驱动唤醒。 如果FDn变得可读写, select() 返回。

Linux设备驱动中的阻塞与非阻塞I/O_第3张图片

        timeout参数是一个指向struct timeval类型的指针, 它可以使select() 在等待timeout时间后若仍然没有文件描述符准备好则超时返回。 struct timeval数据结构的定义如代码清单8.7所示。

struct timeval {
    int tv_sec; /* 秒 */
    int tv_usec; /* 微秒 */
};

设置、 清除、 判断文件描述符集合:

//清除一个文件描述符集合;
FD_ZERO(fd_set *set)

//将一个文件描述符加入文件描述符集合中;
FD_SET(int fd,fd_set *set)

//将一个文件描述符从文件描述符集合中清除;
FD_CLR(int fd,fd_set *set)

//判断文件描述符是否被置位。
FD_ISSET(int fd,fd_set *set)

2.2.2 poll()

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

2.2.3 epoll()

        当多路复用的文件数量庞大、 I/O流量频繁的时候, 一般不太适合使用select() 和poll() , 此种情况下, select() 和poll() 的性能表现较差, 我们宜使用epoll。 epoll的最大好处是不会随着fd的数目增长而降低效率, select() 则会随着fd的数量增大性能下降明显。

/* 参数size:要监听多少个fd */
int epoll_create(int size);

/* 参数epfd:要监听的事件类型epoll_create返回值。
 * 参数op:动作。
    EPOLL_CTL_ADD: 注册新的fd到epfd中。
    EPOLL_CTL_MOD: 修改已经注册的fd的监听事件。
    EPOLL_CTL_DEL: 从epfd中删除一个fd。
 * 参数fd:需要监听的fd。
 * 参数event:告诉内核需要监听的事件类型
    EPOLLIN: 表示对应的文件描述符可以读。
    EPOLLOUT: 表示对应的文件描述符可以写。
    EPOLLPRI: 表示对应的文件描述符有紧急的数据可读( 这里应该表示的是有socket带外数据到
来) 。
    EPOLLERR: 表示对应的文件描述符发生错误。
    EPOLLHUP: 表示对应的文件描述符被挂断。
    EPOLLET: 将epoll设为边缘触发( Edge Triggered) 模式, 这是相对于水平触发( Level Triggered) 来说的。 LT( Level Triggered) 是缺省的工作方式, 在LT情况下, 内核告诉用户一个fd是否就绪了, 之后用户可以对这个就绪的fd进行I/O操作。 但是如果用户不进行任何操作, 该事件并不会丢失, 而ET( EdgeTriggered) 是高速工作方式, 在这种模式下, 当fd从未就绪变为就绪时, 内核通过epoll告诉用户, 然后它会假设用户知道fd已经就绪, 并且不会再为那个fd发送更多的就绪通知。
    EPOLLONESHOT: 意味着一次性监听, 当监听完这次事件之后, 如果还需要继续监听这个fd的话,
需要再次把这个fd加入到epoll队列里。
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);


/* 参数epfd:要监听的事件类型epoll_create返回值。
 * 输出参数events:用来从内核得到事件的集合。
 * 参数maxevents:告诉内核本次最多收多少事件。
 * 参数timeout:超时时间( 以毫秒为单位, 0意味着立即返回, -1意味着永久等待) 。 该函数的返回值是需要处理的事件数目, 如返回0, 则表示已超时。
 */
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

2.3 设备驱动中的轮询编程

2.3.1 poll驱动操作

设备驱动中poll() 函数的原型是:

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

        第1个参数为file结构体指针, 第2个参数为轮询表指针。 这个函数应该进行两项工作。

        1) 对可能引起设备文件状态变化的等待队列调用poll_wait() 函数, 将对应的等待队列头部添加到poll_table中。
        2) 返回表示是否能对设备进行无阻塞读、 写访问的掩码。

        用于向poll_table注册等待队列的关键poll_wait() 函数的原型如下:

void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

        poll_wait() 函数的名称非常容易让人产生误会, 以为它和wait_event() 等一样, 会阻塞地等待某事件的发生, 其实这个函数并不会引起阻塞。

        poll_wait() 函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table) 中, 实际作用是让唤醒参数queue对应的等待队列可以唤醒因select() 而睡眠的进程。

        驱动程序poll() 函数应该返回设备资源的可获取状态, 即POLLINPOLLOUTPOLLPRIPOLLERRPOLLNVAL等宏的位“或”结果。 每个宏的含义都表明设备的一种状态, 如POLLIN(定义为0x0001) 意味着设备可以无阻塞地读, POLLOUT(定义为0x0004) 意味着设备可以无阻塞地写。

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

你可能感兴趣的:(linux,运维,服务器)