news 2026/6/26 19:02:27

Linux I/O多路复用实战:从select到epoll的高并发服务器编程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux I/O多路复用实战:从select到epoll的高并发服务器编程

1. 项目概述:从“头歌”到Linux I/O多路复用的实战之路

最近在“头歌”平台上折腾Linux网络编程的作业,核心就是I/O多路复用。这玩意儿听起来高大上,什么epoll、select、poll,一堆名词,但说白了,它就是服务器端用来高效管理成千上万个网络连接的“调度中心”。想象一下,你开了一家网红餐厅(服务器),只有一个服务员(单线程)。如果来一个客人就派一个服务员全程盯着(阻塞I/O),那店里早就挤爆了。I/O多路复用就是这个聪明的服务员,他站在大厅,耳朵上挂个对讲机,哪个桌的菜好了(数据可读)、哪个桌要结账(连接关闭)、哪个新客人来了(新连接),对讲机里一喊,他马上过去处理一下,然后又回到大厅待命。这样,一个服务员就能照看好整个餐厅。这次在“头歌”的实践,就是把这个“聪明服务员”的调度机制,从理论到代码,彻底搞明白、跑起来。无论你是正在学习操作系统、网络编程的学生,还是想优化后端服务性能的开发者,掌握这套“以一当千”的并发处理模型,都是至关重要的基本功。

2. I/O多路复用核心原理与方案选型

2.1 为什么需要I/O多路复用?

在传统的阻塞I/O模型里,一个进程或线程处理一个连接。当调用readaccept时,如果数据没准备好或者没有新连接,调用就会一直卡在那里,线程也被挂起,什么也干不了。这对于需要同时服务大量客户端的服务器来说是灾难性的。为每个连接创建一个线程呢?这就是多线程/多进程模型。它确实解决了同时处理多个连接的问题,但代价巨大:每个线程都有自己的栈空间,内存开销大;线程间的上下文切换由操作系统内核完成,当线程数量暴涨到几千上万时,CPU时间会大量浪费在切换上,而不是处理实际业务。这就好比餐厅雇了1000个服务员,但大部分时间都在排队等对讲机分配任务和互相让路,真正端菜的时间反而少了。

I/O多路复用就是为了解决这个矛盾而生的。它的核心思想是:用一个进程(或线程)来监视多个文件描述符(在网络编程中主要是socket),一旦某个描述符就绪(可读、可写或出现异常),就通知程序进行相应的读写操作。这样,在连接数很多但活动连接比例不高的场景下(例如长连接、即时通讯、游戏服务器),单线程就能hold住全场,避免了多线程的内存和切换开销。网络热词里提到的“单线程处理海量TCP连接,无多线程上下文切换开销”,指的就是这种模式的理想效果。

2.2 三大神器:select、poll、epoll的深度对比

Linux提供了三种主要的I/O多路复用机制:select、poll和epoll。它们的目标一致,但实现和性能天差地别。选择哪一个,直接决定了你服务器性能的上限。

select:元老级,但已显疲态select是最早出现的接口,它通过一个fd_set(文件描述符集合)来告诉内核:“帮我监视这一堆socket”。它的工作流程是:

  1. 程序将需要监视的读、写、异常事件对应的socket描述符,分别设置到三个fd_set中。
  2. 调用select函数,将这三个集合拷贝到内核。
  3. 内核遍历所有传入的描述符,检查它们的状态。
  4. 当有事件发生或超时后,select返回,并修改那三个fd_set,只保留就绪的描述符。
  5. 程序必须遍历所有原先关注的描述符,用FD_ISSET宏判断哪些在返回的集合里,然后进行处理。

它的致命缺点非常明显:

  • 描述符数量限制fd_set的大小通常定义为1024(FD_SETSIZE),这意味着一个进程最多只能监视1024个连接。这在当今动辄数万并发的场景下完全不够用。
  • 性能线性下降:每次调用select,都需要把庞大的fd_set集合在用户态和内核态之间来回拷贝。同时,内核和应用程序在返回后都需要遍历整个集合来找出就绪项,这是一个O(n)的复杂度。当连接数很大时,这个开销无法忽视。
  • fd_set被内核修改:每次调用后,传入的fd_set都会被内核修改,因此下次调用前必须重新设置,这给编程带来了不便。

poll:小幅改进,本质未变poll使用一个pollfd结构数组来代替selectfd_set,解决了描述符数量限制的问题(理论上只受系统打开文件数的限制)。但是,它依然需要将整个数组拷贝到内核,内核和应用程序在返回后也需要遍历整个数组来查找就绪描述符。所以,它避免了1024的限制,但性能随着监控描述符数量的增长而线性下降的问题依然存在。

epoll:Linux的终极武器epoll是Linux 2.6内核引入的,专门为处理大量并发连接而设计,彻底解决了select/poll的性能瓶颈。它的核心改进在于:

  1. 内核与用户共享存储:通过epoll_create创建一个epoll实例(一个内核数据结构),后续通过epoll_ctl来增删改要监控的事件。这个操作只涉及一次系统调用,且事件信息保存在内核中,不需要每次调用都重复拷贝
  2. 事件驱动,无需遍历:当有事件发生时,内核通过一种高效的方式(如就绪链表)将发生事件的描述符记录下来。程序调用epoll_wait时,内核只将已经就绪的事件拷贝到用户空间提供的数组中。这样,应用程序只需要遍历这个很小的、全是有效事件的数组即可,复杂度是O(1)(与连接总数无关,只与活跃连接数有关)。

这就像餐厅服务员(应用程序)不用再每天背一遍所有桌号列表(遍历所有fd)去问厨房(内核),而是厨房装了一个智能显示屏(就绪列表),哪桌的菜好了,屏幕就自动亮起那一桌的号码,服务员直接看屏幕去端菜就行了。

注意:网络热词中提到的“pipe connection has been broken”这类错误,在使用这些I/O多路复用函数时很常见。它通常意味着对端关闭了连接,而本端尝试进行读写操作。在epoll_wait返回的事件中,如果事件包含EPOLLHUP(挂起)或EPOLLERR(错误),就应该关闭对应的socket并清理资源,否则后续操作就会触发这类I/O错误。

2.3 方案选型背后的逻辑

理解了原理,选型就很简单了:

  • 追求跨平台:如果你的程序需要在Windows、macOS等多个系统上运行,select是唯一广泛支持的原始选项(尽管性能差)。现代跨平台库(如libevent, libuv)在底层封装了各系统的最优实现。
  • 连接数少且固定:如果并发连接数很少(比如几十个),selectpoll的简单性可能更有优势,代码直观。
  • Linux平台高性能服务器毫无悬念选择epoll。它是构建高性能、高并发网络服务(如Nginx、Redis、Memcached)的基石。这也是“头歌”这类实践平台和面试中重点考察的内容。

3. epoll的三种工作模式详解与实战编码

3.1 epoll的工作模式:LT与ET的本质区别

epoll有两种事件触发模式,这是理解其高性能和编程复杂性的关键。

水平触发(Level-Triggered, LT)这是默认模式。只要文件描述符对应的读/写缓冲区非空/非满epoll_wait就会持续报告该事件。

  • 类比:厨房的智能显示屏(LT模式),只要某桌的菜还在出菜口没被端走,屏幕就一直亮着提醒服务员。
  • 编程影响:在LT模式下,当epoll_wait通知你某个socket可读后,你可以不一次性把缓冲区所有数据读完。下次调用epoll_wait时,如果缓冲区里还有数据,它会再次通知你。这给了程序更大的灵活性,可以分多次处理数据,编程也更简单,不容易遗漏事件。但如果不及时处理,会导致频繁的无用通知。

边缘触发(Edge-Triggered, ET)只有当文件描述符状态发生变化时(比如从不可读变为可读,从不可写变为可写),epoll_wait才会报告一次事件。

  • 类比:厨房的智能显示屏(ET模式),只在菜刚做好放到出菜口的那一瞬间闪一下,之后哪怕菜一直没被端走,屏幕也不再亮了。
  • 编程影响:ET模式是高性能的代名词,因为它减少了重复通知的次数。但编程难度陡增:当收到一个可读事件时,你必须循环调用read,直到把socket内核缓冲区里的数据全部读完read返回EAGAINEWOULDBLOCK错误)。否则,如果只读了一次,剩下的数据还在缓冲区,但由于状态没有再次“变化”,epoll_wait将永远不会再通知你,这些数据就会永远滞留,导致逻辑错误。ET模式通常需要将socket设置为非阻塞模式(O_NONBLOCK)配合使用。

ET模式下的一个经典坑假设客户端发送了10KB数据,触发了ET可读事件。你的处理函数只读了4KB就返回了。那么剩下的6KB数据会一直留在内核接收缓冲区。只要客户端不再发送新数据(不触发新的“变化”),你的服务器就再也读不到这6KB数据了,连接看似正常,但业务逻辑已经卡死。

3.2 从零构建一个epoll服务器:代码实战

下面我们用一个简单的回显服务器(Echo Server)来演示LT模式下的epoll用法。这个服务器会把客户端发来的任何数据原样发回去。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #include <errno.h> #define MAX_EVENTS 1024 #define BUFFER_SIZE 4096 int main() { int listen_fd, conn_fd, epoll_fd, nfds; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); struct epoll_event ev, events[MAX_EVENTS]; char buffer[BUFFER_SIZE]; // 1. 创建监听socket listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR,避免“Address already in use”错误,这在快速重启服务器时非常关键 int opt = 1; if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt"); close(listen_fd); exit(EXIT_FAILURE); } // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡 server_addr.sin_port = htons(8080); // 监听8080端口 if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind"); close(listen_fd); exit(EXIT_FAILURE); } // 3. 开始监听 if (listen(listen_fd, SOMAXCONN) == -1) { perror("listen"); close(listen_fd); exit(EXIT_FAILURE); } printf("Echo server listening on port 8080...\n"); // 4. 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); close(listen_fd); exit(EXIT_FAILURE); } // 5. 将监听socket加入epoll监控,关注可读事件(新连接) ev.events = EPOLLIN; // 默认是LT模式 ev.data.fd = listen_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) { perror("epoll_ctl: listen_fd"); close(epoll_fd); close(listen_fd); exit(EXIT_FAILURE); } // 6. 事件循环 while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // -1表示永久阻塞 if (nfds == -1) { perror("epoll_wait"); // 通常被信号中断会返回-1且errno=EINTR,这里可以选择继续循环 if (errno == EINTR) continue; break; // 其他错误则退出 } for (int i = 0; i < nfds; ++i) { // 6.1 处理新连接 if (events[i].data.fd == listen_fd) { conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (conn_fd == -1) { perror("accept"); continue; // 接受一个连接失败,继续处理其他事件 } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 将新连接socket设为非阻塞(为ET模式做准备,LT模式非必须但也是好习惯) // int flags = fcntl(conn_fd, F_GETFL, 0); // fcntl(conn_fd, F_SETFL, flags | O_NONBLOCK); // 将新连接加入epoll监控,关注可读事件 ev.events = EPOLLIN; // LT模式 // 如果要用ET模式:ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) { perror("epoll_ctl: conn_fd"); close(conn_fd); } } // 6.2 处理已连接socket的可读事件(客户端发来数据) else if (events[i].events & EPOLLIN) { int sockfd = events[i].data.fd; ssize_t n = read(sockfd, buffer, BUFFER_SIZE - 1); // 留一个位置给'\0' if (n > 0) { buffer[n] = '\0'; printf("Received from fd %d: %s", sockfd, buffer); // 假设是文本 // 回显数据给客户端 // 这里简单处理,直接写回。实际应考虑写缓冲区满(EPOLLOUT)的情况 write(sockfd, buffer, n); } else if (n == 0) { // 对端关闭连接 printf("Connection closed by client (fd: %d)\n", sockfd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sockfd, NULL); close(sockfd); } else { // 读取出错 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 在非阻塞模式下,数据已读完,可继续 continue; } else { perror("read"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sockfd, NULL); close(sockfd); } } } // 6.3 处理可写事件(本例简略,实际需处理) // else if (events[i].events & EPOLLOUT) { ... } } } // 7. 清理(通常不会执行到这里) close(epoll_fd); close(listen_fd); return 0; }

代码关键点解析:

  1. epoll_create1(0):创建epoll实例。参数可以是0,或者EPOLL_CLOEXEC(表示fork子进程后关闭)。
  2. epoll_ctl:管理监控列表。EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。我们监听了监听socket的EPOLLIN事件(新连接),和每个客户端socket的EPOLLIN事件(数据到达)。
  3. epoll_wait:等待事件发生。返回就绪的事件数量nfds,就绪事件信息存放在events数组中。这是整个程序的核心阻塞点。
  4. 事件处理循环:遍历events数组,根据data.fd区分是监听socket还是客户端socket,根据events字段判断具体是什么事件(读、写、错误等)。
  5. 连接关闭处理:当read返回0时,表示对端(客户端)调用了close,这是TCP连接正常关闭的“FIN”报文。我们必须调用epoll_ctl删除对该socket的监控,并关闭本地的文件描述符,释放资源。这是极易遗漏的一步,会导致epoll实例中残留无效的fd,浪费资源甚至引发错误。

3.3 如何升级到高性能ET模式?

要将上面的LT模式服务器改为ET模式,需要改动几个地方:

  1. 添加ET标志:在epoll_ctl添加或修改事件时,设置EPOLLIN | EPOLLET
  2. 设置非阻塞IO:必须将对应的socket(特别是客户端连接socket)设置为非阻塞模式,使用fcntl(fd, F_SETFL, flags | O_NONBLOCK)
  3. 循环读取直到EAGAIN:在可读事件处理分支中,不能只调用一次read。必须用一个while循环,持续读取,直到read返回-1errnoEAGAINEWOULDBLOCK,这表示内核缓冲区暂时没数据了。
  4. 同理处理可写事件:如果注册了EPOLLOUT事件(通常在写缓冲区满后注册,可写时触发),也需要循环write直到返回EAGAIN

实操心得:对于初学者,强烈建议先从LT模式开始。它编程简单,逻辑清晰,足以应对大多数并发场景。在彻底理解LT和整个事件驱动模型后,再挑战ET模式。很多生产环境中的中间件,为了代码的健壮性和可维护性,也依然在使用LT模式。不要盲目追求ET,合适才是最好的。

4. 常见“坑点”排查与性能优化实录

在实际使用epoll编写服务时,会遇到各种各样的问题。下面是我在“头歌”练习和实际项目中踩过的一些坑,以及排查思路。

4.1 连接关闭与资源泄露

问题现象:服务器运行一段时间后,连接数不再增长,甚至出现“Cannot assign requested address”错误,或者进程的文件描述符耗尽(ulimit -n查看)。

排查与解决

  1. 检查连接关闭逻辑:这是最常见的原因。必须确保在read返回0(对端关闭)或read/write返回-1且不是EAGAIN的错误时,执行epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sockfd, NULL)close(sockfd)忘记EPOLL_CTL_DEL会导致epoll实例内部继续监控一个已经关闭的fd,下次epoll_wait可能会返回这个无效fd的事件,导致程序异常。
  2. 使用lsof命令排查:在Linux服务器上,使用lsof -p <pid>可以查看指定进程打开的所有文件描述符。观察socket类型的描述符是否只增不减。如果已关闭连接的fd仍然出现在列表中,说明没有正确关闭。
  3. 注意closeshutdown的区别close只是减少文件描述符的引用计数,只有当计数为0时才真正关闭TCP连接。如果父子进程共享socket,可能需要所有进程都close才行。shutdown则直接触发TCP连接的关闭流程(发送FIN),更直接。

4.2 惊群问题(Thundering Herd)

问题现象:在多个进程(例如Nginx worker进程)同时监听同一个端口并使用epoll时,当一个新连接到来,内核可能会唤醒所有阻塞在epoll_wait上的进程,但最终只有一个进程能成功accept,其他进程被唤醒后又继续睡眠,造成不必要的上下文切换和CPU资源浪费。

解决方案: 现代Linux内核(2.6+)的epollaccept系统调用本身已经解决了这个问题。但为了绝对兼容,可以在创建监听socket后、监听之前,使用setsockopt设置SO_REUSEPORT选项(Linux 3.9+)。这样,多个进程可以绑定到完全相同的IP和端口,内核会使用哈希算法将新连接相对均匀地分配给这些监听进程,从根源上避免了惊群。Nginx就使用了这种方式。

4.3 事件丢失与ET模式的陷阱

问题现象:在ET模式下,客户端快速发送两段数据,服务器只收到一段;或者大文件发送不完整。

原因与解决: 这就是前面提到的ET模式陷阱。在可读事件触发后,必须用循环读完所有数据。

// ET模式下的标准读处理代码片段 if (events[i].events & EPOLLIN) { int sockfd = events[i].data.fd; ssize_t n; while (1) { n = read(sockfd, buffer, BUFFER_SIZE); if (n > 0) { // 处理数据... } else if (n == 0) { // 对端关闭连接 close_and_clean(sockfd); break; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 数据已全部读完,跳出循环 break; } else { // 真实错误 perror("read error"); close_and_clean(sockfd); break; } } } }

关键点:这个while循环必须在一次epoll_wait返回的事件处理中完成。因为ET模式只在该socket缓冲区从空变为非空时通知一次。

4.4 性能优化要点

  1. 调整epoll实例大小epoll_create1的参数size在现代内核中已无实际限制,但保留它是为了向前兼容,一般设为大于0的数即可。
  2. 合理设置epoll_wait超时:根据业务场景设置。纯网络代理可以设为-1(永久阻塞);混合业务(既要处理网络又要处理定时任务)可以设一个较小的超时(如100毫秒),以便有机会检查其他任务队列。
  3. 避免在事件回调中执行阻塞操作epoll的核心是单线程非阻塞。如果在处理某个socket的读事件时,进行了复杂的数据库查询(阻塞),那么整个线程都会被卡住,所有其他连接都无法响应。必须将耗时操作异步化,比如扔到线程池处理。
  4. 监控就绪事件数量:如果epoll_wait每次返回的nfds都接近你传入的MAX_EVENTS,说明这个值设小了,可能造成事件需要多次调用才能取完,应考虑调大。
  5. 使用EPOLLONESHOT(高级技巧):对于需要保证一个socket在某一时刻只有一个线程处理的场景(比如连接状态机复杂),可以在事件上设置EPOLLONESHOT。内核在通知一次该事件后,会暂时禁用对该fd的监控,直到程序员用epoll_ctlEPOLL_CTL_MOD重新激活它。这可以防止多个线程同时操作一个socket。

5. 从“头歌”实验到真实项目:架构思维延伸

在“头歌”上跑通一个简单的epoll服务器只是第一步。要把这套技术用到真实的高并发项目中,还需要构建更上层的架构思维。

5.1 单Reactor与多Reactor模型

我们上面写的例子,就是最基础的单Reactor单线程模型:一个epoll循环处理所有事件(连接、读、写)。所有逻辑都在一个线程内,简单,但CPU密集型业务会阻塞整个服务。

更高级的是单Reactor多线程模型:主线程(Reactor)只负责I/O事件的分发(accept,read,write的触发)。当read到完整的数据包后,将其封装成一个任务对象,投递到一个共享的任务队列。一组工作线程(Thread Pool)从队列中取出任务进行业务处理(如解析协议、查询数据库、计算等)。处理完成后,再将结果通过队列或直接通知回主线程,由主线程执行write操作。这样,I/O是高效的,耗时计算也由线程池分担了。Memcached大致采用这种模型。

多Reactor多线程/进程模型,则是Nginx、Redis等应用的选择。由一个主Reactor(通常也是主进程)只负责accept新连接,然后将建立好的连接socket通过负载均衡的方式分发给多个子Reactor(子进程或子线程)。每个子Reactor都有自己的epoll循环,独立处理分配给它的那批连接的读写事件。这种模型将连接均匀分散,充分利用多核CPU,扩展性极佳。

5.2 与异步IO(AIO)的辨析

网络热词中提到了“I/O多路复用”和“异步I/O”的概念。这里简单厘清:

  • I/O多路复用(select/poll/epoll):本质是同步非阻塞I/O。程序主动调用epoll_wait去“轮询”或“等待”I/O是否就绪。就绪后,程序需要自己调用read/write来完成数据在用户态和内核态之间的拷贝,这个拷贝过程是同步的(会阻塞当前线程直到完成)。
  • 真正的异步I/O(如Linux的AIO):程序发起一个aio_read请求后立即返回,内核会负责完成从内核缓冲区到用户缓冲区的整个数据拷贝工作。拷贝完成后,内核通过信号或回调函数通知程序“数据已经在你提供的缓冲区里了,直接用吧”。整个过程程序都不需要阻塞等待。

所以,epoll解决了“等待数据就绪”的阻塞问题,但“数据拷贝”这一步仍然是同步的。而AIO则把最后一步也异步化了。但在实际中,由于Linux原生AIO对网络socket的支持并不完善(主要针对磁盘文件),而epoll性能已经足够强悍,因此网络编程中epoll是绝对的主流。

5.3 工具链与调试技巧

  1. strace跟踪系统调用:当程序行为诡异时,用strace -f -p <pid>可以跟踪进程及其子进程的所有系统调用,看看epoll_wait,accept,read,write,close的调用顺序和返回值是否符合预期。
  2. netstatss查看连接状态netstat -antp | grep <port>或更高效的ss -antp | grep <port>,可以查看服务器端口上的连接状态(LISTEN, ESTABLISHED, TIME_WAIT等),帮助判断连接是否正常建立和关闭。
  3. 压力测试工具:学会使用ab(ApacheBench),wrk,jmeter等工具对写好的epoll服务器进行并发压力测试,观察在数百、数千个并发连接下的内存和CPU使用情况,以及是否会出现连接失败或响应变慢。

在“头歌”这类平台练习,重点是把基础打牢,理解每个系统调用的行为、每个参数的意义、每种边缘情况的处理。把这些基础代码反复敲几遍,直到不用看文档就能写出来。然后,再去思考如何将其融入更大的、更复杂的服务架构中。当你真正理解了如何用一个线程管理上万连接时,你对Linux网络编程的理解就已经超越了绝大多数人。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 19:00:20

Weil-Petersson同胚的离散刻画:Beta和与Epsilon和的几何意义

1. 从几何直觉到解析公式&#xff1a;Weil-Petersson 度量的同胚不变量在复分析与双曲几何的交叉领域&#xff0c;Teichmller 空间及其上的 Weil-Petersson 度量构成了一个极其丰富的研究对象。这个空间可以直观地理解为&#xff0c;给定一个拓扑曲面&#xff0c;其上所有可能的…

作者头像 李华
网站建设 2026/6/26 18:57:33

参考文献格式乱如麻?学长安利这几个AI论文平台

写论文最怕的就是被参考文献格式搞到头大&#xff0c;选题难、查资料累、润色费时&#xff0c;再加上格式不统一&#xff0c;简直像在打一场没有硝烟的仗。其实只要用对 AI 工具、走对流程&#xff0c;就能事半功倍——不少资深教授都推荐&#xff1a;千笔AI&#xff08;中文全…

作者头像 李华
网站建设 2026/6/26 18:57:11

登顶顶会|BlockSec 联合研究成果获 SIGMETRICS 2026 最佳论文 Runner-up

BlockSec 与浙江大学、香港城市大学、MBZUAI联合完成的研究论文《Shedding Light on Shadows: Automatically Tracing Illicit Money Flows on EVM-Compatible Blockchains》&#xff0c;获得 SIGMETRICS 2026 最佳论文 Runner-up。本届会议共有约 81 篇论文入选&#xff0c;最…

作者头像 李华
网站建设 2026/6/26 18:51:58

通达信三合一底背离指标(上)

本文详细拆解三合一底背离指标的设计逻辑&#xff0c;逐一说明 MACD、KDJ、RSI 三类底背离的判定规则&#xff0c;提供修正后可直接使用的通达信指标源码&#xff0c;并补充指标使用的注意事项。一、指标设计核心思路单一技术指标的底背离信号容易受行情波动影响&#xff0c;出…

作者头像 李华
网站建设 2026/6/26 18:49:10

TQVaultAE:泰坦之旅周年版的终极物品管理解决方案

TQVaultAE&#xff1a;泰坦之旅周年版的终极物品管理解决方案 【免费下载链接】TQVaultAE Extra bank space for Titan Quest Anniversary Edition 项目地址: https://gitcode.com/gh_mirrors/tq/TQVaultAE 你是否曾在《泰坦之旅》中为背包空间不足而烦恼&#xff1f;是…

作者头像 李华