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, 0);

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

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

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

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

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

        for (fd = 0; 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 == 0) {
                        close(fd);
                        FD_CLR(fd, &readfds);
                        printf("removing client on fd %d\n", fd);
                    }
                    // 客户请求数据到达
                    else {
                        read(fd, &ch, 1);
                        sleep(5);
                        printf("serving client on fd %d\n", fd);
                        ch++;
                        write(fd, &ch, 1);
                    }
                }
            }
        }
    }
}

上面的代码展示了如何使用select来编写多并发服务器的过程。服务器可以让select调用同时检查监听套接字和已连接套接字。一旦select指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个描述符上面有活动发生。
如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用accept创建一个客户的已连接套接字而不用担心阻塞。如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要我们读取处理。如果读操作返回零字节,这表示有一个客户进程已结束,这时我们可以关闭该套接字并把它从描述符集合中删除。

参考资料

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

haozlee

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值