Socket 的 I/O模型

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_32682177/article/details/96727093


在设计网络通讯程序时,需要通过一种机制来确定网络中某些事件的发生。例如,当主机A向主机B发送数据时,在主机B接收到数据时需要让应用程序读取数据,那么应用程序何时读取数据呢?也就是说,应用程序如何确定网络中何时有数据需要接收呢?这就需要在设计网络应用程序时选择一个I/O模型。在Windows操作系统中,I/O模型主要有6种,下面分别介绍。

阻塞式 I/O

应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回

非阻塞式I/O

当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。相当于你去查看有没有数据,告诉你没有,过一会再来吧!应用过一会再来问,有没有数据?没有数据,会有一个返回。但是依旧很不好。应用必须得过一会来一下,问问内核有木有数据啊。这和现实很像啊!好多情况都得去某些地方问问好了没有?木有,明天再过来。明天,好了木有?木有,后天再过来。。。。。忙碌的应用。。。。

I/O复用

I/O复用概述

I/O复用
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符(FileDescription,简称FD),如果有一个文件描述符(FileDescription)就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。虾米意思?就是派一个代表,同时监听多个文件描述符是否有数据到来。等着等着,如有有数据,就告诉某某你的数据来啦!赶紧来处理吧。有没有很感动,一个人待着,帮了很多人。医院的黄牛,一个人排队,大家只要把钱给它,它就会把号给需要的人,开个玩笑。。。。
˝在这里插入图片描述

select和recvfrom

  1. select
    理解了select就抓住了I/O多路复用的精髓,对应的操作系统中调用的则是系统的select函数,该函数会等待多个I/O事件(比如读就绪)的任何一个发生,并且只要有一个网络事件发生,select线程就会执行。如果没有任何一个事件发生则阻塞。我们在下面小节中会重点讲述。函数如下:
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
  1. recvfrom
    用于从(已连接)套接口上接收数据。

阻塞、非阻塞

在这里插入图片描述
这张图可以看出阻塞式I/O、非阻塞式I/O、I/O复用、信号驱动式I/O他们的第二阶段都相同。
阻塞式I/O和I/O复用,两个阶段都阻塞,那区别在哪里呢?就在于第三节讲述的Selector,虽然第一阶段都是阻塞,但是阻塞式I/O如果要接收更多的连接,就必须创建更多的线程。I/O复用模式下在第一个阶段大量的连接统统都可以过来直接注册到Selector复用器上面,同时只要单个或者少量的线程来循环处理这些连接事件就可以了,一旦达到“就绪”的条件,就可以立即执行真正的I/O操作。这就是I/O复用与传统的阻塞式I/O最大的不同。也正是I/O复用的精髓所在。

I/O 多路复用 select、poll 和 epoll

select、poll和epoll

select

我们先分析一下select函数:

int select(int maxfdp1, fd_set *readset, d_set *writeset,fd_set *exceptset,const struct timeval *timeout);

【参数说明】
(1)int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1
(2)readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义。
(3)timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。

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

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

【select机制的问题】

  1. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  3. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);

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

pollfd 使用链表实现

select 和 poll 的比较
  1. 功能
    select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。

    • select 会修改描述符,而 poll 不会;
    • select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制;
    • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高
  2. 速度
    select 和 poll 速度都比较慢

    • select 和 poll 每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区
    • select 和 poll 的返回结果中没有声明哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程都需要使用轮询的方式来找到 I/O 完成的描述符
  3. 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_create 函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。
  • epoll_ctl 函数注册要监听的事件类型。
  • epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

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

select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大连接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

猜你喜欢

转载自blog.csdn.net/qq_32682177/article/details/96727093