Linux多路转接

IO模型

在还在学习语言的阶段,C++里使用cin,或者是C使用scanf的时候,总是要等着我们输入数据才执行,这种IO是阻塞IO。下面是比较正式的说法。
阻塞IO: 在内核将数据准备好之前,系统调用会一直等待数据的获取数据的做法,就是阻塞IO。

所以网络套接字,在你没有自行设置的情况下,用的也是阻塞方式IO。

上面说的仿佛只有读取一种情况,那么写呢?
实际上写也是有一样的问题,内核有缓冲区的空间才写,没有缓冲区空间就一样得阻塞。

非阻塞IO:顾名思义,如果需求的数据,内核还没卓备好,那么操作系统就会直接返回。

在C语言里,如果你使用C接口的非阻塞IO,如果没收到数据系统调用就返回,那么 宏变量 errno就会被设置,其值是EWOULDBLOCK 错误码。
C在C11标准之后,C++11标准之后,都是支持线程安全的。

那么问题来了,究竟什么是IO呢?
等待数据 + 拷贝数据
我们发现不论是网络的套接字,亦或者是我们常用的自己的输入输出,其实无非都在等待一些数据,把这些数据拷贝进我们的内存交由程序处理。

多路转接

理解多路转接之前,我们先思考一个问题,
IO=等待+拷贝。

那我们该什么时候区拷贝呢?
一些比较经典的操作就是,轮询,信号。
所谓轮询就是,每当我需要数据,我就问问你,数据好了没,没有我就稍等一会再来接着问,知道数据好了我取走。
所谓信号就是,当你数据好了你来通知我,让我来取走数据。

我们知道,一个主机可以和其他多个主机建立TCP链接,也就是需要使用多个套接字。

那么每当有一个新的连接来临,我们不想中断我主线程的业务,但是新连接的数据收发也要管理。该如何呢? 其中一般想到的是,开多个线程。
开多线程固然是一种解决方案,其对于一般服务器负载也没问题。

那么有成千上万的连接来临呢?要知道创建新线程也是有开销的,根据我的Linux下的POSIX线程库正常创建线程(不重新设置栈大小等),那么每一个线程约需要10MiB的空间。
算下来4GiB的内存,用户一般有3GiB,那么就是说约莫只有300个线程的情况,显然算不上高并发。

因此就有一种IO模型,其处于非阻塞IO,你的每一个文件描述符(windows下叫文件句柄),都有一个中间者来给你管理,当这些句柄有数据来临时,他来通知你,告诉你改处理这些数据了。而这就是多路转接。

下面介绍Linux多路转接常用的函数,select,poll,和epoll

select 和 poll


int select (int __nfds, fd_set *__restrict __readfds,
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout);
		void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

fd_set # 文件描述符集的类型

注:select和poll在2.6版本之后用的较少,因为后来的机器内存都相对较大,同时主要因为有了epoll的出现,使之取代了select。

select维护了一个文件描述符集,FDS,其类型如上面的 fd_set,你可以用一些列函数接口来操作。当你有一个文件描述符是5号文件描述符,那么你就可以调用 FD_SET取设置入你的fd_set的数据类型里面。然后最后交给select帮你管理。

虽然答题过程如上输代码一言,但是其实际使用并不方便。原因也十分简单,select的思想处理其实是一种轮询的方式。、,这导致你每次都要设置文件描述符。
下面是一段示例代码。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define MAX_FD 10  // 最大文件描述符数量
#define BUFFER_SIZE 1024  // 读取缓冲区大小

int main() {
    int fd[MAX_FD];  // 存储文件描述符的数组
    fd_set read_fds;  // select的可读文件描述符集合
    int max_fd = 0;  // 当前最大的文件描述符
    char buffer[BUFFER_SIZE];  // 读取数据的缓冲区
    int i, ret;

    // 初始化文件描述符,这里只是示例,实际情况可能是套接字
    for (i = 0; i < MAX_FD; i++) {
        fd[i] = -1;  // 初始化为-1,表示未使用
    }

    // 假设我们监听标准输入(文件描述符0)
    fd[0] = 0;
    max_fd = 0;  // 标准输入的文件描述符是0

    while (1) {
        // 清空fd集合
        FD_ZERO(&read_fds);

        // 将需要监听的文件描述符加入到fd集合
        for (i = 0; i <= max_fd; i++) {
            if (fd[i] != -1) {
                FD_SET(fd[i], &read_fds);
            }
        }

        // 设置超时时间,这里设置为永远等待
        struct timeval timeout;
        timeout.tv_sec = 10;  // 10秒
        timeout.tv_usec = 0;  // 0微秒

        // 调用select
        ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
        if (ret == -1) {
            perror("select error");
            exit(EXIT_FAILURE);
        } else if (ret == 0) {
            printf("select timeout\n");
            continue;
        }

        // 检查哪个文件描述符可读
        for (i = 0; i <= max_fd; i++) {
            if (fd[i] != -1 && FD_ISSET(fd[i], &read_fds)) {
                // 这里处理文件描述符i的数据
                memset(buffer, 0, BUFFER_SIZE);
                ssize_t count = read(fd[i], buffer, BUFFER_SIZE - 1);
                if (count > 0) {
                    printf("Read from fd %d: %s\n", fd[i], buffer);
                } else if (count == 0) {
                    // EOF,可能需要关闭文件描述符
                    close(fd[i]);
                    fd[i] = -1;
                } else {
                    // 读取错误
                    perror("read error");
                }
            }
        }
    }

    return 0;
}

你会发现意见很让人觉得效率低且麻烦的事情,那就是select每一次都需要遍历。如同轮询一般,因为你放进去的select文件描述符发生事件时,select并不会告诉你具体是谁发生了,只知道在 FD_MAX(目前最大的文件描述符为止),有事件发生,这就显得麻烦且效率低下。不过因为select上限文件描述符大多数都是1024,也就是 FD_SETSIZE 宏。所以select的整体效率不算高,但是其适用于一些比较没有那么支持性能的机器。
总结:
1.每次调用select都需要把fd集合从用户态往内核态拷贝一次,而每次拷贝都需要通过系统调用进入内核态,且在内核也是遍历访问这个开销在fd很多时会很大
2.select支持的文件描述符数量太小了,默认是1024
3.select返回后,需要遍历文件描述符集合,来获取已经就绪的socket
4.select不支持O_NONBLOCK
5.每次对要用第三方数组,动不动就需要遍历,十分耗时

poll类似select,解决了文件描述符上限,同时解决了输入输出每次重置的问题。(也就是select每次都要传一个表进去,同时也要传出来。)
具体用法不多叙述,可以自行百度。

epoll

epoll整体设计理念相较于select就比较人性化,我们知道每当有数据来临的时候,目前许多OS采用的都是硬件中断,让CPU临时去被数据接受之后存储起来。比如你的键盘输入就是如此。
那么为什么不把每个文件描述符有数据需要处理时,都会有信号,那么既然如此我维护这份记录就行,因此epoll就是如此做的。每当一个进程调用epoll时,会创建一个红黑树,将你关心的文件描述符添加进去,每当有事件来临,他就去红黑树里面找关心了这个事件与否,然后如果发现时关心了的,那么就通过回调(ep_poll_callback )把这个节点给放到 另外的就绪队列上去,如此你就知道这个事件需要用了。

因此总结一下:使得epoll关心文件描述符的方法
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪;

具体调用可查询手册,下面是例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10
#define PORT 8080

int main() {
    int listen_sock, conn_sock, epfd;
    struct sockaddr_in serv_addr;
    struct epoll_event event;
    struct epoll_event events[MAX_EVENTS];
    int num_fds;

    // 创建监听socket
    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);

    // 绑定socket
    if (bind(listen_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 监听socket
    if (listen(listen_sock, 5) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 添加监听socket到epoll实例
    event.data.fd = listen_sock;
    event.events = EPOLLIN;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &event) == -1) {
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    }

    // 事件循环
    while (1) {
        num_fds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (num_fds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < num_fds; i++) {
            if (events[i].data.fd == listen_sock) {
                // 处理新的连接
                conn_sock = accept(listen_sock, NULL, NULL);
                if (conn_sock == -1) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                printf("Accepted connection on fd %d\n", conn_sock);

                // 将新的连接添加到epoll实例
                event.data.fd = conn_sock;
                event.events = EPOLLIN | EPOLLET; // 边缘触发模式
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &event) == -1) {
                    perror("epoll_ctl: conn_sock");
                    exit(EXIT_FAILURE);
                }
            } else {
                // 处理已连接socket的数据
                if (events[i].events & EPOLLIN) {
                    char buffer[1024];
                    ssize_t count;

                    count = read(events[i].data.fd, buffer, sizeof(buffer));
                    if (count == -1) {
                        perror("read");
                        close(events[i].data.fd);
                    } else if (count == 0) {
                        // 连接关闭
                        printf("Closed connection on fd %d\n", events[i].data.fd);
                        close(events[i].data.fd);
                    } else {
                        // 处理读取到的数据
                        printf("Read %zd bytes from fd %d\n", count, events[i].data.fd);
                        // 这里可以将数据发送回去或者进行其他处理
                    }
                }
            }
        }
    }

    close(listen_sock);
    return 0;
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值