天天看点

I/O多路复用之select系统调用

I/O多路复用模型允许我们同时等待多个套接字描述符是否就绪。Linux系统为实现I/O多路复用提供的最常见的一个函数是

select

函数,该函数允许进程指示内核等待多个事件中的任何一个发生,并只有在一个或多个事件发生或经历一段指定的时间后才唤醒它。

作为一个例子,我们可以调用

select

,告知内核仅在下列情况发生时才返回:

  • 当集合{0, 4}中任意描述符准备好读时返回
  • 当集合{1, 2, 7}中任意描述符准备好写时返回
  • 已经历了10.2秒

也就是说,我们调用

select

可以告知内核我们对哪些描述符感兴趣以及等待多久时间。

select

是一个复杂的函数,有许多不同的应用场景,我们将只讨论第一种场景:等待一组描述符准备好读。

#include <unistd.h>
#include <sys/types.h>

int select(int n, fd_set *fdset, NULL, NULL, struct timeval *timeout);

FD_ZERO(fd_set *fdset);          // 将fdset初始为为空集合
FD_CLR(int fd, fd_set *fdset);   // 从fdset清除fd
FD_SET(int fd, fd_set *fdset);   // 将fd添加到fdset
FD_ISSET(int fd, fd_set *fdset); // fd是否存在于fdset 
           

我们来看下

select

函数的参数。参数

n

指定需要测试的描述符的数目,测试的描述符范围从0到n-1。第二个参数

fdset

指定需要测试的可读描述符集合。当

fdset

集合中有描述符可读,或者经历了

timeout

时间时,

select

将返回。当

select

返回时,作为一个副作用,

select

修改了参数

fdset

指向的描述符集合,这时

fdset

变成由读集合中准备好可以读了的描述符组成。

select

函数的返回值则指明了就绪集合的基数。值得注意的是,由于这个副作用,我们必须每次在调用

select

时都更新读集合。

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int listenfd, connfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    fd_set readfds, testfds;

    // 创建套接字
    listenfd = socket(AF_INET, SOCK_STREAM, );

    // 命名套接字
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons();
    server_len = sizeof(server_address);
    bind(listenfd, (struct sockaddr*)&server_address, server_len);

    // 创建套接字队列
    listen(listenfd, );

    FD_ZERO(&readfds);
    FD_SET(listenfd, &readfds);

    // 等待客户请求
    while () {
        char ch;
        int fd;
        int nread;

        // 同时检查监听套接字和已连接套接字
        testfds = readfds;
        printf("server waiting\n");
        int result = select(FD_SETSIZE, &testfds, (fd_set*), (fd_set*), (struct timeval*));
        if (result < ) {
            perror("select error");
            exit();
        }

        for (fd = ; fd < FD_SETSIZE; fd++) {
            // 检查哪个描述符可读
            if (FD_ISSET(fd, &testfds)) {
                // 如果是一个新的客户连接请求
                if (fd == listenfd) {
                    client_len = sizeof(client_address);
                    connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_len);
                    FD_SET(connfd, &readfds);
                    printf("adding client on fd %d\n", connfd);
                }
                // 如果是旧的客户活动
                else {
                    ioctl(fd, FIONREAD, &nread);
                    // 如果客户断开连接
                    if (nread == ) {
                        close(fd);
                        FD_CLR(fd, &readfds);
                        printf("removing client on fd %d\n", fd);
                    }
                    // 客户请求数据到达
                    else {
                        read(fd, &ch, );
                        sleep();
                        printf("serving client on fd %d\n", fd);
                        ch++;
                        write(fd, &ch, );
                    }
                }
            }
        }
    }
}
           

上面的代码展示了如何使用

select

来编写多并发服务器的过程。服务器可以让

select

调用同时检查监听套接字和已连接套接字。一旦

select

指示有活动发生,就可以用

FD_ISSET

来遍历所有可能的文件描述符,以检查是哪个描述符上面有活动发生。

如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用

accept

创建一个客户的已连接套接字而不用担心阻塞。如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要我们读取处理。如果读操作返回零字节,这表示有一个客户进程已结束,这时我们可以关闭该套接字并把它从描述符集合中删除。

参考资料

  1. 深入理解计算机系统,第2版,机械工业出版社
  2. Linux程序设计(第4版),Neil Matthew等著,人民邮电出版社,2010年
  3. UNIX 网络编程卷1:套接字联网API(第三版), W.Richard Stevens 等著

继续阅读