网络IO模型

文章内容来源于:https://segmentfault.com/a/1190000003063859?utm_source=Weibo&utm_medium=shareLink&utm_campaign=socialShare&from=timeline&isappinstalled=0
以及极客时间专栏

基本概念

程序内核空间与用户空间: 在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。它首先会被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的缓冲区;

同步与异步:IO资源可用与否是自己去检测,还是依赖于状态、信号、回调等其它机制来通知(得到结果的方式);

阻塞与非阻塞: IO调用的函数在资源不可用时候是否立即返回,还是被挂起状态直到资源可用(线程的状态);


image.png
  • read 总是在接收缓冲区有数据时就立即返回,不是等到应用程序给定的数据充满才返回。当接收缓冲区为空时,阻塞模式会等待,非阻塞模式立即返回 -1,并有 EWOULDBLOCK 或 EAGAIN 错误。
  • 和 read 不同,阻塞模式下,write 只有在发送缓冲区足以容纳应用程序的输出字节时才返回;而非阻塞模式下,则是能写入多少就写入多少,并返回实际写入的字节数。
  • 阻塞模式下的 write 有个特例, 就是对方主动关闭了套接字,这个时候 write 调用会立即返回,并通过返回值告诉应用程序实际写入的字节数,如果再次对这样的套接字进行 write 操作,就会返回失败。失败是通过返回值 -1 来通知到应用程序的。
    举例说明:
    同步阻塞:老王烧水,在炉子旁死等;
    同步非阻塞:老王烧水,去看报,每隔一段时间过来看一眼,只等水开;
    异步阻塞:老王用带哨子的壶烧水,自己去客厅等,壶响了过来;
    异步非阻塞:老王用带哨子的壶烧水,自己在客厅看报,壶响了过来

常见网络模型

  1. 同步阻塞


    image.png

    recvfrom默认为阻塞状态;而且两个阶段都为阻塞

  2. 同步非阻塞模型


    image.png

    将recvfrom设为非阻塞状态,若无数据准备好立即返回错误 EWOULDBLOCK。当内核无数据准备好时,非阻塞,当内核有数据执行recvfrom时,为阻塞。

  3. IO多路复用(select、poll、epoll)


    image.png

    虽然两个阶段都为阻塞,但是与同步阻塞模型相比,它可以同时监听多个socket句柄。I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。事件有,可读可写,套接字就绪,超时。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

一般IO多路复用与非阻塞配合使用,因为多路复用返回与实际进行读写非原子的。非阻塞 I/O 可以使用在 read、write、accept、connect 等多种不同的场景,在非阻塞 I/O 下,使用轮询的方式引起 CPU 占用率高,所以一般将非阻塞 I/O 和 I/O 多路复用技术 select、poll 等搭配使用,在非阻塞 I/O 事件发生时,再调用对应事件的处理函数。这种方式,极大地提高了程序的健壮性和稳定性,是 Linux 下高性能网络编程的首选。

  1. 异步非阻塞IO


    image.png
  2. 比较


    image.png

    只有异步IO模型才是纯异步非阻塞模型。其他四种第二阶段都为阻塞。


    image.png

IO多路复用技术

  1. select
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
maxfd 表示的是待测试的描述符基数,它的值是待测试的最大描述符加 1

void FD_ZERO(fd_set *fdset);      
void FD_SET(int fd, fd_set *fdset);  
void FD_CLR(int fd, fd_set *fdset);   
int  FD_ISSET(int fd, fd_set *fdset);

最多只支持1024个描述符

  1. poll
int poll(struct pollfd *fds, unsigned long nfds, int timeout); 

返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1

struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };
  1. epoll
int epoll_create(int size);
int epoll_create1(int flags);

返回值: 若成功返回一个大于0的值,表示epoll实例;若返回-1表示出错

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

返回值: 若成功返回0;若返回-1表示出错

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

返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1.

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 */
 };
  1. 三者比较


    image.png
  • epoll监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大
  • epoll支持边缘触发与条件触发
  • epoll可以直接得到有事件发生的数组,select/poll返回的是事件就绪的个数,需要遍历注册的文件描述符数组,才能确定哪些描述符有事件发生
  • poll/select 先将要监听的 fd 从用户空间拷贝到内核空间, 然后在内核空间里面进行处理之后,再拷贝给用户空间。这里就涉及到内核空间申请内存,释放内存等等过程,这在大量 fd 情况下,是非常耗时的。而 epoll 维护了一个红黑树,通过对这棵黑红树进行操作,可以避免大量的内存申请和释放的操作,而且查找速度非常快
  • epoll IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。select/poll 从休眠中被唤醒时,如果监听多个 fd,只要其中有一个 fd 有事件发生,内核就会遍历内部的 list 去检查到底是哪一个事件到达,并没有像 epoll 一样, 通过 fd 直接关联 eventpoll 对象,快速地把 fd 直接加入到 eventpoll 的就绪列表中

高并发网络模型设计

C10K--单机支持10000并发
设计重点是,在进行IO事件处理时,合理分配进程线程资源,处理以下任务:

  • accept 根据监听套接字获取已连接套接字
  • read:从套接字收取数据;
  • decode:对收到的数据进行解析;
  • compute:根据解析之后的内容,进行计算和处理;
  • encode:将处理之后的结果,按照约定的格式进行编码;
  • send:最后,通过套接字把结果发送出去。
  1. 阻塞IO+进程


    image.png
  2. 阻塞IO+线程(池)


    image.png
  3. 非阻塞 I/O + readiness notification(多路复用) + 单线程


    image.png
  4. 非阻塞 I/O + readiness notification(多路复用) + 多线程
    事件驱动模型的设计的思想是,一个无限循环的事件分发线程在后台运行,一旦做了某种操作触发了一个事件,这个事件就会被放置到事件队列中,事件分发线程的任务,就为这个发生的事件找到对应的事件回调函数并执行它。事件驱动的好处是占用资源少,效率高,可扩展性强,是支持高性能高并发的不二之选。

Reactor模型——解决了空闲连接占用资源的问题,Reactor线程只负责处理 I/O 相关的工作,业务逻辑相关的工作都被裁剪成一个一个的小任务,放到线程池里由空闲的线程来执行。当结果完成后,再交给反应堆线程,由Reactor线程通过套接字将结果发送出去。

  • single reactor thread + worker threads


    image.png
  • single主 -多 从 reactor threads 模式


    image.png
  • single主 - 多从 reactor threads+worker threads 模式(netty)


    image.png

    这里将 decode、compute、encode 等 CPU 密集型的工作从 I/O 线程中拿走,这些工作交给 worker 线程池来处理,而且这些工作拆分成了一个个子任务进行。encode 之后完成的结果再由 sub-reactor 的 I/O 线程发送出去。

  1. 异步IO+多线程

你可能感兴趣的:(网络IO模型)