IO模型与IO多路复用

Linux下的五大网络IO模型

引言

在讲IO模型之前,我们先了解了解Linux进程数据通信。一个应用程序对于OS来说,就是一个进程,进程拥有与其它进程共享的内核空间,也拥有私有的用户空间,而从访问权限上看,用户进程只能访问用户空间,系统进程才能访问内核空间,当进程需要数据通信时候,就向内核空间请求数据,并且将内核空间的数据拷贝到用户空间进行处理。

IO模型

下面,我们将讲解Linux下的5种IO模型,分别是:阻塞IO、非阻塞IO、IO复用、信号驱动IO、异步IO

阻塞IO

发送请求就阻塞当前线程,直到内核返回数据,例如Socket,当调用recvfrom之后,就会一直等待。

img

非阻塞IO

发送请求之后,接收到一个返回状态码,由状态码判断进行阻塞还是已经获取数据,例如调用recvfrom,如果收到EWOULDBLOCK状态,则证明没有数据,这个时候,选择定时进行轮询。

img

IO复用

IO复用全称为IO多路复用,这里做个简单的描述,IO理解为网络IO,多路意味着多通道、多连接,复用是指用一个线程、一组线程去处理多个通道的IO请求。进程通过将一个或者多个fd传递给select/poll调用,这样select/poll就能检测多个fd的状态,当有fd就绪时,立刻调用rollback回调函数,执行对应操作。

img

IO复用有多种实现机制,具体的实现机制在后面进行详细的说明。

信号驱动IO

首先开启套接字的信号驱动IO功能,通过系统调用sigation执行一个信号处理函数,当数据准备就绪,通过生成一个sigio信号,并且通过信号的回调,通知进程recvfrom读取数据进行处理。就类似与银行排号,当叫到你的时候,就可以去处理业务了。

img

异步IO

异步IO是真正的异步模型,发出请求就返回,剩下的事情会异步自动完成,不需要做任何处理。好比有事秘书干,自己啥也不用管。 img

异步IO与信号驱动IO的区别在于,异步IO是系统帮我们异步执行完,通知我们已经完成了,而信号驱动则是系统通知我们,可以去执行了。


深入IO复用模型

IO复用模型一共有三种:

  • select
  • poll
  • epoll

下面,深入讲解这三种IO模型,其中select/poll较为相似。

select

基础原理

  • 将每个fd拷贝到内核空间,然后采用轮询(遍历)的机制,查询每个fd的设备状态,当就绪时,通知进程调用对应的函数去处理数据。

运行机制与说明

  • select是基于文件描述符集合的,因此,支持的连接数受到系统的最大文件描述符大小限制,可通过该命令查看:
cat /proc/sys/fs/file-max
  • 围绕函数入口来讲解
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

【参数说明】
int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。
**fd_set *readset , fd_set writeset , fd_set exceptset
fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。
const struct timeval *timeout timeout告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

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

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

存在问题

  • 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  • 复杂度在O(N)

poll

基础原理

  • poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制,是基于链表实现。

运行机制与说明

先上函数

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

typedef struct pollfd {
        int fd;                         // 需要被检测或选择的文件描述符
        short events;                   // 对文件描述符fd上感兴趣的事件
        short revents;                  // 文件描述符fd上当前实际发生的事件
} pollfd_t;

poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024

【参数说明】

struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域

nfds_t nfds 记录数组fds中描述符的总数量

【返回值】
int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

存在问题

  • FD在用户和内核态之间的拷贝问题
  • 同样遍历的问题

epoll

基础原理

  • epoll是linux2.6之后提出的,是基于事件机制的IO复用,与select/poll区别如下:
    • epoll没有文件描述符数量限制
    • 由于加入事件,因此只将用户关心的事件类型的FD拷贝到内核空间
    • epoll并未使用到MMAP

运行机制与说明

上原型代码

//创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。
int epoll_create(int size);

//epfd 表示epoll句柄
//op 表示fd操作类型,有如下3种:
//EPOLL_CTL_ADD 注册新的fd到epfd中
//EPOLL_CTL_MOD 修改已注册的fd的监听事件
//EPOLL_CTL_DEL 从epfd中删除一个fd
// fd 是要监听的描述符
//event 表示要监听的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//注册要监听的事件类型

//等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//epfd 是epoll句柄
//events 表示从内核得到的就绪事件集合
//maxevents 告诉内核events的大小
//timeout 表示等待的超时事件
struct epoll_event {
    __uint32_t events;  /* Epoll events */
    epoll_data_t data;  /* User data variable */
};

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

具体描述

  • 它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
  • 遍历过程采用的是红黑树数据结构,加快查询速度。

工作流程

(1) epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。

(2) 文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。

(3) ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。

(4) ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。

(5) ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对应的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。之后如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist,否则(ET模式)不在加入rdlist。

LT

  • 默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件(不改变状态);下次调用epoll_wait时,会再次通知此事件

ET

  • 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理(没有改变FD的事件状态),下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

两者之所以通知多次与一次的原因在于:

  • 通知是通过rdlist来处理的,源码:

    list_add_tail(&epi->rdllink, &ep->rdllist);
    
  • 当选择LT,fd加入rdlist的情况有两种,而在ET情况下,只有一种:

    • LT当状态改变会加入到RDLIST中;当epoll_wait调用到但是不处理,仍然会加入到RDLIST中。
    • ET当状态改变才会加入到RDLIST中。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值