IO多路复用模型及网络服务器模型

同步,异步和阻塞

同步:客户端请求服务器的内容,一直等待内容返回,期间客户端不做别的。

异步:调用后不会立刻得到结果,调用完成后通过状态,通知或者回调来通知调用者。例如ajax异步请求。

阻塞:调用返回前,线程/进程被挂起,不使用cpu资源。对同步调用,线程还是激活,只是逻辑上函数没有返回。例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。

  1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
  2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
  3. 阻塞, 就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
  4. 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者

通俗点的例子

(忘了从哪copy的)

老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 老张把水壶放到火上,立等水开。(同步阻塞)
    老张觉得自己有点傻
  2. 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
    老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
  3. 老张把响水壶放到火上,立等水开。(异步阻塞)
    老张觉得这样傻等意义不大
  4. 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
    老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

五种IO模型

阻塞IO

recvfrom,recv,一直等内核准备数据,如果缓冲区没有数据,调用线程在数据到来前一直睡眠。默认的socket是阻塞的。
send,sendto,发送缓冲区没有空间会一直睡眠直到有空间。
accept,等待连接请求,没有睡眠
connect,发主动连接,收到服务器应答前不会返回

非阻塞IO

反复进行系统调用,没有数据立刻返回,有数据阻塞拷贝数据然后返回。
socket设置成非阻塞,当请求的IO无法完成时不睡眠而是返回错误。

IO复用(事件驱动IO)

select,阻塞等待多个套接字中的一个变成可读,内核返回可读条件;再调用recvfrom读数据。
可以同时阻塞多个IO操作,对多个读,写IO函数进行检测,有可读或可写时调用真正的IO操作。
优势在于单个进程可以同时处理多个网络连接的IO

信号驱动IO

建立SIGIO信号处理程序,内核准备好数据通知应用,进入信号处理程序

异步IO

aio_read,系统调用直接返回,内核准备好后提交在aio_read指定的信号中

对比

IO多路复用模型及网络服务器模型_第1张图片

IO多路复用机制

  • select,对socket线性扫描,轮询,需要维护存放大量fd的数据结构,使得用户空间和内核空间传递该结构时开销增大,监听的端口数量受限。内核对文件描述符集合进行线性扫描,检查是否有事件产生,做标记后把集合拷贝回用户态,用户态遍历并寻找可读写的socket。需要两次遍历,两次拷贝。使用固定长度的BitsMap表示文件描述符集合,受FD_SERSIZE限制默认最大1024。
  • poll,基于链表来存储,监听不受限。线性遍历。传递数据结构开销大。不再受到FD_SETSIZE限制,但是仍收到操作系统的socket数目限制。
  • 上述两个本质都是线性结构存储进程的socket集合,并发数高性能损耗指数级增长。
  • epoll,使用事件的就绪通知方式,使用红黑树跟踪文件描述符,通过epoll_ctl注册fd,该fd就绪,内核会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知,把这个socket加入就绪事件的链表。不受并发连接数限制,效率提升,非轮询,使用mmap映射,内核空间用户空间共享一块内存来实现(epoll源码中epoll_wait还是对数据从内核和用户进行拷贝) 。在所有socket都很活跃的情况下,才会有性能问题。

表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

epoll的两种触发模式

  • 边缘触发:被监控的socket有事件发生,epoll_wait苏醒一次,通知用户可以读数据
  • 水平触发:…,不断地从epoll_wait苏醒,直到缓冲区数据读完。不断地检测是否依旧可以读写,不断通知用户。

边缘触发IO事件只发生一次,不知道读写多少数据,只能尽可能多的读写,所以会循环读写,应该使用非阻塞的IO。所以边缘触发一般搭配非阻塞IO
一般来说边缘触发效率比水平触发高,可以减少epoll_wait调用次数。select/poll只有水平触发模式,epoll默认是水平触发,可以设置成边缘触发。

网络服务器模型

循环服务器

bind,死循环,recv,process,send

并发服务器

服务器主进程构建多个子进程,请求来的时候随便选一个子进程处理客户端连接。
如何确定子进程数量,动态增加
bind后,fork,传入socket,子进程死循环,recv,send
对信号进行处理,接收到信号所有子进程退出
bind,fork(accept,recv,process,send)

TCP高级并发服务器模型

主进程统一处理客户端连接,连接到来以后才fork,子进程处理请求。
bind,listen,死循环,accept,fork(recv,process,send)
线程更轻量。
多线程,线程内(accept,recv,process,send),需要设置互斥锁,防止一起调用accept
线程先进入互斥区,再accept,离开互斥区。

IO复用循环服务器

并发服务器的缺陷,建立多个并行处理单元,客户端增加时,负载转移到并行单元的现场切换,嵌入式系统尤为明显。IO复用循环服务器为了降低系统切换的开销,集中系统能力在核心业务。在系统开始时,建立多个不同工作类型的处理单元,处理连接,业务。客户端连接来,放到一个状态池中,对所有客户端连接状态在一个处理单元中进行轮询。
两个线程:

  • handle_connect:负责accept,然后扔到连接池
    死循环,accept,放到连接池
  • handle_request:负责从连接池里取连接,处理数据
    死循环,从连接池(文件描述符数组)取合法的描述符放进select的文件描述符集合中;然后select,等待数据到来或者超时,判断返回值合法性,判断是否此文件描述符数据到来,recv,process,send;处理完毕在连接池中标记该文件描述符已经处理过,然后关闭该连接。

参考资料

小林coding - 这次答应我,一举拿下 I/O 多路复用!

你可能感兴趣的:(socket,网络通信)