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)
1
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,比如readwriteaccept,都有非阻塞模式。像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");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在Linux系统里,除了poll系统调用,还有selectepoll。老古董select系统调用和poll基本差不多,就是最大文件描述符数量被限制得很小,不适用于有大量客户端连接的应用。epoll API由三个系统调用组成:epoll_createepoll_waitepoll_ctlepoll API是有状态的,它不是把fd集合作为系统调用的参数,而是用epoll_ctl来操作由epoll_create创建的fd集合,epoll_wait就是对这个集合进行操作。

下一章我们会用poll系统调用,因为它的代码量比有状态的epoll API稍微少点。不过在实际项目里,epoll API更受欢迎,因为随着fd数量增加,poll的参数会变得特别大。

上次更新: 2025/03/25, 00:48:42

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>