网络编程--io多路复用

阻塞io 和 非阻塞io

三次握手中,服务端listen之后fd放在半连接队列,accept之后是全连接队列,accept返回每个客户端fd。

阻塞在哪里?阻塞在网络线程上,阻塞io操作 accept 、connect、 read、 write

例如 read函数会一直等待内核态中的读缓冲区有新的数据

int n = read(fd, buf, sz);

1.n = 0 表示连接断开

2.n < 0 errno = EWOULDBLOCK 表示读缓冲区没有数据

io函数到底做了哪些事情?1.检测连接数据状态 2.操作具体io

io多路复用:会检测数据连接状态,并不操作具体的io;可以用一个线程检测多个连接的数据状态

为什么需要io多路复用?

阻塞io的缺点:独占线程

非阻塞io缺点:经常检测io状态消耗cpu

io多路复用有哪些? select、 poll、 epoll

如果只有一两条连接那么用阻塞io效率最高;而select能处理的最大fd数量是有限的1024,在这个范围内用select效率较高,若远大于1024则用epoll。

epoll的事件处理:边缘触发和水平触发

边缘触发ET:读缓冲区从没有数据到有数据只会触发一次(或者客户端发来数据触发),此时需要把收到的数据循环全部取出。

应用:nginx

  1. nginx反向代理不需要关注的数据完整性,在转发时使用边缘触发,因为数据量可能很大,需要尽快将数据全部转发出去。

水平触发LT:读缓冲区中有数据时会一直触发,可以一次取一部分,分多次取出。

应用:redis/memcached

  1. 比较关注数据的完整性,需要界定数据包,当接收到读缓冲区中的一段数据是一个完整的包时,剩下的数据等下一次再通知。通常需要界定数据包去做业务逻辑的都需要水平触发。
  2. 数据包小的时候;
  3. listenfd监听时多个客户端同时连接,用水平触发,因为用边缘触发只会触发一次,可能会漏掉其他clientfd

LT与ET的性能差异

二者的差异在于 level-trigger 模式下只要某个 fd 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 fd;而 edge-trigger 模式下只有某个 fd 从 unreadable 变为 readable 或从 unwritable 变为 writable 时,epoll_wait 才会返回该 fd。因此edge-trigger 模式的触发次数会比较少。

ET为什么更高效

epoll_wait 的效率是 O(ready fd num) 级别的,因此 edge-trigger 模式的真正优势在于减少了每次 epoll_wait 可能需要返回的 fd 数量,在并发 event 数量极多的情况下能加快 epoll_wait 的处理速度,但别忘了这只是针对 epoll 体系自己而言的提升,与此同时 user app 需要增加复杂的逻辑、花费更多的 cpu/mem 与其配合工作,总体性能收益究竟如何?只有实际测量才知道,无法一概而论。不过,为了降低处理逻辑复杂度,常用的事件处理库大部分都选择了 level-trigger 模式(如 libevent、boost::asio等)

C10K问题

来一个TCP连接,就需要分配一个进程。假如有C10K,就需要创建1W个进程,可想而知单机是无法承受的。当创建的进程或线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞,进程/线程上下文切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质。

C10K问题的解决方案

  1. 每个连接分配一个独立的线程/进程(客户端不多的情况下)
  2. 同一个线程/进程同时处理多个连接

io多路复用方案:

  1. select方式:使用fd_set结构体告诉内核同时监控那些文件句柄,使用逐个排查方式去检查是否有文件句柄就绪或者超时。该方式有以下缺点:文件句柄数量是有上线的,逐个检查吞吐量低,每次调用都要重复初始化fd_set,就是把所有监听的FD设置到监听集合中,从用户态拷贝到内核态,同样就绪时也会把所有fd从内核态拷贝到用户态。
  2. poll方式:该方式主要解决了select方式的2个缺点,文件句柄上限问题(链表方式存储)以及重复初始化问题(对新来的连接fd单独添加即可),但是逐个去检查文件句柄是否就绪的问题仍然没有解决。
  3. epoll方式:该方式可以说是C10K问题的killer,他不去轮询监听所有文件句柄是否已经就绪。epoll只对发生变化的文件句柄感兴趣。其工作机制是,使用"事件"的就绪通知方式,通过epoll_ctl注册文件描述符fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd, epoll_wait便可以收到通知, 并通知应用程序。而且epoll使用一个文件描述符管理多个描述符,将用户进程的文件描述符的事件存放到内核的一个事件表中, 这样数据只需要从内核缓存空间拷贝一次到用户进程地址空间。而且epoll是通过内核与用户空间共享内存方式来实现事件就绪消息传递的,其效率非常高。但是epoll是依赖系统的(Linux)。

epoll的底层实现

epoll需要管理所有fd的总集,以及一个就绪队列,那么需要设计一个kv的结构去存储这两个集合。

1.所有fd的总集,选择红黑树

hash,缺点是:空间浪费,优点是:数量很多,查找性能高

btree/b+tree,多叉树,层高低,查找效率也就是对比次数不会比红黑树,应用与磁盘索引寻址

红黑树,适合做内存中的索引查找,在插入、删除、查找的综合效率方面,红黑树是最优的。

2.就绪队列,选择队列

栈是先进后出的结构,对于先触发事件的客户端会被排到最后处理,甚至被忽略,因此选择队列。

epoll是否是线程安全的?

int epoll_create:创建出一个红黑树保存fd的集合
int epoll_ctl:ADD增加fd,DEL删除fd,MOD修改fd,操作的是红黑树,需要对红黑树加互斥锁
int epoll_wait:获取就绪队列中的fd节点,操作的是就绪队列,对就绪队列加自旋锁

为什么对与队列而言选择自旋锁?因为互斥锁它在拿不到锁的时候会让出cpu,而自旋锁不会,队列的增删操作只是简单的几条指令,速度快,因此自旋锁等待时间内,比让出cpu之后再切换回来的开销小。

时序图

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值