【C++高并发服务器WebServer】-15:poll、epoll详解及实现

news2025/3/15 4:25:58

在这里插入图片描述

本文目录

  • 一、poll
  • 二、epoll
    • 2.1 相对poll和select的优点
    • 2.2 epoll的api
    • 2.3 epoll的demo实现
    • 2.5 epoll的工作模式

一、poll

poll是对select的一个改进,我们先来看看select的缺点。

在这里插入图片描述
我们来看看poll的实现。

struct pollfd {
	int fd; /* 委托内核检测的文件描述符 */
	short events; /* 委托内核检测文件描述符的什么事件 */
	short revents; /* 文件描述符实际发生的事件 */
};

struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明如下。
fds:是struct pollfd结构体数组,这是一个需要检测的文件描述符集合。

当内核检测到有变动之后,有需要修改的,会直接修改revents,不需要修改events了,相对select来说,就不需要每次重置fds集合。

除此之外,相对于select来说,并没有1024的限制。
nfds是第一个参数数组中最后一个有效元素的下标+1。

timeout,注意这个是int类型的,当为0时代表不阻塞,当为-1时表示阻塞,当检测到需要检测的文件描述符发生了变化,解除阻塞。>0表示阻塞的时长。

poll函数返回值为-1时表示失败,>0会返回n,表示检测到集合中有n个描述符发生了变化。

在这里插入图片描述

poll的服务端实现代码如下。

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;

    while(1) {

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
                    if(fds[i].fd == -1) {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            for(int i = 1; i <= nfds; i++) {
                if(fds[i].revents & POLLIN) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}


对应的客户端代码我们继续沿用之前的即可。

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        usleep(1000);
    }

    close(fd);

    return 0;
}

二、epoll

首先调用epoll_create实现一个epoll的实例,这个epoll实例是在内核区,是结构体类型,可以理解成一块数据。返回值是一个文件描述符,那我们就可以通过这个文件描述符来操作这块内核当中的epoll数据(通过epoll提供的一些api来进行操作)。

eventpoll中有两个最关键的数据,就是rbrrdlist,也就是红黑树双向就绪链表

rbr记录需要检测的文件描述符。(之前需要把一些表从用户态拷贝到内核态,现在是直接在内核态,效率高了很多。另外现在是红黑树,之前是链表,红黑树的遍历效率也高很多。)

rdlist是检测文件描述符当中哪些是有数据发生改变的。

在函数epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev)中最后一个参数evstruct epoll_event类型,需要设置eventsev.data.fd

调用epoll_wait之后内核就会去检测rbr里面的文件描述符是否有对应的数据改变。如果有改变的(就绪的),就会把文件描述符的信息放到rdlist中,然后会把这个list拷贝到用户区,这样用户区直接遍历这几个fd,就可以进行对应的读写操作了。

在这里插入图片描述

2.1 相对poll和select的优点

时间复杂度方面,每次调用 select 或 poll 时,内核需要遍历所有被监控的文件描述符,检查它们的状态。select 和 poll 的时间复杂度是 O(n)。当文件描述符数量很大时(例如成千上万个),这种线性扫描的效率会非常低。

epoll 的时间复杂度是 O(1),epoll 使用红黑树和双向链表来管理文件描述符。当文件描述符的状态发生变化时,内核会将其加入到就绪链表中,用户程序只需要检查就绪链表即可,而不需要遍历所有文件描述符。

文件描述符数量限制方面,默认情况下,select 只能监控最多 1024 个文件描述符(由 FD_SETSIZE`定义)。如果需要监控更多的文件描述符,需要修改内核参数并重新编译程序。poll 使用数组来存储文件描述符,理论上可以监控任意数量的文件描述符。但当文件描述符数量很大时,遍历整个数组的效率会非常低。

epoll 可以轻松支持数万个甚至更多的文件描述符。它使用红黑树来存储文件描述符,查找和插入的效率很高。

用户态和内核态的数据拷贝方面,每次调用 select 或 poll 时,都需要将文件描述符集合从用户态拷贝到内核态:当文件描述符数量很大时,这种拷贝操作会带来较大的开销。

对于epoll,文件描述符只需要通过 epoll_ctl 添加到内核事件表中一次,后续不需要重复拷贝。当文件描述符状态变化时,内核会直接将事件放入就绪链表中,用户程序通过 epoll_wait 获取就绪事件。

事件触发模式方面,select 和 poll 只支持水平触发(Level-Triggered,LT)模式:如果文件描述符的状态满足条件(例如有数据可读),select 和 poll 会一直通知用户程序,直到状态发生变化。

在这里插入图片描述

epoll 支持水平触发(LT)和边缘触发(Edge-Triggered,ET)模式:
水平触发(LT):与 select 和 poll 的行为相同,只要文件描述符的状态满足条件,就会一直通知用户程序。边缘触发(ET):只有当文件描述符的状态发生变化时,才会通知用户程序。这种模式可以减少重复通知的次数,提高效率。但是需要用户程序一次性处理完所有数据,否则可能会丢失数据。

内核实现机制方面,select 和 poll 是基于轮询的机制:每次调用时,内核需要遍历所有文件描述符,检查它们的状态。这种机制在大规模并发场景下效率较低。

epoll 是基于事件回调的机制:内核会为每个文件描述符注册回调函数,当文件描述符的状态发生变化时,内核会调用回调函数将其加入到就绪链表中。这种机制避免了不必要的遍历,效率更高。

2.2 epoll的api

头文件如下。

#include <sys/epoll.h>

/创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。【从linux内核2.6.8开始,size这个参数已经被忽略了,但是必须大于0。】

int epoll_create(int size);
- 参数:
	size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:
	-1 : 失败
	> 0 : 文件描述符,操作epoll实例的

对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:
	- epfd : epoll实例对应的文件描述符
	- op : 要进行什么操作
		EPOLL_CTL_ADD: 添加
		EPOLL_CTL_MOD: 修改(比如从读事件改成写事件)
		EPOLL_CTL_DEL: 删除
	- fd : 要检测的文件描述符
	- event : 检测文件描述符什么事情

epoll_event是检测事件的结构体,定义如下。

struct epoll_event {
	uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
	- EPOLLIN 
	- EPOLLOUT 
	- EPOLLERR
	- EPOLLET (设置边沿触发)

在其中,又有一个联合体epoll_data_t,定义如下。通过联合体,用户可以选择存储不同类型的数据,如指针、文件描述符、32 位或 64 位整数。

ptr是一个指向任意类型的指针。用户可以将与事件相关的任意数据存储在这个指针中,例如指向某个结构体的指针。这种方式非常灵活,可以存储用户自定义的数据结构。
fd,这是 epoll 最常用的用途之一,直接存储与事件相关的文件描述符。
u32是一个 32 位的无符号整数。用户可以存储一些简单的整数值作为用户数据。u64同理。

联合体 epoll_data_t 的设计允许用户根据需要选择存储不同类型的数据。联合体的特性是所有成员共享同一块内存,因此在任何时刻,联合体中只有一个成员是有效的。用户可以根据实际需求选择存储哪种类型的数据。

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

检测函数如下。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, inttimeout);
	- 参数:
	- epfd : epoll实例对应的文件描述符
	- events : 传出参数,保存了发送了变化的文件描述符的信息
	- maxevents : 第二个参数结构体数组的大小
	- timeout : 阻塞时间
		- 0 : 不阻塞
		- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
-		 > 0 : 阻塞的时长(毫秒)
	- 返回值:
	- 成功,返回发送变化的文件描述符的个数 > 0
	- 失败 -1

这里有个问题是,在使用 epoll 时,epoll_ctl 函数确实已经将文件描述符(fd)注册到了 epoll 实例中,但 epoll_event 结构体中的 data.fd 仍然需要存储文件描述符的原因主要有以下几点:

首先epoll_ctl是用于将文件描述符注册到 epoll 实例中,并设置相关的事件类型(如 EPOLLIN、EPOLLOUT 等)。它的作用是告诉 epoll 哪些文件描述符需要被监控,以及监控哪些类型的事件。

epoll_event 中 用于在 epoll_wait 调用时返回检测到的事件。
它的作用是告诉用户哪些文件描述符发生了事件,以及发生了哪些类型的事件。epoll_event 中的 data.fd 是为了方便用户在 epoll_wait 返回后,能够直接获取到发生事件的文件描述符。

epoll_wait 返回时,它会返回一个 epoll_event 数组,每个 epoll_event 表示一个发生事件的文件描述符及其事件类型。通过在 epoll_event 中存储 fd,用户可以直接从 epoll_event 中获取到发生事件的文件描述符,而无需额外查找。

2.3 epoll的demo实现

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN | EPOLLOUT; //监听的事件比较多,所以每一种事件在下方都需要进行对应的处理。
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

2.5 epoll的工作模式

epoll 有两种工作模式:LT(水平触发)模式和 ET(边沿触发)模式。在 LT 模式中,当内核检测到文件描述符(fd)的读缓冲区中有数据时,会通知用户。如果用户没有读取数据,数据会一直保留在缓冲区中,epoll 会持续通知用户。即使用户只读取了一部分数据,epoll 也会继续通知,直到缓冲区的数据被完全读走。LT 模式同时支持阻塞(block)和非阻塞(non-block)的 socket,它是一种缺省的工作方式,内核会持续告知用户文件描述符是否就绪,并允许用户对这个就绪的 fd 进行 I/O 操作。如果用户不进行任何操作,内核会继续发送通知。

相比之下,ET 模式是一种高速工作方式,仅支持非阻塞 socket。在这种模式下,内核仅在文件描述符从未就绪变为就绪时通过 epoll 通知用户一次。一旦通知,内核会假定用户知道文件描述符已经就绪,并且不会再为该文件描述符发送更多的就绪通知,除非用户执行了某些操作导致文件描述符不再处于就绪状态。在 ET 模式中,如果用户不对 fd 执行 I/O 操作,从而使得它再次变为未就绪状态,内核不会再次发送通知。这种模式显著减少了 epoll 事件被重复触发的次数,因此比 LT 模式更高效。在 ET 模式下工作时,必须使用非阻塞套接字,以避免由于单个文件句柄的阻塞读/写操作导致处理多个文件描述符的任务饿死。

需要特别注意的是,ET模式中,如果用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不会再通知了。

如果使用了ET模式,那么在监听到有客户端连接之后,对cfd的属性需要设置非阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2295981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git提交到GitHub问题汇总

1.main->master git默认主分支是maser&#xff0c;如果是按照这个分支名push&#xff0c;GitHub会出现两个branch&#xff0c;与预期不符 解决方案&#xff1a;更改原始主分支名为main git config --global init.defaultBranch main2.git&#xff1a;OpenSSL SSL_read: SS…

CNN-GRU卷积神经网络门控循环单元多变量多步预测,光伏功率预测(Matlab完整源码和数据)

代码地址&#xff1a;CNN-GRU卷积神经网络门控循环单元多变量多步预测&#xff0c;光伏功率预测&#xff08;Matlab完整源码和数据) CNN-GRU卷积神经网络门控循环单元多变量多步预测&#xff0c;光伏功率预测 一、引言 1.1、研究背景和意义 随着全球能源危机和环境问题的日…

编译原理面试问答

编译原理面试拷打 1.编译原理的基本概念 编译原理是研究如何将高级程序语言转换为计算机可执行代码的理论与技术&#xff0c;其核心目标是实现高效、正确的代码翻译。 **编译器&#xff1a;**将源代码转化为目标代码&#xff08;机器码、字节码等&#xff09;。一次翻译整个程…

LIMO:上海交大的工作 “少即是多” LLM 推理

25年2月来自上海交大、SII 和 GAIR 的论文“LIMO: Less is More for Reasoning”。 一个挑战是在大语言模型&#xff08;LLM&#xff09;中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据&#xff08;通常超过 100,000 个示例&#xff09;&#xff0c;但本文展…

Ollama 部署本地大语言模型

一、下载安装ollama 1.百度 ollama Ollama 2.点击下载 可以复制下载链接&#xff0c;使用下载器下载。 3.双击安装 默认安装目录&#xff1a;C:\Users\用户名\AppData\Local\Programs\Ollama 二、更改模型下载目录 0.默认下载目录 (跳过) 之前没下载过模型&#xff0c;不…

pytest-xdist 进行多进程并发测试!

在软件开发过程中&#xff0c;测试是确保代码质量和可靠性的关键步骤。随着项目规模的扩大和复杂性的增加&#xff0c;测试用例的执行效率变得尤为重要。为了加速测试过程&#xff0c;特别是对于一些可以并行执行的测试用 例&#xff0c;pytest-xdist 提供了一种强大的工具&…

24.ppt:小李-图书策划方案【1】

目录 NO1234​ NO5678​ NO1234 新建PPT两种方式&#x1f447;docx中视图→导航窗格→标题1/2/3ppt新建幻灯片→从大纲→重置开始→版式设计→主题插入→表格 NO5678 SmartArt演示方案&#xff1a;幻灯片放映→自定义幻灯片放映→新建→选中添加

模型 替身决策

系列文章分享模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。替身决策&#xff0c;换位思考&#xff0c;多角度决策。 1 替身决策模型的应用 1.1 替身决策模型在面试中的应用-小李的求职面试 小李是一名应届毕业生&#xff0c;正在积极寻找工作机会。在面试过程中…

ESP32S3读取数字麦克风INMP441的音频数据

ESP32S3 与 INMP441 麦克风模块的集成通常涉及使用 I2S 接口进行数字音频数据的传输。INMP441 是一款高性能的数字麦克风&#xff0c;它通过 I2S 接口输出音频数据。在 Arduino 环境中&#xff0c;ESP32S3 的开发通常使用 ESP-IDF&#xff08;Espressif IoT Development Framew…

docker环境下部署face-search开源人脸识别模型

由于我们是直接将face-search部署在docker容器中的,所以,在部署之前一定要检查一下自己的docker环境,要不然部署过程中会出现各种各样的问题 我这里的docker环境是 一、安装docker环境 如果docker版本比较低或者docker-compose的版本比较低的情况下,部署的时候docker的yml…

飞牛fnOS安装了Airplay没有声音找不到声卡的问题

主要问题描述&#xff1a;我在飞牛的Docker里安装了 Airplay&#xff0c; 这样把NAS接一个外接音箱&#xff0c;就可以当成无线音箱来用&#xff0c;直接把手机的音乐播放投到上面来播放。 &#xff08;文章底部有写我是怎么安装Airplay的&#xff09; 我的报错如下&#xff1…

netcore openTelemetry+prometheus+grafana

一、netcore项目 二、openTelemetry 三、prometheus 四、grafana添加Dashborad aspire/src/Grafana/dashboards at main dotnet/aspire GitHub 导入&#xff1a;aspnetcore.json和aspnetcore-endpoint.json 效果&#xff1a;

全程Kali linux---CTFshow misc入门(38-50)

第三十八题&#xff1a; ctfshow{48b722b570c603ef58cc0b83bbf7680d} 第三十九题&#xff1a; 37换成1&#xff0c;36换成0&#xff0c;就得到长度为287的二进制字符串&#xff0c;因为不能被8整除所以&#xff0c;考虑每7位转换一个字符&#xff0c;得到flag。 ctfshow{5281…

用 DeepSeek + Kimi 自动做 PPT,效率起飞

以下是使用 DeepSeek Kimi 自动做 PPT 的详细操作步骤&#xff1a; 利用 DeepSeek 生成 PPT 内容&#xff1a; 访问 DeepSeek 官网&#xff0c;完成注册/登录后进入对话界面。输入指令&#xff0c;例如“请用 Markdown 格式生成一份关于[具体主题]的 PPT 大纲&#xff0c;需包…

自动驾驶数据集三剑客:nuScenes、nuImages 与 nuPlan 的技术矩阵与生态协同

目录 1、引言 2、主要内容 2.1、定位对比&#xff1a;感知与规划的全维覆盖 2.2、数据与技术特性对比 2.3、技术协同&#xff1a;构建全栈研发生态 2.4、应用场景与评估体系 2.5、总结与展望 3、参考文献 1、引言 随着自动驾驶技术向全栈化迈进&#xff0c;Motional 团…

DeepSeekMoE 论文解读:混合专家架构的效能革新者

论文链接&#xff1a;DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models 目录 一、引言二、背景知识&#xff08;一&#xff09;MoE架构概述&#xff08;二&#xff09;现有MoE架构的问题 三、DeepSeekMoE架构详解&#xff08;一&a…

【python】简单的flask做页面。一组字母组成的所有单词。这里的输入是一组字母,而输出是所有可能得字母组成的单词列表

目录结构如下&#xff1a; https://github.com/kaede316/Pythons_pj.git 效果&#xff1a; 后续可扩展为工具网站&#xff1a; 更新 2025.02.09 1、增加等间距制作人 时间信息 2、增加判断润年的功能

[权限提升] Linux 提权 维持 — 系统错误配置提权 - Sudo 滥用提权

关注这个专栏的其他相关笔记&#xff1a;[内网安全] 内网渗透 - 学习手册-CSDN博客 0x01&#xff1a;Sudo 滥用提权原理 Sudo 是一个 Linux 系统管理命令&#xff0c;它允许系统管理员授予普通用户以指定身份执行指定命令的权限。该命令不仅减少了 Root 用户的登录时间和管理时…

如何修改IDEA的maven远程仓库地址

IDEA自动的maven的远程仓库地址为国外地址&#xff0c;导致下载依赖时很慢&#xff0c;通过如下方法可以将其修改为国内地址 选中模块&#xff0c;右击&#xff0c;创建setting.xml文件 添加阿里仓库地址 <mirrors><mirror><id>nexus-aliyun</id><…

LLMs之DeepSeek r1:TinyZero(复现 DeepSeek R1 Zero 的核心功能)的简介、安装和使用方法、案例应用之详细攻略

LLMs之DeepSeek r1&#xff1a;TinyZero(复现 DeepSeek R1 Zero 的核心功能)的简介、安装和使用方法、案例应用之详细攻略 目录 TinyZero的简介 1、TinyZero的特点 TinyZero的安装和使用方法 1、安装 创建 conda 环境 数据准备 (倒计时任务) 训练执行 单GPU (适用于模型…