C10K问题:
网络服务在处理数以万计的客户端连接时,往往出现效率底下甚至完全瘫痪,这被成为C10K问题。
(C10K = connection 10 kilo 问题)。k 表示 kilo,即 1000 比如:kilometer(千米), kilogram(千克)。
非阻塞I/O,最关键的部分是readiness notification(when ready, then notify!)和找出哪一个socket上面发生了I/O事件。
一般我们首先会想到用select来实现。
int select(int n, fd_set *rd_fds; fd_set *wr_fds, fd_set *ex_fds, struct timeval * timeout);
其中用到了fd_set结构,而fd_set不能大于FD_SETSIZE,默认是1024,很容易导致数组越界。
针对fd_set的问题,*nix提供了poll函数作为select的一个替代品:
int poll(struct pollfd *ufds, unsigned int nfds, int timeout)
第一个参数ufds是用户提供的一个pollfd数组,大小由用户自行决定,因此避免了FD_SETSIZE带来的麻烦。
然而select和poll在连接数增加时,性能急剧下降。这有两方面的原因:
《1》首先操作系统面对每次的select/poll操作,都需要重新建立一个当前线程的关心事件链表,并把线程挂在这个复杂的等待队列上,这是相当耗时的。
《2》其次,应用软件在select/poll返回后也需要对传入的句柄链表做一次循环扫描来dispatch,这也是很耗时的。这两件事都是和并发数相关,而I/O事件的密度也和并发数相关,导致CPU占用率和并发数近似成O(n2)的关系。
基于以上原因,*nix的hacker们开发了epoll, kqueue, /dev/poll这3套利器。epoll是Linux的方案,kqueue是freebsd的方案,/dev/poll是solaris的方案。
简单的说,这些api做了两件事:
《1》避免了每次调用select/poll时kernel分析参素建立事件等结构的开销,kernel维护一个长期的时间关注列表,应用程序通过句柄修改这个链表和捕获I/P事件
《2》避免了select/poll返回后,应用程序扫描整个句柄表的开销,kernel直接返回具体的链表给应用程序。
在接触具体api之前,先了解一下边缘触发(edge trigger)和条件触发(level trigger)的概念。边缘触发是指每当状态变化时发生一个io事件,假定经过长时间的沉默后,现在来了100个字节,这是无论边缘触发和条件触发都会产生一个read
ready notification通知应用程序可读。应用程序在读完来的50个字节,然后重新调用api等待io事件。这时条件触发的api会因为还有50个字节可读从而立即返回用户一个read ready notification。而边缘触发的api会因为可读这个状态没有发生变化而陷入长期等待。
因此在使用边缘触发的api时,要注意每次都要读到socket返回EWOULDBLOCK为止,否则这个socket就算废了。而使用条件触发的api时,如果应用程序不需要写就不要关注socket可写的事件,否则会无限次的立即返回一个write ready nitification. 大家常用的select就是属于条件触发这一类,以前本人翻过长期关注socket写事件从而CPU 100%的毛病。
本文深入探讨了C10K问题及其解决方案,从传统的select函数出发,介绍了fd_set结构的问题以及poll函数的引入。接着分析了select和poll在高并发场景下的性能瓶颈,最后详细讲解了epoll、kqueue和/dev/poll等高级API如何解决这些问题。同时,文章还解释了边缘触发和条件触发的概念,并提供了实际应用中遇到的常见问题和解决方案。

407

被折叠的 条评论
为什么被折叠?



