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% 的毛病。
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 创建 kernel 中的关注事件表,相当于创建 fd_set。
epoll_ctl 修改这个表,相当与 FD_SET 等操作。
epoll_wait 完全是 select/poll 的升级版,支持的事件完全一致。并且 epoll 同时支持边缘触发和条件触发,一般来讲边缘触发的性能要好一些。
简单的例子:
strut epoll_event ev, *events; int kdpfd = epoll_create(100); // 创建 kernel 中的关注事件表,返回一个 kernel 事件表的句柄 ev.events = EPOLLIN | EPOLLET; // 边缘触发 ev.data.fd = listener; epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev); // 将事件 ev 加入到 kernel 关注的事件表中 for(;;){ nfds = epoll_wait(kdpfd, events, maxevents, -1); // 等待被通知 for(n = 0; n < nfds; n++){ if(events[n].data.fd == listener){ client = accept(listener, (struct sockaddr*)&local, &addrlen); if(client < 0){ peror("accept"); continue; } setnonblocking(client); ev.events = EPOLLIN | EPOLLET; ev.data.fd = client; if(epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0){ fprintf(stderr, "epoll set insertion error: fd = %d, client); return -1; } }else do_use_fd(events[n].data.fd); } }