一种理解同步/异步,阻塞/非阻塞,Linux IO 模型,select /poll /epoll 的方法

前言

强迫症不能忍受这种极其绕的概念而不给个说法。同步(synchronous)/异步(asynchronous),阻塞(blocking)/非阻塞(non-blocking),阻塞IO/非阻塞IO/同步IO/异步IO/IO复用(IO Multiplexing),select/poll/epoll这些概念困扰我许久,下面给出这一阶段我个人的理解。

同步/异步与阻塞/非阻塞的理解

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。用线程执行程序流的过程去理解同步异步,阻塞非阻塞。同步异步关注的是流执行过程需不需要等待外部调用的结果,而阻塞非阻塞关注的是外部调用对流本身产生的影响。

同步与异步

线程的执行过程中,产生一个外部调用,如果需要等待该调用返回才能继续线程流则叫做同步,不需要等待结果返回线程流可以继续往下执行的情况叫做异步。 区分:线程流向下执行需不需要等待系统调用的结果。

阻塞与非阻塞

线程执行过程中,产生一个外部调用后,会不会把该线程流“堵”住,会“堵”对应的是阻塞,反之为非阻塞。

Linux的五种IO模型

上一节中对同步/异步,阻塞/非阻塞的描述只能说能够恰好区分它们,如果不是在计算机领域而是生活中,道理也类似。然而计算机中的某些专业术语又需要放在专门的情景中去看,例如下面将要提到的Linux IO模型,建议理解模型本身,而不是抠同步/异步与阻塞非阻塞的字眼,因为会发现就算是非阻塞模型也有阻塞的部分,同步IO与异步IO的区别是IO操作的时候会不会让process阻塞。 下面对模型描述的最终来源为: Richard Stevens的“UNIX:registered: Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ” A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes; An asynchronous I/O operation does not cause the requesting process to be blocked;

同步IO模型

用IO操作中有阻塞来判断,5种IO模型中4种属于同步IO,分别是阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动IO模型。

blocking IO

阻塞IO是socket的默认设置,其模型如下图所示,程序调用recvfrom产生一个系统调用,kernel收到该调用请求后有两个步骤,第一是等待数据准备好,第二是将数据从内核空间拷贝到用户空间然后返回OK,用户空间收到系统调用返回后才会继续程序流的执行。

non-blocking IO

socket使用非阻塞IO模型需要对socket进行另行设置,非阻塞IO模型如下所示。内核收到系统调用后,若数据未准备好立即返回error,用户进程收到error会继续产生系统调用,直到数据准备好了并被拷贝到用户空间。

IO multiplexing

select/poll/epoll对应的是IO复用模型,优势是能够监听多个socket,模型图如下所示。用户进程调用select产生系统调用,kernel会监听所有select负责的socket,一旦有一个socket数据准备好了,kernel即返回,用户再去recvfrom产生系统调用将数据从内核空间读到用户空间。

signal-driven IO

用户程序注册一个信号handler,然后继续做后续的事情,当内核数据准备好了会发送一个信号,程序调用recvfrom进行系统调用将数据从内核空间拷贝到用户空间。

异步I/O模型

用IO操作中无阻塞来判断,5种IO模型中只有异步IO。

asynchronous IO

异步IO模型如下,aio_read产生系统调用,kernel在数据准备好后将数据从内核空间拷贝到用户空间后返回一个信号告知read数据成功,整个过程程序调用aio_read后就继续执行其他部分直到收到信号,调用handler处理。

模型的对比

Kernel有两个过程,等待数据准备好和拷贝数据到用户空间,用户程序的阻塞一般有两种情况,select的阻塞和socket IO的阻塞,5中IO模型的对比如下。

select/poll/epoll

Select/poll/epoll能够同时监听多个文件描述符fd,当有fd的读写操作完成时会返回这些fd,可以对应于IO复用模型中的系统调用查询fd是否准备好数据的那一部分。

select

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

typedef struct {
    unsigned long *in, *out, *ex;
    unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;

select在用户层使用某个结构标识被监听的fd以及监听的状态,每一个fd用1bit表示,为1表示这个文件是被监听的,0表示不监听。in, out, ex指向的bit数组表示对应的读,写,异常文件的描述符。res_in, res_out,res_ex指向的bit数组表示对应的读,写,异常文件的描述符的检测结果。

  1. 这个结构被拷贝到内核层,
  2. 对所有的fd注册回调函数__pollwait
  3. 调用fd的poll方法遍历整个FD_SESIZET所有的fd,检查是不是自己需要监听的,如果监听的fd发生了感兴趣的事(文件读写操作完成或者异常,参考用户态预先的设置),则poll方法返回一个描述读写操作是否就绪的mask掩码,根据mask掩码给fd_set赋值。poll->poll_wait->__pollwait会把当前进程挂到对应文件的inode中的fd的等待队列。
  4. 如果一轮遍历无果则挂起,直到超时或者有设备驱动发生自身资源可读写后将其从等待队列唤醒,则执行新一轮的遍历。
  5. 把fd_set从内核空间拷贝到用户空间并将进程从各个等待队列中删除。

poll

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

struct pollfd {
    int fd;
    short events;
    short revents;
};

poll的实现机制与select类似,不一样的是poll的使用中用户态直接提供了需要监听的fd的信息,pollfd结构记录被监听的fd和它的状态。

struct poll_list {
struct poll_list *next;
    int len;
    struct pollfd entries[0];
};

另外poll使用poll_list结构来记录监听的fd,每一个poll_list节点都包含一个pollfd数组,参数被拷贝到内核后,poll_list被遍历,换言之pollfd数组被遍历,与select一样,所有的fd都被遍历。

epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll将过程细化为一组系统调用,包括1个epoll_create,多个epoll_ctrl,1个epoll_wait。内核针对epoll操作添加了一个文件系统”eventpollfs”,每一个或者多个要监视的fd都有一个对应的eventpollfs文件系统的inode节点,主要信息保存在eventpoll结构体中,而被监视的文件的重要信息则保存在epitem结构体中。在执行epoll_create和epoll_ctrl时把用户态的信息保存到内核态了,之后即使反复地调用epoll_wait,也不会像select/poll那样重复地拷贝参数,扫描fd,反复地把当前进程放入/放出等待队列。

epoll_create

epoll_create()的功能是创建一个eventpollfs文件系统的inode节点,主要信息保存在eventpoll结构体中,eventpoll记录了eventpollfs文件系统的inde节点的重要信息,其中成员rbr保存该epoll文件节点监视的所有描述符,组织的方式是一棵红黑树,被监视的文件的重要信息则保存在epitem结构体中。

epoll_ctl

Epoll_ctl实现一系列操作,它调用ep_find()从eventpoll中的红黑树获得epitem结构体,根据op参数的不同而选择不同的操作。当op为EPOLL_CTL_ADD,一般情况下epitem无法在eventpoll的红黑树中找到,所以调用ep_insert创建一个epitem结构体并插入到对应的红黑树中,

init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
…
revents = tfile->f_op->poll(tfile, &epq.pt);
…

然后ep_insert调用init_poll_funcptr注册一个回调函数ep_ptable_queue_proc,该函数会在调用f_op->poll时被执行。Ep_ptable_queue_proc函数分配一个epoll等待队列结点epoll_entry,一方面把它挂到文件操作的等待队列中,另一方面把它挂到epitem的队列中。此外它还注册了一个等待队列的回调函数ep_poll_callback,ep_poll_callback在完成操作完成,唤醒当前等待进程之前被调用,会把epitem放到eventpoll的完成队列,然后唤醒等待进程。

epoll_waite

epoll_wait的工作是等待文件操作完成并返回。它的主体是ep_poll(),该函数检查eventpoll中有没有已经完成的事件,有的话就把结果返回。没有的话调用schedule_timeout()进入休眠,直到进程被再度唤醒或者超时。

对比

select/poll的弱点在于需要轮询遍历fd,当监听fd多时开销大;而epoll依赖于回调函数,当活跃fd过多时开销就大。

监听fd的个数

消息传递的方式

是否遍历所有fd

适用场景

select

最大为FD_SETSIZE,修改需要重新编译内核

内核与用户空间的拷贝

连结数少且活跃

poll

无限制,基于链表存储

内核与用户空间的拷贝

连接数少且活跃

epoll

很大

共享内存

活跃socket少

总结与展望

其实我并不是很相信自己写的某些部分,例如poll的细节,毕竟是整合的别人的资料,但是目前为止确实能解释我的某些疑惑,以后有空有机会再去深挖求证。

参考资料

[1]http://blog.csdn.net/hguisu/article/details/7453390 [2]http://blog.csdn.net/historyasamirror/article/details/5778378

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java一日一条

Java线程面试题 Top 50

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚...

582
来自专栏企鹅号快讯

一文学会Python协程

Python圣诞学习狂欢夜 距离开始还有3天 . . . 详情 . . . 生成器和协程的介绍 生成器(Generator)的本质和特点 生成器 是 可以生成一...

22710
来自专栏互联网技术杂谈

flume 1.8.0 开发基础

Apache Flume是一个用于高效地从大量异构数据源收集、聚合、传输到一个集中式数据存储的分布式、高可靠、高可用的系统。

3266
来自专栏JAVA同学会

Zookeeper应用之——队列(Queue)

为了在Zookeeper中实现分布式队列,首先需要设计一个znode来存放数据,这个节点叫做队列节点,我们的例子中这个节点是/zookeeper/queue。

882
来自专栏牛肉圆粉不加葱

Spark 核心 RDD 剖析(上)

本文将通过描述 Spark RDD 的五大核心要素来描述 RDD,若希望更全面了解 RDD 的知识,请移步 RDD 论文:RDD:基于内存的集群计算容错抽象

602
来自专栏java一日一条

如何在 Java 中正确使用 wait, notify 和 notifyAll – 以生产者消费者模型为例

wait, notify 和 notifyAll,这些在多线程中被经常用到的保留关键字,在实际开发的时候很多时候却并没有被大家重视。本文对这些关键字的使用进行了...

642
来自专栏JAVA同学会

Zookeeper应用之——队列(Queue)

  为了在Zookeeper中实现分布式队列,首先需要设计一个znode来存放数据,这个节点叫做队列节点,我们的例子中这个节点是/zookeeper/queue...

1012
来自专栏猿人谷

进程和线程的区别

进程和线程的区别 简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 线程的划分尺度小于进程,使得多线程程序的并发性高。 另外,进程在执行过程中拥有独...

1935
来自专栏Linux驱动

45.INIT_WORK()工作队列使用

中断中通过调用schedule_work(work)来通知内核线程,然后中断结束后,再去继续执行work对应的func函数

611
来自专栏Java3y

多线程基础必要知识点!看了学习多线程事半功倍

1798

扫码关注云+社区