Linux IO多路复用
IO多路复用
一个进程可以对多个网路IO(fd)进行监听,检查多个IO的读写就绪状态。与多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建多个进程/线程。
基本IO模型
【1】blockingIO - 阻塞IO
【2】nonblockingIO - 非阻塞IO
【3】signaldrivenIO - 信号驱动IO
内核在io就绪后向进程发送信号,进程收到信号后对io进行处理
【4】asynchronousIO - 异步IO应用程序发起io请求后立即返回,内核在后台准备好数据,拷贝到应用层后再通知进程数据已经就绪
【5】IOmultiplexing - IO多路复用
基本网络通信
1、创建一个socket(socket)
int socketfd=socket(AF_INET, SOCK_STREAM, 0);
创建socket一般会返回一个文件描述符,简称fd,用于描述io信息的,返回值一般从3开始,一个连接+1。
0 是标准输入流 /dev/stdin
1 是标准输出流 /dev/stdout
2 是标准错误流 /dev/stderr
fd的分配方式一般是直接分配系统中最小的可用的fd,已经释放关闭的fd需要等1分钟左右才能重新分配。
2、设置协议族(sockaddr_in)
IP、端口协议族绑定
struct sockaddr_in addr; // 定义IPv4地址结构
memset(&addr, 0, sizeof(struct sockaddr_in)); // 清零结构体
addr.sin_family = AF_INET; // 使用IPv4协议
addr.sin_port = htons(port); // 设置端口号(转换为网络字节序)
addr.sin_addr.s_addr = INADDR_ANY; // 设置协议族绑定所有网卡(0.0.0.0)
addr.sin_addr.s_addr = inet_addr(ip); //设置协议族绑定某个ip
返回的结构体解析成IP和端口
char clientIP[128];
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP));
int port=ntohs(clientAddr.sin_port);
3、协议族绑定socketfd (bind)
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("bind");
return 2;
}
bind函数错误码
| errno | 宏定义 | 常见原因 | 解决方案 |
|---|---|---|---|
| 13 | EACCES | 端口 < 1024 需要 root 权限 | 用 sudo运行 或 换高位端口 |
| 98 | EADDRINUSE | 端口被占用(最常见) | 换端口或等待释放 |
| 22 | EINVAL | 已绑定过的 socket 重复绑定 | 检查是否重复调用 bind |
| 9 | EBADF | socket 文件描述符无效 | 检查 socket()是否成功 |
| 99 | EADDRNOTAVAIL | IP 地址不可用 | 检查 IP 是否正确 |
4、监听socket(listen)
int ret=listen(sockfd, 5)
//成功返回0,不成功返回-1
//全连接队列(已经完成三次握手)最多能容纳 5 个已完成的连接,等待 accept()取走,超过
5、获取客户端连接的地址信息accept
struct sockaddr_in client_addr;
memset(&client_addr,0,sizeof(struct sockaddr_in));
socklen_t addrlen = sizeof(struct sockaddr_in);
int clientFd=accept(sockfd, (struct sockaddr_in*)&client_addr, &addrlen); // 返回连接客户端的fd,错误返回-1
获取错误码的方式
调用函数后,函数返回错误,直接获取errno宏,对应的是一个整数返回值,strerror(errno)返回客户端的报错信息。
select IO多路复用
每次调用都需要把fd_set集合从用户控件copy到内核空间中。
以遍历的方式,循环查看每一个fd是否有读写时间就绪。
基本函数
FD_CLR(fd,&fdset); //将fd从集合中去除
基本流程
1、创建一个fd_set集合,并初始化清空集合。
fd_set fdset;//创建集合
FD_ZERO(&fdser); //清空集合
2、将服务器(客户端)的fd加入集合中。
FD_SET(fd,&fdset);//将fd加入集合中
3、启用select函数监听集合中的IO事件。
int ret=select(maxfd,可读集合,可写集合,错误集合,超时时间); //检查IO
//函数返回值:
//大于0:成功,返回集合中已就绪的IO总个数
//等于-1:调用失败
//等于0:没有就绪的IO
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
// nfds:监控的文件描述符集里最大文件描述符加1
// readfds:监控有读数据到达文件描述符集合,传入传出参数
// writefds:监控写数据到达文件描述符集合,传入传出参数
// exceptfds:监控异常发生达文件描述符集合, 传入传出参数
// timeout:定时阻塞监控时间,3种情况
// 1.NULL,永远等下去
// 2.设置timeval,等待固定时间
// 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
/*
select返回值大于0时,说明集合中有io事件就绪,此时集合中就只剩下就绪的IO了,没有就绪的IO会被清除。
FD_ISSET 判断服务器的fd是否还存在集合中就绪,如果存在,则有新的连接进来,使用accept接受新的连接,记录客户端的fd。
FD_ISSET 循环判断是否是客户端的fd就绪,客户端的fd就绪说明客户端有消息发送过来,用recv接收消息即可。
重新循环一遍,将set全部清零,然后重新添加所有的fd进set中,然后继续使用select检查io就绪信息。
if(ret>0){
if(FD_ISSET(serverfd,&sset)){
struct sockaddr_in clientAddr;
socklen_t addrlen=(socklen_t)sizeof(struct sockaddr_in);
memset(&clientAddr,0,sizeof(struct sockaddr_in));
int clientFd=accept(socketfd,(struct sockaddr*)&clientAddr,&addrlen);
if(clientFd>0){
char buff[1024]="hello client,i am server!\n";
int res=send(clientFd,(void*)buff,strlen(buff),0);
if(res<=0){
printf("send function fail %d!\n",errno);
return -4;
}
FD_SET(clientFd,&rset);
maxfd=maxfd>clientFd?maxfd:clientFd;
}
}
int i;
for(i=socketfd+1;i<=maxfd;i++){
if(FD_ISSET(i,&sset)){
//阻塞等待一句消息
char buff[1024]={0};
int res=recv(i,(void*)buff,sizeof(buff),0);
if(res<0){
printf("recv function fail!res:%d errno:%d\n",res,errno);
continue;
}
else if(res==0){
INFO("client disconnect %d\n",i);
close(i);
FD_CLR(i,&rset);
continue;
}
INFO("clientFd:%d recv:%s\n",i,buff);
}
}
select的优缺点
【1】每次调用select,都需要把被监控的fds集合从用户态空间拷贝到内核态空间,高并发场景下这样的拷贝会使得消耗的资源是很大的(缺点) 【2】能监听端口的数量有限,单个进程所能打开的最大连接数由FD_SETSIZE宏定义,一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048(缺点)
【3】只要有一个fd的状态就绪(可读),那么所有的fd都要重新遍历一遍(缺点)
【4】select函数出现的较早,因此稳定性和兼容性较强,这个是他的优点(优点)
poll IO多路复用
【1】底层原理还是和select一样,都是通过循环去判断,而且每次检测都需要将数据拷贝到内核。
【2】poll的实现和select非常相似,只是描述fd集合的方式不同。
【3】针对select遗留的问题中,poll只是使用pollfd结构而不是select的fd_set结构,这就解决了select集合大小1024限制问题。
【4】但poll和select同样存在一个性能缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
基本函数
struct pollfd fds[1024]={0}; //创建pollfd结构体
fds[socketfd].fd=socketfd; //在结构体中写入fd
fds[socketfd].envents=POLLIN; //定义可读事件
int nready=poll(fds,maxfd+1,-1); //pollfd集合,描述数组 fds 的大小,超时时间
struct pollfd {
int fd; /*文件描述符*/
short events; /*监控的事件*/
short revents; /*监控事件中满足条件返回的事件*/
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
//函数返回值:
//-1:失败
//大于0:表示检测的集合中已就绪的文件描述符的总个数
基本流程
1、创建pollfd结构体数组,并将fd加入结构体中,将结构体设置为可读事件
struct pollfd fds[1024]={0};
fds[socketfd].fd=socketfd;
fds[socketfd].events=POLLIN; //可读事件
int maxfd=socketfd;
2、poll监听事件是否有触发
int nready=poll(fds,maxfd+1,-1);
3、查看poll函数返回值,查看服务器的fd的返回时间是否有可读事件,有的话调用accept接收新的客户端fd,将新的fd再加入pollfdd的结构体当中
if(fds[socketfd].revents & POLLIN){ //如果返回的事件中有服务器可读事件
struct sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(struct sockaddr_in));
socklen_t addrlen=(socklen_t)sizeof(struct sockaddr_in);
int clientfd=accept(socketfd,(struct sockaddr*)&clientAddr,&addrlen);
if(clientfd>0){
fds[clientfd].fd=clientfd;
fds[clientfd].events=POLLIN; //可读事件
maxfd=maxfd>clientfd?maxfd:clientfd;
}
}
4、查看是否是客户端的fd事件有返回,客户端有可读事件可以用recv接收消息
for(i=socketfd+1;i<=maxfd;i++){
if(fds[i].revents & POLLIN){
char buff[1024]={0};
int res=recv(i,(void*)buff,sizeof(buff),0);
if(res==0){ //有客户端断开连接
close(i);
fds[i].fd=-1;
fds[i].events=0;
continue;
}
else if(res<0){ //recv报错
continue;
}
else{} //recv返回的字节数
printf("recv function return %s\n",buff);
}
}
实现伪代码
poll服务端实现伪码:
struct pollfd fds[POLL_LEN];
unsigned int nfds=0;
fds[0].fd=server_sockfd;
fds[0].events=POLLIN|POLLPRI;
nfds++;
while {
res=poll(fds,nfds,-1);
if(fds[0].revents&(POLLIN|POLLPRI)) {
//执行accept并加入fds中,nfds++
if(--res<=0) continue
}
//循环之后的fds
if(fds[i].revents&(POLLIN|POLLERR)) {
//读操作或处理异常等
if(--res<=0) continue
}
}
优缺点
【1】解决了fd数量限制的问题,但是基本框架还是和select一样,效率较低,无法满足高并发的需求场景。
epoll IO多路复用
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率,避免了select/poll:每次调用都拷贝所有 fd到内核态。
基本函数
1、创建epoll 句柄
int epfd=epoll_create(int size) //创建一个epoll句柄
//功能:该函数生成一个 epoll 专用的文件描述符。
//参数size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。
2、创建epoll_event事件
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=serverfd;
3、将epoll_event事件绑定到内核中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//向 epoll 对象中添加/修改/删除要管理的连接
//成功:返回 0
//失败:返回 -1,并设置 errno
//功能:epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
//参数epfd: epoll 专用的文件描述符,epoll_create()的返回值
//参数op: 表示动作,用三个宏来表示:
//1. EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
//2. EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
//3. EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
4、阻塞检测网络IO是否有触发事件
int nready=epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//如果成功,表示返回需要处理的事件数目
//如果返回0,表示已超时
//如果返回-1,表示失败
-
功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。
-
参数epfd: epoll 专用的文件描述符,epoll_create()的返回值
-
参数events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
-
参数maxevents: maxevents 告之内核这个 events 有多少个 。
-
参数timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞。
基本流程
1、epoll_create创建epoll 的结构体。
2、创建epoll_event事件,将fd绑定到epoll_event事件中,并标记为可读事件EPOLLIN
3、使用epoll_ctl将fd和事件分发到内核空间
4、创建一个epoll_event数组用于存放返回的事件,使用epoll_wait监听事件返回
5、epoll_wait返回事件的数量,检查返回事件中是否包含服务器的fd或者客户端的fd,使用accrpt或者recv接收即可。
水平触发(LT)
关注点是数据是否有无,只要读缓冲区不为空,写缓冲区不满,那么epoll_wait就会一直返回就绪,水平触发是epoll的默认工作方式。
边缘触发(ET)
关注点是变化,只要缓冲区的数据有变化,epoll_wait就会返回就绪。 这里的数据变化并不单纯指缓冲区从有数据变为没有数据,或者从没有数据变为有数据,还包括了数据变多或者变少。即当buffer长度有变化时,就会触发。 假设epoll被设置为了边缘触发,当客户端写入了100个字符,由于缓冲区从0变为了100,于是服务端epoll_wait触发一次就绪,服务端读取了2个字节后不再读取。这个时候再去调用epoll_wait会发现不会就绪,只有当客户端再次写入数据后,才会触发就绪。 这就导致如果使用ET模式,那就必须保证要「一次性把数据读取&写入完」,否则会导致数据长期无法读取/写入。
优缺点
1、epoll 在内黑采用红黑树管理文件描述符,不会产生用户态到内核态的多次拷贝,并且红黑树效率更高。红黑树插入和删除的都是时间复杂度 O(logN),不会随着文件描述符数量增加而改变。
2、epoll 将文件描述符添加和检测分离,减少了文件描述符拷贝的消耗。 select&poll 调用时会将全部监听的 fd 从用户态空间拷贝至内核态空间并线性扫描一遍找出就绪的 fd 再返回到用户态。下次需要监听时,又需要把之前已经传递过的文件描述符再读传递进去,增加了拷贝文件的无效消耗,当文件描述很多时,性能瓶颈更加明显。 而epoll只需要使用epoll_ctl添加一次,后续的检查使用epoll_wait,减少了文件拷贝的消耗。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)