~/blog/IO多路复用

IO多路复用机制

发布日期
16497分钟

问题引入

如果一个服务器需要同时监听处理多个连接请求,你需要如何设计?

1. 多进程

当我们听到多个请求,同时处理这些字段时很容易想到使用多线程或者多进程来进行监听,当请求数量比较少时,看起来还算OK,但是当出现大量连接时,会带来一些问题:

  1. 线程资源是有限的,这就导致了我们的服务器连接数也是有限的,过多的请求导致没有足够的线程资源进行处理。
  2. 当线程读取未准备完成的数据时,会被阻塞,浪费线程资源同时也会浪费连接资源。
  3. 线程之间的切换也会带来一定的开销,降低服务器性能甚至导致宕机。

2. 单线程

更加上面的分析我们发现,每个请求设置一个线程是不可取的,那么我们是否可以使用单线程来轮询监听所有的请求,当有请求到来时,我们再创建一个新的线程来处理这个请求呢? 这样既解决了连接数目的问题,也解决了创建大量的监听线程的问题。

上面的这种方式就是Select模型所采用的思想

Select模型

Select的原理就是创建一个轮询线程对监听的Socket进行轮询,当接收到请求时,对这个socket进行处理

// C语言伪代码
while(true) {
    for(fd in fds) { // 不断轮询所有的文件描述符
        if(fd.receivedData()) { // 当接收到数据时,进行处理
            handle(fd);
        }
    }
}

让我们看一下Linux中的Select示例:

int main() {
    fd_set rfds;
    int retval, nfds;

    // 如果用作服务器监听程序,一般将下面代码放入while循环中
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);
    // select会监听一系列文件描述符(rfds),当任意文件描述符接收到数据的,会返回1,并将有数据的文件描述符对应的bitmap进行set
    retval = select(nfds, &rfds, NULL, NULL, NULL);  // 会阻塞当前线程

    if (retval == -1)
        perror("select() error");
    else if (retval) {
        // 当有数据到来时,遍历所有的文件描述符,处理数据
        for (int i = 0; i < nfsd; i++) {
            if (FD_ISSET(i, &rfds)) handle(i, rfsd);
        }
    } else
        printf("No data received.\n");
    return 0;
}

void handle(int i, fd_set fd) {
    // 处理逻辑
}

select函数是Linux系统调用,是Linux系统Select模型的实现,他会监听传入的文件描述符(Linux系统中一切皆文件,每个Socket连接表示为一个文件描述符) 当有数据到来时,会将对应的文件描述符的bitmap进行set,同时返回接收数据的文件描述符个数,当没有数据到来时,会阻塞当前线程,直到有数据到来。 要注意的是,select()只能监听不超过FD_SETSIZE(1024)个文件描述符。

当接收到数据时,会遍历所有的文件描述符,找到接收到文件的描述符,然后处理数据。

Select看似很美好,我们可以使用一个线程来监听所有的请求,但是同样还算不算完美:

  1. select()只能监听不超过FD_SETSIZE(1024)个文件描述符,限制了服务器的连接数目。
  2. 当我们用作服务器监听程序时,在while循环调用select()会有用户态和内核态的切换产生开销。
  3. select()会遍历所有的文件描述符,找到接收到文件的描述符,然后处理数据,这样会带来一定的开销。

Poll模型