05. 事件循环与非阻塞I/O
# 05. 事件循环与非阻塞I/O
在服务器端网络编程里,处理并发连接有三种方法:多进程(forking)、多线程(multi-threading),还有事件循环(event loops)。多进程是给每个客户端连接创建新的进程来实现并发;多线程则是用线程代替进程。事件循环呢,靠的是轮询(polling)和非阻塞I/O(Nonblocking IO),而且通常在单线程上运行。由于进程和线程开销的问题,现在大多数生产级别的软件在网络编程这块都用事件循环。
咱们服务器事件循环的简化伪代码是这样的:
all_fds = [ ... ]
while True :
active_fds = poll(all_fds)
for each fd in active_fds:
do_something_with(fd)
def do_something_with(fd):
if fd是监听套接字:
add_new_client(fd)
elif fd是客户端连接:
while work_not_done(fd):
do_something_to_client(fd)
def do_something_to_client(fd):
if should_read_from(fd):
data = read_until_EAGAIN(fd)
process_incoming_data(data)
while should_write_to(fd):
write_until_EAGAIN(fd)
if should_close(fd):
destroy_client(fd)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
咱们不是直接对文件描述符(fd)进行读写或接收操作,而是用轮询(poll)操作来告诉我们哪些fd能直接操作还不会阻塞。当对fd进行I/O操作时,这个操作得在非阻塞模式下进行。
在阻塞模式下,如果内核里没数据,read
操作会阻塞调用者;如果写缓冲区满了,write
操作会阻塞;要是内核队列里没有新连接,accept
操作也会阻塞。在非阻塞模式下,这些操作要么不阻塞直接成功,要么失败并返回错误码EAGAIN
,这个错误码的意思就是“还没准备好”。因为EAGAIN
而失败的非阻塞操作,必须在轮询通知其准备就绪后重新尝试。
在事件循环里,轮询(poll)是唯一的阻塞操作,其他所有操作都得是非阻塞的,这样单线程就能处理多个并发连接啦。所有阻塞式的网络I/O API,比如read
、write
和accept
,都有非阻塞模式。像gethostbyname
这种没有非阻塞模式的API,还有磁盘I/O操作,应该在线程池(thread pools)里执行,这部分内容后面的章节会讲到。另外,定时器(timers)也必须在事件循环里实现,因为在事件循环里可不能靠sleep
等待。
把fd设置为非阻塞模式的系统调用是fcntl
:
static void fd_set_nb(int fd) {
errno = 0;
int flags = fcntl(fd, F_GETFL, 0);
if (errno) {
die("fcntl error");
return ;
}
flags | = O_NONBLOCK;
errno = 0;
(void)fcntl(fd, F_SETFL, flags);
if (errno) {
die("fcntl error");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在Linux系统里,除了poll
系统调用,还有select
和epoll
。老古董select
系统调用和poll
基本差不多,就是最大文件描述符数量被限制得很小,不适用于有大量客户端连接的应用。epoll
API由三个系统调用组成:epoll_create
、epoll_wait
和epoll_ctl
。epoll
API是有状态的,它不是把fd集合作为系统调用的参数,而是用epoll_ctl
来操作由epoll_create
创建的fd集合,epoll_wait
就是对这个集合进行操作。
下一章我们会用poll
系统调用,因为它的代码量比有状态的epoll
API稍微少点。不过在实际项目里,epoll
API更受欢迎,因为随着fd数量增加,poll
的参数会变得特别大。