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

cpp 复制代码
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的思想处理其实是一种轮询的方式。、,这导致你每次都要设置文件描述符。

下面是一段示例代码。

cpp 复制代码
#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, 等待文件描述符就绪;

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

cpp 复制代码
#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;
}
相关推荐
AlfredZhao16 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
小宇宙Zz2 天前
Maven依赖冲突
java·服务器·maven