当前位置: 首页 > news >正文

Linux——I/O复用

目录

一、I/O复用技术

1.1 I/O复用定义

1.2 I/O复用本质

1.3 实现原理

1.4 应用场景

1.5 常见的I/O复用技术 

二、select() 

2.1 工作原理

2.2 select实现TCP服务器端

2.2.1 C语言通过select()实现TCP服务器端

2.2.2 select的缺点

三、poll()

3.1 工作原理

3.2 poll实现TCP服务端

3.3 poll优缺点 

四、epoll() 

4.1 工作原理

epoll的核心机制

水平触发与边缘触发

epoll的性能优势

epoll的适用场景

4.2 epoll实现TCP服务端

4.3 ET模式和LT模式 

ET模式与LT模式的概念

ET模式(边缘触发)

LT模式(水平触发)

关键区别对比

代码示例

4.4 epoll()的优缺点


一、I/O复用技术

1.1 I/O复用定义

        I/O复用(Input/Output Multiplexing)是一种高效处理多个I/O操作的机制,允许单个进程或线程同时监控多个文件描述符(如套接字、管道等),并在这些描述符中的任意一个准备好进行读写操作时通知程序。其核心目标是减少系统资源消耗,避免为每个I/O操作创建独立的线程或进程,从而提升并发性能。

1.2 I/O复用本质

        I/O复用使得程序能够同时监听多个文件描述符,但本身也是阻塞的。如果多个描述符同时就绪,则只能按顺序处理其中的每一个文件描述符。

1.3 实现原理

        通过系统调用(如selectpollepollkqueue)将多个文件描述符注册到内核中,内核监控这些描述符的状态变化(如可读、可写或异常)。当某个描述符就绪时,内核通知应用程序,应用程序再处理对应的I/O事件。

1.4 应用场景

  • 网络服务器(如Web服务器、聊天程序)需同时处理多个客户端连接。
  • 需要非阻塞I/O操作的场景,避免线程阻塞导致资源浪费。
  • 高并发系统中优化性能,减少线程/进程切换的开销。

1.5 常见的I/O复用技术 

(1)select
通过检测多个文件描述符的状态(可读、可写、异常)来进行I/O操作。

但在处理的文件描述符较多时效率较低。

(2)poll
类似于select,也是在指定时间内轮询一定数量的文件描述符,测试是否有就绪者。

但没有描述符数量的限制,支持较大规模的文件描述符集,更加灵活。

(3)epoll(Linux特有)
使用一组函数来完成任务,而不是单个函数。

比select和poll更高效,采用事件驱动机制,适合大量连接的服务器应用。

二、select() 

2.1 工作原理

select() 是一种 I/O 多路复用机制,用于监视多个文件描述符的状态变化(如可读、可写或异常)。它允许程序在单个线程中同时处理多个 I/O 操作,避免阻塞等待单个描述符。

基本流程:

  1. 初始化文件描述符集合:通过 fd_set 结构体设置需要监视的文件描述符(如 sockets、pipes 等)。

  2. 调用 select():传入待监视的描述符集合及超时时间。内核会检查这些描述符的状态。

  3. 内核检查状态:内核遍历所有描述符,判断是否有满足条件的事件(如数据可读、缓冲区可写等)。

  4. 返回结果:select() 返回就绪的描述符数量,并修改 fd_set 集合,仅保留就绪的描述符。

  5. 处理就绪事件:程序遍历 fd_set,处理已就绪的 I/O 操作。

关键特点

  • 同步阻塞:select() 本身是阻塞调用,直到有描述符就绪或超时。

  • 数量限制:受限于 FD_SETSIZE(通常 1024),不适合高并发场景。

  • 效率问题:每次调用需重新传递描述符集合,且内核需线性扫描所有描述符。

性能对比

  • 优点:跨平台兼容性好,适合少量连接。

  • 缺点:与 epoll 或 kqueue 相比,高并发时性能较差。

2.2 select实现TCP服务器端

2.2.1 C语言通过select()实现TCP服务器端

①首先,创建一个数组,该数组用来存放程序中的文件描述符。

②然后,创建一个集合,并调用FD_ZERO 方法,将集合中的每一个位清空。

③接着,使用FD_SET方法,将数组中的每个文件描述符传入到fd_set的集合中。

④调用select()方法,返回就绪文件描述符的总数。

⑤最后,使用FD_ISSET方法,检测哪些位被设置。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
//TCP服务器端使用select,同时处理监听套接字和连接套接字#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(int fds[]) //清空数组,每个元素是-1
{for(int i=0;i<MAXFD;i++){fds[i]=-1;}
}void fds_add(int fds[],int fd)//添加一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){fds[i]=fd;break;}}
}void fds_del(int fds[],int fd)//删除一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==fd){fds[i]=-1;}}
}void accept_client(int sockfd,int fds[]) //接受连接
{int c=accept(sockfd,NULL,NULL);//得到连接套接字if(c<0){return;}printf("accept c=%d\n",c);fds_add(fds,c);//将连接套接子加入数组
}void recv_data(int c,int fds[]) //接受数据
{char buff[128]={0};int n=recv(c,buff,127,0);//只要接收缓冲区有数据,select会继续执行(未读完继续读)if(n<=0)   //如果对方关闭或者接受失败,则删除数组中的套接字{close(c);fds_del(fds,c);printf("client close\n");return;}printf("bufff=%s\n",buff);send(c,"ok",2,0);
}int main()
{//创建套接字int sockfd=socket_init(); if(sockfd==-1){exit(1);}//定义一个数组,收集描述符//原因:需要数组来保存所有的文件描述符,将数组中的元素再存放到集合中,执行select后,select会修改集合中的文件描述符int fds[MAXFD];fds_init(fds);//初始化数组fds_add(fds,sockfd);//向数组中添加监听套接字fd_set fdset;//定义一个集合while(1){FD_ZERO(&fdset);//清除fdset的所有位int maxfd=-1;//定义文件描述符最大值for(int i=0;i<MAXFD;i++) //循环遍历数组,将数组中的套接字放入到集合,并置为1{if(fds[i]==-1){continue;}FD_SET(fds[i],&fdset);//将数组中的元素添加到集合中,将对应的文件标识符位置置为1if(maxfd<fds[i]) //找出最大的文件描述符{maxfd=fds[i];}}//定义时间struct timeval tv={5,0};//使用select方法,会修改fdset集合int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//失败if(n==-1){printf("select err\n");}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){continue;}if(FD_ISSET(fds[i],&fdset))//测试发现该描述符fds[i]有事件{if(fds[i]==sockfd)//accept{accept_client(sockfd,fds);}else//recv{recv_data(fds[i],fds);}}}}}}int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}

        需要注意的是:使用数组,而不直接集合是因为select方法会对集合进行修改,不能保证下一次调用的正确性。

2.2.2 select的缺点

(1)文件描述符数量有限(受限于FD_SETSIZE)

(2)每次调用时都需要重新设置文件描述符集合,效率较低

(3)在大量连接时性能下降(因为要线性扫描所有描述符)

三、poll()

3.1 工作原理

poll() 是 Unix/Linux 系统中的系统调用,用于监视多个文件描述符的状态变化,如是否可读、可写或出现异常。它是 select() 的改进版本,解决了 select() 的一些限制,如文件描述符数量受限和性能问题。

poll() 的函数原型

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  • fds:指向 struct pollfd 数组的指针,每个结构体描述一个需要监视的文件描述符。

  • nfds:数组 fds 中元素的数量。

  • timeout:超时时间(毫秒)。

    • 0:立即返回,不阻塞。

    • -1:无限阻塞,直到事件发生。

    • >0:等待指定毫秒数后超时返回。

pollfd 结构体

struct pollfd {int fd;         // 文件描述符short events;   // 监视的事件(输入)short revents;  // 实际发生的事件(输出)
};

常用事件标志

  • POLLIN:数据可读。

  • POLLOUT:数据可写。

  • POLLERR:错误发生。

  • POLLHUP:连接挂起(如对端关闭)。

  • POLLNVAL:文件描述符未打开。

工作流程

  1. 初始化 pollfd 数组,设置需要监视的文件描述符和事件(events)。

  2. 调用 poll(),系统会阻塞直到事件发生或超时。

  3. poll() 返回后,检查每个 pollfdrevents 字段,判断哪些文件描述符发生了事件。

  4. 根据 revents 处理对应的文件描述符(如读写数据)。

返回值

  • >0:发生事件的文件描述符数量。

  • 0:超时且无事件发生。

  • -1:出错,可通过 errno 获取错误原因。

与 select() 的对比

  • 文件描述符数量poll() 使用数组,无固定限制;select()FD_SETSIZE 限制。

  • 性能poll() 在文件描述符较多时效率更高。

  • 事件类型poll() 提供更丰富的事件标志。

poll() 适合需要监视大量文件描述符的场景,但在高并发场景下,epollkqueue 可能更高效。

3.2 poll实现TCP服务端

①首先,创建一个pollfd数组,该数组用来存放程序中的文件描述符。

②为该数组的数据成员(fd,events,revents)赋值

③使用&检测事件

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<poll.h>
//TCP服务器端使用poll
//poll传递的参数是结构体数组#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(struct pollfd fds[])//初始化结构体数组
{for(int i=0;i<MAXFD;i++){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;}
}void fds_add(struct pollfd fds[],int fd) //向结构体数组中添加描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1){fds[i].fd=fd;fds[i].events=POLLIN;//设置事件为可读fds[i].revents=0;break;}}
}void fds_del(struct pollfd fds[],int fd) //向结构体数组中删除描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==fd){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;break;}}
}void accept_client(int sockfd,struct pollfd fds[])//接受客户端的连接
{int c=accept(sockfd,NULL,NULL);if(c<0){return ;}printf("accept c=%d\n",c);fds_add(fds,c);//向结构体数组中添加文件描述符
}void recv_data(int c,struct pollfd fds[]) //接受客户端的数据
{char buff[128]={0};int num=recv(c,buff,127,0);//只要接收缓冲区有数据,poll会继续执行(未读完继续读)if(num<=0) //对方关闭缓冲区或者缓冲区五无数据则关闭连接套接字{close(c);fds_del(fds,c);//从结构体数组中删除描述符printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);}
int main()
{int sockfd=socket_init(); //创建套接字if(sockfd==-1){exit(1);}struct pollfd fds[MAXFD]; //定义结构体数组fds_init(fds);//初始化结构体数组fds_add(fds,sockfd);//向结构体数组中添加数据while(1){int n=poll(fds,MAXFD,5000);//执行pollif(n==-1) //失败{printf("poll err\n");}else if(n==0)//超时{printf("time out \n");}else{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1)//无效{continue;}if(fds[i].revents&POLLIN) //检测是否有读数据的描述符{if(fds[i].fd==sockfd) //如果是监听套接字,则建立连接{accept_client(sockfd,fds);}else  //接受客户端连接{recv_data(fds[i].fd,fds);}}}}}}int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}

3.3 poll优缺点 

优点:

实时性:poll(投票/轮询)能够快速获取用户反馈或数据变化,适用于需要频繁更新的场景,例如实时监控或动态数据展示。

简单易用:实现轮询的逻辑通常较为简单,代码复杂度低,适合基础场景或快速原型开发。

兼容性强:几乎所有设备和浏览器都支持轮询,无需依赖特定技术(如WebSocket或Server-Sent Events)。

缺点:

资源浪费:频繁的轮询请求可能导致服务器和客户端资源浪费,尤其是在无数据变化时仍会发送请求。

延迟问题:轮询间隔决定了数据更新的实时性。较长的间隔会导致延迟,较短的间隔会增加服务器负载。

可扩展性差:高并发场景下,大量客户端频繁轮询可能导致服务器性能瓶颈。替代方案(如长轮询或WebSocket)更适合大规模应用。

四、epoll() 

4.1 工作原理

        epoll是Linux内核提供的一种高效I/O多路复用机制,主要用于处理大量文件描述符的I/O事件。相比select和poll,epoll在性能和扩展性上具有显著优势。

epoll的核心机制

        epoll通过三个系统调用实现:epoll_createepoll_ctlepoll_waitepoll_create创建一个epoll实例并返回对应的文件描述符。epoll_ctl用于向epoll实例注册、修改或删除需要监控的文件描述符及其事件。epoll_wait等待事件的发生并返回就绪的事件列表。

        epoll采用红黑树和就绪列表的双数据结构设计。红黑树存储所有注册的文件描述符,保证插入、删除和查找操作的高效性。就绪列表存储已就绪的事件,避免遍历所有文件描述符。

水平触发与边缘触发

        epoll支持两种事件触发模式:水平触发(LT)和边缘触发(ET)。水平触发模式下,只要文件描述符处于就绪状态,epoll会不断通知应用程序。边缘触发模式下,epoll仅在文件描述符状态发生变化时通知一次,应用程序需处理所有可用数据。

        水平触发模式更简单,但可能效率较低。边缘触发模式更高效,但需要应用程序更精确地处理事件,避免遗漏数据。

epoll的性能优势

        epoll避免了select和poll的线性扫描问题,仅关注活跃的文件描述符。这使得epoll在高并发场景下性能更优,尤其适合处理大量连接但活跃度不高的网络应用。

        内核通过回调机制将就绪的文件描述符加入就绪列表,无需遍历所有描述符。epoll_wait直接返回就绪列表,时间复杂度为O(1)。

epoll的适用场景

        epoll特别适合高并发的网络服务器,如Web服务器、即时通讯服务器等。在这些场景中,epoll能够高效处理成千上万的并发连接,同时保持较低的CPU和内存开销。

        epoll不适用于文件I/O或低并发的场景,因为文件I/O通常不适合非阻塞模式,而低并发场景中select或poll可能已经足够。

4.2 epoll实现TCP服务端

①首先,使用epoll_create()创建epoll对象 。

②然后,使用epoll_ctl()操作内核事件表。

③接着,使用epoll_wait()等待事件发生,获取就绪队列。

事件发生后,立即获得就绪的描述符集合(无须线性扫描全部描述符)

#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>#define MAXFD 10
//创建套接字
int socket_init() 
{int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5);if(res==-1){return -1;}return sockfd;}//添加数据到内核事件表
void epoll_add(int epfd,int fd)
{struct epoll_event ev; //定义数组结构体ev.data.fd=fd;  //将数据添加到表中ev.events=EPOLLIN;//设置事件为读事件if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1){printf("epoll_ctl err\n");}                                                                                          
}
//从内核表中删除数据
void epoll_del(int epfd,int fd)
{if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1){printf("epoll ctl del err\n") ;}
}
//接受客户端的连接
void accept_client(int sockfd,int epfd)
{int c=accept(sockfd,NULL,NULL);if(c<=0){printf("accept err\n");return ;}printf("accept c=%d\n",c);epoll_add(epfd,c);//将c添加到内核事件表
} 
//接受客户端的数据
void recv_data(int c,int epfd)
{char buff[128]={0};int n=recv(c,buff,1,0);if(n<=0){epoll_del(epfd,c); //将c从内核事件表删除close(c);printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);
}int main()
{int sockfd=socket_init();//创建套接字if(sockfd==-1){exit(1);}//创建内核事件表(红黑树) 就绪队列(链表)int epfd=epoll_create(MAXFD);//将监听套接字添加到内核事件表epoll_add(epfd,sockfd);struct epoll_event evs[MAXFD];//用户数组,收集就绪描述符while(1){//获取就绪描述符,会阻塞int n=epoll_wait(epfd,evs,MAXFD,5000);//从内核事件表中获取就绪描述符存放到evs中if(n==-1)  //失败{printf("epoll wait err\n");exit(1);}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<n;i++)//就绪描述符的个数=n{int fd=evs[i].data.fd;//从就绪数组中取出描述符if(fd==-1){continue; //无效}if(evs[i].events&EPOLLIN) //判断读事件是否发生               {if(fd==sockfd) //accept{accept_client(fd,epfd); }else   {recv_data(fd,epfd);}}    }}}
}

4.3 ET模式和LT模式 

ET模式与LT模式的概念

        ET(Edge Triggered)模式和LT(Level Triggered)模式是I/O多路复用技术中的两种事件触发机制,常见于epollselectpoll等系统调用中。两者的核心区别在于事件通知的方式和对用户态程序的要求。

ET模式(边缘触发)

        ET模式仅在状态变化时触发事件。例如,当文件描述符(fd)从不可读变为可读(如新数据到达)时,仅通知一次。若未完全处理数据,后续不会重复触发。

特点:

  • 高效:减少重复事件通知的次数。
  • 需非阻塞IO:必须一次性处理完所有数据,否则可能丢失事件。
  • 需手动维护:需循环读取数据直到EAGAINEWOULDBLOCK错误。

适用场景:

  • 高并发场景,如高性能服务器。
  • 开发者能确保事件被及时处理。

LT模式(水平触发)

LT模式在状态满足条件时持续触发事件。例如,只要fd可读,每次调用epoll_wait都会通知,直到数据被完全读取。

特点:

  • 简单易用:无需担心事件丢失,未处理的数据会重复触发。
  • 可能效率较低:频繁的事件通知可能增加开销。
  • 阻塞/非阻塞IO均可:未强制要求一次性处理完数据。

适用场景:

  • 对代码健壮性要求较高的场景。
  • 开发者希望简化事件处理逻辑。

关键区别对比

特性ET模式LT模式
触发条件状态变化时(边缘)状态持续满足时(水平)
通知次数仅一次多次直至条件解除
IO要求必须非阻塞阻塞或非阻塞均可
数据未处理可能丢失事件持续触发事件
性能更高相对较低

代码示例

ET模式下的处理(需非阻塞IO)

while (true) {int n = epoll_wait(epfd, events, MAX_EVENTS, timeout);for (int i = 0; i < n; i++) {if (events[i].events & EPOLLIN) {while (1) {ssize_t cnt = read(fd, buf, sizeof(buf));if (cnt == -1) {if (errno == EAGAIN) break; // 数据已读完else handle_error();} else if (cnt == 0) {close(fd); // 连接关闭}// 处理数据}}}
}

LT模式下的处理

while (true) {int n = epoll_wait(epfd, events, MAX_EVENTS, timeout);for (int i = 0; i < n; i++) {if (events[i].events & EPOLLIN) {ssize_t cnt = read(fd, buf, sizeof(buf));if (cnt <= 0) close(fd); // 连接关闭或错误// 处理数据(未读完的数据下次会再次触发)}}
}

选择建议

  • ET模式:追求极致性能,且能确保及时处理事件。
  • LT模式:优先代码简洁性和容错性,或对性能要求不高。

在实际开发中,epoll默认使用LT模式,需通过EPOLLET标志显式启用ET模式。

4.4 epoll()的优缺点

epoll()的优点:

高效的事件通知机制:epoll采用基于事件驱动的回调机制,仅通知活跃的文件描述符(FD),避免了遍历所有FD的开销。与select/poll相比,性能随FD数量增加而线性下降的问题得到显著改善。

支持大并发连接:epoll使用红黑树管理FD,内核通过mmap共享用户空间和内核空间的数据,减少了复制开销。理论上支持的FD数量仅受系统内存限制,适合高并发场景(如Web服务器)。

边缘触发(ET)与水平触发(LT)模式:ET模式仅在FD状态变化时通知一次,减少重复事件触发;LT模式兼容传统poll行为,简化编程。开发者可根据场景选择模式。

零拷贝优化:通过共享内存机制(mmap)避免FD集合在用户和内核空间之间的频繁拷贝,降低CPU和内存开销。

epoll()的缺点:

仅限Linux平台:epoll是Linux特有的API,不具备跨平台兼容性。其他系统需使用类似机制(如kqueue、IOCP)。

编程复杂度较高:ET模式需非阻塞IO并处理EAGAIN错误,否则可能遗漏事件。相比select/poll,代码逻辑更复杂。

不适合短连接场景:频繁增删FD(如短连接的HTTP服务)会导致红黑树调整开销,可能弱化性能优势。

内核版本依赖:旧内核(早于2.5.44)不支持epoll,且部分特性(如EPOLLEXCLUSIVE)需较新版本。

http://www.lryc.cn/news/583201.html

相关文章:

  • 零知开源——STM32F407VET6驱动SHT41温湿度传感器完整教程
  • Linux 中的 .bashrc 是什么?配置详解
  • Python 初识网络爬虫:从概念到实践
  • 什么是公链?
  • 微软 Bluetooth LE Explorer 实用工具的详细使用分析
  • 新零售“云化”进化:基于定制开发开源AI智能名片S2B2C商城小程序的探索
  • 【视频观看系统】- 技术与架构选型
  • HashMap源码分析:put与get方法详解
  • 爬楼梯及其进阶
  • Kubernetes 存储入门
  • 由 DB_FILES 参数导致的 dg 服务器无法同步问题
  • 搭建一款结合传统黄历功能的日历小程序
  • 汽车智能化2.0引爆「万亿蛋糕」,谁在改写游戏规则?
  • A1220LUA-T Allegro高精度霍尔效应开关 车规+极致功耗+全极触发 重新定义位置检测标准
  • 【Gin】HTTP 请求调试器
  • 微软官方C++构建工具:历史演变、核心组件与现代实践指南
  • Rust与Cypress应用
  • 在Ubuntu上安装配置 LLaMA-Factory
  • 人工智能-基础篇-27-模型上下文协议--MCP到底怎么理解?对比HTTP的区别?
  • AI应用实践:制作一个支持超长计算公式的计算器,计算内容只包含加减乘除算法,保存在一个HTML文件中
  • Apache Tomcat SessionExample 漏洞分析与防范
  • 【AI大模型】PyTorch Lightning 简化工具
  • Node.js 是什么?npm 是什么? Vue 为什么需要他们?
  • Flutter基础(前端教程⑦-Http和卡片)
  • 【数字后端】- Standard Cell Status
  • SQLZoo 练习与测试答案汇总(复杂题有最优解与其他解法分析、解题技巧)
  • Java 各集合接口常用方法对照表
  • 解决SQL Server SQL语句性能问题(9)——SQL语句改写(7)
  • 如何识别SQL Server中需要添加索引的查询
  • nl2sql的解药pipe syntax