阻塞与非阻塞IO
- 非阻塞 IO
非阻塞 IO:当用户进程发出 IO 请求后,不进行等待,立刻返回一个结果,如果返回错误,表示数据没有准备好,这时用户进程可以选择再次发出 IO 请求。如果内核数据已经准备好,那么数据会马上被拷贝到用户线程,然后返回用户线程处理数据。
- 阻塞 IO
当用户线程发出 IO 请求后,内核会去查看数据是否就绪,如果没就绪,线程便进入阻塞状态,让出 CPU。当数据就绪后,内核才会将数据拷贝到用户空间,最后返回用户线程,开始处理数据。
阻塞IO一般借助等待队列实现。
一、等待队列
是指linux系统中进程所组成的队列,就是需要其他事件的发生才会被唤醒的进程,也就是说这些进程本身是在等待其他某些进程为他们提供进程发生的条件。他们是属于消费者的,但是他们要消耗的东西还没有产生,这些就是处于等待状态的进程,组成了等待队列。
/* 等待队列头部(固定) */
struct _wait_queue_head{
spinlock_t lock; //自旋锁
struct list_head task_list //链表头
};
typefef struct _wait_queue_head wait_queue_head_t;
/* 队头之后的其他项,真正包含任务的成员 */
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;
DECLARE_WAIT_QUEUE_HEAD(name); //定义并初始化一个等待队列头
init_waitqueue_head(q); //初始化等待队列头,q 为队列头指针
DECLARE_WAITQUEUE(name, tsk); //创建并初始化一个等待队列项,tsk 一般设置为 current(一个全局变量)相当于当前进程
/* 添加等待队列项,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);
/* 不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真 */
wait_event(wq_head, condition);
/* 可中断的阻塞等待,让调用进程进入可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真 */
wait_event_interruptible(wq_head, condition);
wake_up(wait_queue_head_t *q); //唤醒等待队列 q 所有休眠进程
wake_up_interruptible(wait_queue_head_t *q); //唤醒等待队列 q 可中断的休眠进程
等待队列使用方法:
- 初始化等待队列头,并将条件 condition 设置为假(0)
- 在需要阻塞的地方调用 wait_event(),让进程进入休眠状态
- 想要唤醒线程时,将条件 condition 置为 1,然后调用 wake_up() 唤醒等待队列中的休眠进程
// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
/* IO操作1 */
{
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, 0);//0则睡眠,1则唤醒
......
}
/* IO操作2 */
{
//唤醒休眠中的 my_wait_queue 进程
wake_up_interruptible(&my_wait_queue);
......
}
二、非阻塞 IO 实验
在驱动的 read() 中,先判断 file->f_flags 是否带有 O_NONBLOCK,即是否为非阻塞 IO,如果是,则继续判断数据就绪条件 condition 的值,数据没有就绪就直接退出 read(),并返回 -EAGAIN。
// 读操作中根据标记位决定是否要阻塞
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 如果 open() 的 flags 参数带 O_NONBLOCK
if(file->f_flags& O_NONBLOCK)
{
// 如果数据没就绪,直接退出 read()
if(tmp_dev->condition == 0)
return -EAGAIN;
}
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
return -1;
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
return -1;
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
其实就当写操作完成后才能读取,没有写之前,如果有 O_NONBLOCK 标志都是直接返回。
没有 O_NONBLOCK 就还是一样阻塞到等待队列中。
三、IO 多路复用
IO 多路复用可以实现一个进程监控多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件操作符就绪时就会阻塞应用程序,从而释放出 CPU 资源。
Linux 提供了三种实现 IO 多路复用的模型,分别是 select、poll 和 epoll,功能上是一样的。
如果是大量的密集并发就选择 poll,大量但不密集的并发就选择 epoll。
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:为要监听的文件描述符集合,是一个数组,该结构体定义见下文;
nfds:为要监听的文件描述符数量;
timeout:为超时时间,单位为 ms,timeout 大于 0,poll 等待时间直到指定的时间,timeout 为 0,poll 立即返回,timeout 为 -1,一直等待,直到时间发生。
返回值小于等于 0 表示没有任何事件发生,大于 0 时为事件发生个数。
struct pollfd {
int fd;
short events; //事件
short revents; //返回事件
};
events:
POLLIN 普通数据可读
POLLRDNORM 普通数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述符不是一个打开的文件
驱动层 poll 函数定义:
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
wait:对应用户层 poll 函数的 timeout 参数,在驱动中调用的休眠函数为 poll_wait(),wait 便是 poll_wait() 的一个参数。
该函数的返回值就是应用层 struct pollfd 的 events 成员。
poll_wait() 定义如下,主要用于 poll 驱动函数中的休眠操作:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
wait_queue_head_t 为上篇笔记提到的等待队列头,即 poll_wait 依靠等待队列来实现阻塞线程的操作。
#include <linux/wait.h>
#include <linux/poll.h>
// 初始化等待队列头用于poll等待
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
......
// 读操作中根据标记位决定是否要阻塞
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 如果 open() 的 flags 参数带 O_NONBLOCK
if(file->f_flags& O_NONBLOCK)
{
if(tmp_dev->condition == 0) /* 如果数据没就绪,直接退出 read */
return -EAGAIN;
}
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
tmp_dev->condition = 0; // 条件标志复位
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
return -1;
return 0;
}
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
return -1;
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
// poll 的驱动层实现,作用于 read ,受限于 write,有wirte完成就通知开放 read
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
poll_wait(file, &my_wait_queue, wait); // 阻塞
if(tmp_dev->condition == 1)
{
return POLLIN; // 返回事件类型
}
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
......
.poll = chrdev_poll, //将 poll 字段指向 chrdev_poll()函数
};
应用层代码:
{
struct pollfd fds[1];
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
......
// 初始化 fbs[0]
fds[0].fd = fd;
fds[0].events = POLLIN; // 事件类型为可读事件
while(1)
{
ret = poll(fds, 1, 5000); // 每次阻塞5秒,监听文件的事件
if(ret <= 0)
printf("poll timeout.\n");
else if(fds[0].revents == POLLIN) // 如果返回事件为数据可读取
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("read successfully, ndata: %s\n", buf);
else
printf("read failed.\n");
sleep(3);
}
}
// 关闭设备文件
close(fd);
}
select
select 与 poll 机制类似,都是轮询监听,但 select 最大的文件描述符个数是有限制的,默认为 1024,而 poll 没有这个限制。
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:被监控的三类文件描述符集合中最大的文件描述符 + 1
readfds:读事件文件描述符集合
writefds:写事件文件描述符集合
exceptfds:异常文件描述符集合
timeout:超时时间,为 NULL 时, select() 进入阻塞状态,一直等待,直到有事件发生;超时时间为 0 时,不阻塞,立刻返回;超时时间大于 0,select() 便会在超时时间内监测事件是否发生,超时后则退出 select()
返回值等于 0 表示没有任何事件发生,大于 0 时为事件发生个数,小于 0 表示出错。
struct timeval{
long tv_sec; //秒
long tv_usec; //毫秒
};
void FD_CLR(int fd, fd_set *set); // 将 set 中与 fd 对应的标志位清除
int FD_ISSET(int fd, fd_set *set); // 判断 set 里与 fd 对应的标志是否为 1
void FD_SET(int fd, fd_set *set); // 把 set 里的与 fd 对应的标志位置为 1
void FD_ZERO(fd_set *set); // 把 set 里所有文件描述符标志位置 0
每次调用 select() 都需要重新给 timeout 赋值(向下计数,计数值到 0 表示超时),同时也要调用 FD_ZERO() 将 3 种文件描述符数组清空(一般用到的都是 read 描述符)
核心代码:
#include <sys/select.h>
#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM 5
int main(int argc, char** argv)
{
int fd, tmp, i;
int ret = 0;
char buf[32] = {0};
int fds[FDS_NUM] = {0};
int max_fd = 0;
fd_set readfds;
fd_set writefds;
fd_set errorfds;
struct timeval timeout;
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
......
// 初始化 fbs[0]
fds[0] = fd;
// 获取最大 fd
for(i = 0; i < FDS_NUM; i++)
{
max_fd = fds[i] > max_fd ? fds[i] : max_fd;
}
while(1)
{
// 将 fds[0] 添加到文件描述符集合
FD_SET(fds[0], &readfds);
FD_SET(fds[0], &writefds);
FD_SET(fds[0], &errorfds);
// 赋值超时时间为3S
timeout.tv_sec = 3;
timeout.tv_usec = 1000;
// 监听文件是否有事件发生
ret = select(max_fd + 1, &readfds, &writefds, &errorfds, &timeout);
switch(ret)
{
case (ret <= 0):
printf("select timeout.\n");
case (FD_ISSET(fds[0], &errorfds): // 如果返回事件为异常事件
printf("excepted event.\n");
case (FD_ISSET(fds[0], &writefds): // 如果返回事件为可写
printf("writable event.\n");
case (FD_ISSET(fds[0], &readfds): // 如果返回事件为可读
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
}
// 清空集合里所有文件描述符
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&errorfds);
}
// 关闭设备文件
close(fd);
return 0;
}
epoll
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events:
EPOLLIN //文件描述符可读
EPOLLOUT //文件描述符可写
EPOLLPRI //文件描述符有紧急数据可读
EPOLLERR //文件描述符产生错误
EPOLLHUP //文件描述符被挂断
EPOLLET //文件描述符有事务产生
int epoll_create(int size);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
size:epoll 文件描述符可支持的文件描述符个数
epfd:epoll_create 生成的 epoll 文件描述符
epoll_event:用来回传成功检测的事件数组
maxevents:每次监听的最大事件个数
timeout:timeout 为 -1,表示一直阻塞监听;timeout 为 0,表示非阻塞监听;timeout 为正数时,代表监听超时时间(单位为毫秒)
fd:要操作的文件描述符
event:epoll_event 指针
op:要进行的操作,可取值见下文
EPOLL_CTL_ADD 1 注册操作
EPOLL_CTL_DEL 2 删除操作
EPOLL_CTL_MOD 3 修改操作
核心代码:
#include <sys/epoll.h>
#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM 5
#define MAX_EVENTS 10
int main(int argc, char** argv)
{
int fd, tmp, i;
int ret = 0;
char buf[32] = {0};
int epollfd;
struct epoll_event event;
struct epoll_event events[MAX_EVENTS];
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
......
// 创建一个 epollfd
epollfd = epoll_create(FDS_NUM);
if(epollfd == -1)
return 0;
// event 结构体初始化
event.events = EPOLLIN; // 读事件
event.data.fd = fd; // 要监听的文件描述符
// 设置 epollfd
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
return 0;
while(1)
{
// 等待 events
ret = epoll_wait(epollfd, events, MAX_EVENTS, 3000);
if(ret < 0)
{
printf("epoll_wait error.\n");
return 0;
}
else if(ret == 0)
printf("epoll wait timeout.\n");
else
{
// 根据监测到的事件作相应处理
for(i = 0; i < ret; i++)
{
if(events[i].data.fd == fd)
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
sleep(3);
}
}
}
}
// 关闭设备文件
close(fd);
return 0;
}