[What]Linux I/O 阻塞与非阻塞

参考链接: I/O模式详解

参考宋宝华老师的书 Linux设备驱动开发详解 ,来理解I/O的阻塞与非阻塞概念。

阻塞与非阻塞

  • 阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程进入睡眠状态直到满足可操作的条件后再进行操作。
  • 非阻塞操作的进程在不能进行设备操作时,并不挂起,要么放弃,要么不停地查询,直至可以进行操作为止。
  • 驱动程序应该根据用户是否提供 O_NONBLOCK(非阻塞) 标志来选择是否要阻塞用户空间的进程。
    • xxx_read(),xxx_write() 中可以根据资源是否可用和用户标志来选择是否阻塞用户进程

阻塞的实现

队列属于linux内核中的基础数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问,信号量也是依赖等待队列实现的。

当一个进程或线程仅有简单一个的读写操作时,使用简单的阻塞可以满足要求。
但当需要处理多个I/O操作时在同一个线程时,使用阻塞就不合适,因为其中一个阻塞那么其他操作便捕捉不到了。

操作

  • 定义及初始化“等待队列头部”
/// 定义头部
wait_queue_head_t my_queue;
///初始化头部
init_waitqueue_head(&my_queue);

///定义并初始化头部
DECLARE_WAIT_QUEUE_HEAD(name)
  • 队列元素操作
/// 定义并初始化一个名为 name 的等待队列元素
DECLARE_WAITQUEUE(name, tsk)

/// 将队列元素添加 / 移除等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
  • 等待及唤醒
/// 等待事件
wait_event(queue, condition);
wait_event_interruptible(queue, condition);//可以被信号打断
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);//可以被信号打断

/// 唤醒队列
/**
* @note wake_up() 应该与 wait_event() 或 wait_event_timeout() 成对使用
* wake_up_interruptible() 应该与 wait_event_interruptible() 或 wait_event_interruptible_timeout() 成 * 对使用
* wake_up() 可以唤醒 处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的进程,但 wake_up_interruptible() 只能唤醒处于 TASK_INTERRUPTIBLE 的进程
*/

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);//与wait_event_interrupt 成对使用


/// 在等待队列上睡眠

/**
* @brief 将目前进程的状态设置为 TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,
* 之后把它挂到等待队列头部 q 指向的双向链表,直到资源可以获得, q队列指向链接的进程被唤醒
* @note 与 wake_up 成对使用
*/

sleep_on(wait_queue_head_t *q);

/**
* @brief 将目前进程的状态设置为 TASK_INTERRUPTIBLE,并定义一个等待队列元素,
* 之后把它挂到等待队列头部 q 指向的双向链表,直到资源可以获得, q队列指向链接的进程被唤醒,或者进程收到信号
* @note 与 wake_up_interruptible 成对使用
*/

interruptible_sleep_on(wait_queue_head_t *q);

实例

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, lofft_t *ppos)
{

/// 定义等待队列元素
DECLARE_WAITQUEUE(wait, current);
/// 添加元素到等待队列
add_wait_queue(&xxx_wait, &wait);

do{
avail = device_writeable(...);
if(avail < 0){
///非阻塞
if(file->f_flags & O_NONBLOCK){
ret = -EAGAIN;
goto out;
}
///改变进程状态
__set_current_state(TASK_INTERRUPTIBLE);
///调度其他进程执行
schedule();
///如果是因为信号唤醒
if(signal_pending(current)){
//向应用空间将接收到 EINTR 错误返回(errno)
ret = -ERESTARTSYS;
goto out;
}
}
}while(avail < 0);
device_write(...);
out:
remove_wait_queue(&xxx_wait, &wait);
set_current_state(TASK_RUNNING);
return ret;
}

轮询

当应用程序需要 同时监控多个文件 是否可读可写时,通常会使用select()和poll()系统调用。

这两个调用最终会使设备驱动中的poll()函数被执行。

应用程序中的编程

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

/**
* @brief 监控文件是否可进行相应操作
* @param nfds :需要检查的号码最高的fd 加 1
* @param readfds: 监视读文件描述符集
* @param writefds: 监视写文件描述符集
* @param exceptfds: 监视异常处理文件描述符集
* @param timeout : 超时返回
* @note 当监视的文件集中任何一个文件可以操作,此函数便返回
*
* select() 第一次操作时,如果文件集中有文件可以满足要求,则立即返回。
* 第二次select时若没有文件满足则阻塞且睡眠。
*
* 调用select()时每个驱动的poll()接口都会被调用,实际上执行select()的进程被挂到了每个驱动的等待队列上
* 可以被任何一个驱动唤醒。
*/

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
;


//清除一个文件描述符集合
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);


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

当多路复用的文件数量庞大、I/O流量频繁的时候,应该使用 epoll,因为 select() 和 poll 会随着fd数量增大而性能下降明显。

epoll 多用于网络服务器监听许多 socket。

/**
* @brief 创建一个epoll句柄,告诉内核要监听多少个fd
* @note 函数返回的是fd,会占用文件描述符,所以在使用完成后需要使用 close() 关闭
*/

int epoll_create(int size);

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
/**
* @brief 对指定描述符设置对应的监听事件
* @param epfd: epoll_create() 返回值
* @param op: 对描述符的操作
* EPOLL_CTL_ADD : 增加fd到监听事件中
* EPOLL_CTL_MOD : 修改已经注册的fd的监听事件
* EPOLL_CTL_DEL : 从epfd中删除一个fd
* @param *event: 监听事件类型
* EPOLLIN: 读
* EPOLLOUT: 写
* EPOLLPRI: 有外来数据到来时时读
* EPOLLERR: 发生错误
* EPOLLHUP: 被挂断
* EPOLLET: 边缘触发(Edge Triggered),内核仅通知一次
* EPOLLONESHOT: 只监听依次,当监听事件发生后,需要再次把这个fd加入队列
*/

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/**
* @brief 等待事件发生
* @param events: 事件集合
* @param maxevents: 本次最多接收多少事件,不能大于epoll_create() 的 size
* @param timeout: 超时退出
* @ret : 需要处理事件的数目
*/

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

设备驱动中的编程

/**
* @param wait: 轮询表指针
* @note : 1. 对可能引起设备文件状态变化的等待队列调用 poll_wait() 函数,将对应的等待队列头部添加到 poll_table 中
* 2. 返回表示是否能对设备进行无阻塞读、写访问的掩码
* @ret :返回设备资源的可获取状态:POLLIN,POLLOUT,POLLPRI,POLLERR...
*/

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

/**
* @brief 把当前进程添加到wait 参数指定的等待列表中(poll_table)
* @note 实际作用是让唤醒参数queue对应的等待队列可以唤醒因select() 而睡眠的进程
*/

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


/**
* @brief 模板
*/

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 | POLLRFNORM; //返回数据可读标记

if(...)//如果当前资源可写
make |= POLLOUT | POLLWRNORM; //返回数据可写标记

return mask;
}
Last Updated 2018-10-14 日 19:32.
Render by hexo-renderer-org with Emacs 26.1 (Org mode 9.1.14)