I/O模型

I/O模型

一个输入操作通常包括两个阶段:
1. 等待数据准备好
2. 从内核向进程复制数据

unix下有5种I/O模型
- 阻塞IO
- 非阻塞IO
- IO复用
- 信号驱动式IO
- 异步IO

阻塞I/O

应用进程被阻塞,直到数据复制到应用进程缓冲区才返回。
这里写图片描述

非阻塞式I/O

应用进程执行系统调用时,内核返回一个错误码。应用进程可以继续执行,但需要不断的执行系统调用来获知IO是否完成,这种方式称为轮询。
这里写图片描述

I/O复用

使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。
这里写图片描述

信号驱动I/O

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
这里写图片描述

异步I/O

进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
这里写图片描述

同步I/O与异步I/O

  • 同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。
  • 异步 I/O:不会阻塞

I/O复用

select

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。fd_set使用数组实现,数组大小使用FD_SETSIZE定义。
timeout为超时函数,调用select会一直阻塞直到有描述符的时间到达或者等待堵塞时间超过timeout。
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。

fd_set fd_in, fd_out;
struct timeval tv;

FD_ZERO(&fd_in);
FD_ZERO(&fd_out);

FD_SET(sock1,&fd1);
FD_SET(sock,&fd2);

int largest_sock = sock1 > sock2 ? sock1:sock2;

tv.tv_sec = 10;
tv.tv_usec = 0;

int ret = select(largest_sock + 1,&fd_in,&fd_out,NULL,&tv);
if(ret == -1)
    // report error and abort
else if(ret == 0)
    // timeout; no event detected
else
{
    if(FD_ISSET(sock1,&fd_in);
        // input event on sock1
    if(FD_ISSET(sock2,&fd_out))
        // output event on sock2    
}

poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout);
pollfd使用链表实现。

strcut pollfd fds[2];
fd[0].fd = sock1;
fd[0].events = POLLIN;

fd[1].fd = sock2;
fd[1].events = POLLOUT;

int ret = poll(&fds, 2, 10000);
if(ret == -1)
    // Check if poll actually succeed
else if(ret == 0)
    // timeout; no event detected
else
{
    if(pfd[0].revents & POLLIN)
    {
        pfd[0].revents = 0;
        // input event on sock1
    }
    if(pfd[1].revents & POLLOUT)
    {
        pfd[1].revents = 0;
        // output events on sock2
    }
}

select 和 poll 的比较:
1. select会修改文件描述符,而poll不会
2. select的文件描述符使用数组实现,FD_SETSIZE 大小默认为 1024, 。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符的数量的限制;
3. poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
4. 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定
5. select和poll,每次调用都需要将全部描述符从应用缓冲区复制到内核缓冲区
6. select和poll的返回结果中没有声明哪些文件描述符已经准备好,如果返回值大于0时,应用进程都需使用轮询方式来找到IO完成的描述符。
7. 几乎所有系统都支持select,但只有比较新的系统支持poll。

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_ctl()向内核注册新的文件描述符或改变某个描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上。通过回调函数内核将IO准备好的描述符加入到一个链表中管理,进程调用epoll_wait()便可以得到事件完成的描述符。

epoll只需要将文件描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll仅使用于Linux,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

int epfd = epoll_create( 0xCAFE ); // size
if ( pollingfd < 0 )
 // report error

struct epoll_event ev = { 0 };
ev.data.ptr = pConnection1;
ev.events = EPOLLIN || EPOLLONESHOT;

if(epoll_ctl(epfd,EPOLL_CTL_ADD, pConnection1->getSock(), &ev) != 0)
    // report error
struct epoll_event pevents[ 20 ];
int ready = epoll_wait(epfd, pevents, 20, 10000 );
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    // Check if any events detected
    for ( int i = 0; i < ret; i++ )
    {
        if ( pevents[i].events & EPOLLIN )
        {
            // Get back our connection pointer
            Connection * c = (Connection*) pevents[i].data.ptr;
            c->handleReadEvent();
         }
    }

epoll的描述符有两种工作模式:LT 和 ET
1. LT,当epoll_wait()检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用epoll_wait()会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
2. ET,和LT模式不同的是,通知之后进程必须立即处理事件,下次再调用epoll_wait()时不会再得到事件的通知。很大程度减少了epoll事件被重复触发的次数,效率要比LT高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

应用场景

  1. select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如核反应堆的控制。select 可移植性更好,几乎被所有主流平台所支持。
  2. poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
  3. epoll只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值