linux网络编程之IO模型

单线程 + 阻塞IO

代码逻辑

主线程创建一个listenfd,bind端口并listen,之后accept一个confd,进行通信(recv + send)。

特点

  • accept连接和对该连接的处理(读写操作)均在同一个线程中进行。
  • 最原始的tcp通信代码范例,只适用与点对点通信,无法为多个客户端提供服务。
  • 端口处于监听状态,可以响应多个客户端的连接请求(三次握手在内核的tcp协议栈中自动进行),同时多个客户端也可以给服务端发数据(数据通过互联网送到服务端主机的网卡中,并进入对应tcp连接的接收缓冲区内),但由于应用层程序只取出了一个连接并recv和send,故只有一个连接的confd在应用层被取到(通过系统调用将位于内核的某个连接的接收缓冲区中的数据copy到应用层),并进行解析最后将结果发送给此客户端。其他的客户端均未得到服务(发送的请求滞留在了内核缓冲区内,没有被应用层获取并处理)。同时如果当前连接没有数据到达,则会发生读阻塞,此时即使其他连接有数据到达,程序也处理不了,只能等当前阻塞的连接读就绪。

多线程 + 阻塞IO

代码逻辑

主线程创建一个listenfd,bind端口并listen,之后是一个while循环:accept在循环内,每当accept一个confd,就创建一个子线程并将此连接的服务安置在这个子线程内,子线程运行函数为一个while循环:不断进行 读 + 解析 + 写 的操作。

特点

  • 主线程 + 若干子线程的结构,主线程不断取出连接并交给子线程,单个子线程只负责特定连接的所有服务,主线程和子线程各司其职,将取出连接的操作和对连接的服务进行了分离,避免了单一线程同时处理连接和提供服务而导致无法并发的问题。
  • 只适用于小规模并发连接的场景,一般1k以内,无法逾越C10K(一万连接)。原因:当并发连接的数量过多时,会产生大量的子线程,需要大量的内存空间来存放线程栈,导致内存不足;同时线程太多会导致频繁的切换,最终可能线程切换所用时间比执行时间业务所用时间还大。

IO多路复用

代码逻辑

主线程将listenfd加入监控集合,然后调用select/poll/epoll_wait获取当前活跃(可读/可写/异常)的fd集合,然后使用accept获取新连接并将其confd加入待检测集合,然后依次为每个已有活跃连接提供服务。

特点

  • 避免了多线程,使用单一线程(多个IO复用同一个线程)对多个连接进行了处理。由于是用单个线程为多个连接提供服务,所以在宏观上并发,但在微观上是串行的。
  • 需要区分lisenfd 和 confd。

单线程select特点

  • 单个select只能监控1024个fd。
  • 一个select结构占用一个线程,可以监控1023个confd(其中一个fd被listenfd占了),通过使用多线程(多个select结构),可以突破C10K的限制,但无法突破C1000k(百万连接)。
  • 具有配套的fd_set结构体,默认有1024个bit位的数组(在64位机的情况下,是一个长度为16的long数组)。select结构具有三种类型的fd_set,分别表示可读集合、可写集合、异常集合。
  • select函数的功能:(1)传入应用层的待检测集合数组,copy给内核对应数组,传入轮询的最大边界(2)在内核中进行轮询 (3)将轮询结果copy给应用层的数组并返回状态值。
  • select函数的特点:(1)每次使用完都会将内核中的数组重置,原因:既然已经将当前非阻塞的fd事件都告诉了应用层,那么必须重置,等待下一次事件到来在将相应位置1。(2)默认是阻塞运行:如果轮询后发现没有任何一个非阻塞的fd事件,则程序主动阻塞,直到某一时刻有事件到来(某些位由0变成1),程序被唤醒并立即返回结果集;也可设置为非阻塞运行(将timeout参数设置为0),轮询一遍,不管有没有非阻塞的fd事件均立即返回,返回大于0表示当前非阻塞的fd事件个数,返回等于0表示没有事件触发,返回小于0表示select发生错误。
  • 缺陷1:每次调用select函数需要将用户态的事件数组拷贝到内核空间,目的是告诉select组件需要监控哪些fd,然后轮循内核中的fd_set中的fd并找到可读(可写/异常)的那些fd集合,然后重置内核中的fd_set,最后将结果集拷贝到用户态。
  • 缺陷2:copy出来的结果集长度仍为1024,只知道结果集内有几个就绪的fd,但不知道他们具体在结果集的第几位,仍然需要轮询一遍才能在应用层取出那些非阻塞的fd。
  • 缺陷3:对于内部的fd_set,采用轮循的方式来确定哪些fd是可读(可写/异常)。

单线程poll特点

  • 具有配套的pollfd结构体来表示事件,对比select需要三个事件数组,poll只使用一个pollfd数组就能实现,在外部接口上更加简洁了,但在内核内走的流程同select相同,所以在性能上没有任何提升。
  • poll是水平触发。
  • poll内部是链式结构,所以没有大小的限制。
  • poll函数特点:与select函数高度相似。
  • 缺陷1:同select。
  • 缺陷2:同select。
  • 缺陷3:同select。

单线程epoll特点

  • 相比于select和poll,使用了三个配套函数(epoll_wait、epoll_ctl、epoll_wait)和两个结构(待检测fd集合、就绪队列)。
  • 内部结构为一个红黑树(保存待检测fd集合) + 双向链表(就绪队列,用来保存非阻塞fd集合)。
  • epoll_wait会从内核中的就绪队列里拷贝指定数量的fd到用户空间,相比于select和poll而言,只需copy出来,而无需copy进去,。
  • select和poll虽然也是事件触发,但没有将触发的那些事件单独拎出来形成一个单独的数据结构,这样大概率会导致必须轮询一遍整个待检测集合才能得到哪些非阻塞的fd事件。而epoll使用了两个结构:红黑树和双向列表,其中红黑树用来保存待检测集合,而双向链表用来保存触发的事件,epoll_wait会将双向链表的事件copy到用户层的events数组内,在用户层遍历时无多余的操作,提升了效率。

epoll相比于select和poll的优势

  • epoll最核心的优势就是使用了两个结构,一个是红黑树,用来保存所有需要检测的事件,一个是链表,用来保存当前触发的事件。同时在创建了epoll对象后,即使在不调用epoll_wait的情况下,内核也会自动检测红黑树里的事件,当触发了就将对应的事件结点复制一份到链表里,这样调用了epoll_wait后就能拿到当前所有的活跃事件(无脑将整个链表(活跃集)copy到应用层);而select总共只有一个总集,相比于epoll来说少了一个关键的活跃集,这样导致程序必须要在内核里轮询一次(而epoll在内核里无需轮询,只有copy的动作)来确定具体哪些bit位是1,然后再把结果copy到应用层。epoll的本质就是空间换时间
  • 就事件触发这一点上,select、poll、epoll都能实现,epoll的优势只是将触发的那些事件用一个数据结构给保存下来了,而select和poll没有保存,所以程序只能在内核里的总集里去找(轮询),这样效率就差很多了,尤其是在总连接数很多但活跃连接很少情况下。

水平触发和边缘触发的理解

  • 水平触发:缓冲区内有数据就一直触发;边缘触发:有新数据到缓冲区才触发。
  • 边缘触发应该设置为循环读,直到读完;水平触发则不用循环。
  • 小数据量一般用边缘触发;大数据量一般用水平触发;listenfd必须用水平触发。

单线程reactor

在这里插入图片描述

特点

  • reactor以epoll为核心,在其基础上多了一层封装。相比与原生的epoll,从IO的管理变成了对事件 + 回调函数的管理模式。虽然在性能上没有提升,但提高了代码的复用性和扩展性,常作为高性能网络框架使用。
  • 同一sockfd对应一个事件结构体,同一事件结构体在同一时刻只绑定一个事件和回调函数。
  • 在应用层使用若干eventblock内存块对所有事件进行存储,利用了同一进程内创建的sockfd连续递增的特点,配合整除和取余快速定位事件的位置。
  • 对于感兴趣的事件需要事先注册到reactor(epoll)中,这样内核才会帮忙监视。
  • 三中类型的事件(新连接到来,已有连接的对端发来数据,向已有连接的对端发送数据)的处理在三个回调函数内完成(accept_cb, recv_cb、send_cv)。
  • 当连接事件到来时,会触发accept_cb:调用accept获取此新连接的sockfd,将其注册为EPOLLIN事件,回调函数设置为recv_cb,加入到epoll中(表示此时多了一个需要监听的读事件)。
  • 当读事件到来时:会触发recv_cb:调用recv读取发送来的数据(需要保证读完一个完整的请求),如果读到了正常的数据,则进行消息解析 + 消息处理,将处理结果保存到对应的event结构里,然后将此事件修改为EPOLLOUT,回调函数修改为send_cb(表示此时服务器已经处理了此请求,需要将结果发送给对端,但是由于不知道sendbuffer是否可写,所以需要交给epoll来帮忙监听)。;如果读到了fin报文,表示对端结束连接,那么直接将此event结构从reactor和epoll中删除。
  • 当写事件到来时:会触发send_cb:表明此写事件对应连接的sendbuffer可写(未满),则调用send将之前保存好的处理结果取出并发送到sendbuffer(之后由cp协议栈将其打包成tcp数据包通过网络发送给对端),然后将其事件再复原为EPOLLIN,回调函数也复原为recv_cb(表示服务器已经完成了本次请求,并等待此连接的下一次请求)。

应用场景

redis、skynet、netty

多线程reactor

在这里插入图片描述

特点

根据机器CPU核心数来设置reactor个数,每个线程内运行一个reactor;其中有一个reactor专门处理新连接请求,通过此reactor将新连接负载均衡到其他reactor中去。

应用场景

memcached

多进程reactor

在这里插入图片描述

特点

master主进程fork多个子进程worker,每个worker内运行一个reactor,均绑定和监听同一个网络端口,多个进程通过共享内存来竞争锁,拿到锁的worker将accept新连接到自己的reactor中去。

应用场景

nginx

协程 + reactor

特点

应用场景

,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值