计算机网络(十二) —— 高级IO
#1024程序员节 | 征文#
目录
一,预备
1.1 重新理解IO
1.2 五种IO模型
1.3 非阻塞IO
二,select
2.1 关于select
2.2 select接口参数解释
2.3 timeval结构体和fd_set类型
2.4 socket就绪条件
2.5 select基本工作流程
2.6 简单select的服务器代码
2.7 select优缺点
三,poll
3.1 关于poll
3.2 poll接口介绍
3.3 poll服务器
3.4 poll优缺点
四,epoll
4.1 关于epoll
4.2 epoll的系列系统调用
4.2.1 epoll_create
4.2.2 epoll_wait
4.2.3 epoll_ctl
4.3 epoll模型
4.4 epoll服务器编写
4.5 epoll优点
五,epoll两种工作模式
5.1 LT,ET工作模式
5.2 ET工作模式的epoll_server服务器(较难,了解即可)
一,预备
1.1 重新理解IO
我们以read && write这两个系统接口为例:
- 调用read时,底层缓冲区没数据,read就阻塞着;当write写的时候,写入缓冲区满了或者滑动窗口为0了,那么write也阻塞等着
- 应用层对套接字读和写,不是直接从网络里读写,而是从OS提供的缓冲区中读写 --> read和write本质就是拷贝函数
- 当进行IO的时候,大部分时间尤其是在网络的情况下是在等待的,所以 IO = 拷贝 + 等待,这个等待就是在等缓冲区的条件是否满足,所以要进行拷贝,必须要先判断条件成立(条件:读写事件,就是等待读写事件就绪)
- 网络通信本质就是在IO,那么,什么叫做高效的IO呢?(高效:单位时间内能拷贝的数据量越大,等的比重就越小,IO的效率就越高;而现在几乎所有提高IO效率的策略,本质就是“使单位时间内IO等的比重越小”)
1.2 五种IO模型
我们以钓鱼佬去钓鱼为例,宏观的钓鱼分为两个阶段:1,“等”鱼上钩 2,鱼上钩了把鱼拉上来(一个叫做“等”,一个叫做“钓”,鱼竿相当于文件描述符或者套接字)
场景①:张三是新手,抛竿后,张三就两只手握着鱼竿,哪也不去,啥也不做,就死死盯着水面,突然被咬钩了,就把鱼拉了上来
- 在这个过程中,张三钓鱼过程中,他什么也不做,什么也打扰不了他,我们叫做“阻塞式IO”,我们学到的大部分接口,都是阻塞式的
场景②:李四来了,是个有两三年钓鱼的人,李四开始钓鱼,但是不和张三一样死死盯着鱼竿了,开始刷视频,每隔几分钟去查看有没有咬钩,没有咬钩就继续去刷视频,咬钩了就把鱼拉上来
- 李四并不会因为鱼没咬钩卡在那里,李四会定期检测鱼竿又没有鱼,条件不满足就直接返回了做其他事情去了,满足就开始钓鱼,这个过程中,李四即把鱼钓了,也做了其它事情,我们叫做“非阻塞式IO”
场景③:王五来了,五年钓龄,他看着前面两人,一个一动不动,一个一直在动,他觉得这俩方式很喽。所以王五在鱼竿上放了一个铃铛,然后就躺下不管了,当有鱼来的时候,听到铃铛响了,就把鱼上来了
- 前面两个是自己检测有没有鱼上钩,而王五的钓鱼方法,是让鱼来通知我,这种方式叫做:“信号驱动式IO”,底层有数据时,主动通知上层让上层来读取数据
场景④:赵六来了,但是赵六是开着卡车过来的,拉了一车的鱼竿,然后每根鱼竿都插在岸边,然后赵六就去周期性地区轮询检测有没有鱼上钩
- 4,赵六这样周期性的去检测有没有鱼上钩的这种方式,我们叫做“多路复用,多路转接”
问题:前四个人谁的钓鱼效率最高?
解答:赵六,因为他鱼竿多,那为什么鱼竿多效率就高呢?
假设鱼咬一个钩的概率是1/100,那么鱼咬前面三个人的钩的概率是一样的,都是1/100,但是要赵六的概率就翻了十倍,就是1/10了;所以多个鱼竿可以让我们IO时等待的时间是重叠的,并行的
场景⑤:田七来了,是方园五百里的首富,是公司老板,很忙,田七看着岸边是四个人钓鱼,一个一动不动,一个一直在动,一个躺着玩手机,一个左右在动;田七不喜欢钓鱼,但是喜欢吃鱼,突然田七要回去开会,就喊同行的小王拿上桶,电话等装备,让小王去帮田七钓鱼,当桶满了的时候,就打电话给田七,到时候田七就开车来接小王
- 小王怎么钓不关心,但是在这个过程中,田七这个人他没有参与钓鱼的过程,真正钓鱼的过程是小王,所以田七不算真正意义上的钓鱼者,只是钓鱼的发起者,因为田七不是要钓鱼,他想要的只是鱼,田七的这种钓鱼方式,我们叫做“异步IO”,小王指的就是操作系统,田七就是应用层程序
我们把前四种IO我们叫做“同步IO”,第五种叫做“异步IO”
问题:阻塞式IO与非阻塞式IO有什么区别?
解答:在IO效率方面其实没有任何区别,因为IO = 等待 + 拷贝,从这个角度看,阻塞式和非阻塞式IO效率没有区别,区别就在于它们“等”的方式不一样
问题:同于IO与异步IO有什么区别?
解答:
- 上面前四种IO方式,其实都有在等(假设鱼就是数据,所以其实都在等,王五也在等),并且,它们四个都参与了钓鱼的两个阶段,所以同步IO的本质就是当前特定的进程或者人“有没有参与IO”(参与:参与了“等”或“拷贝”或者都参与了),只要参与了IO,那就都是同步IO
- 异步IO,就是不参与IO,只是发起IO,最后拿结果就行
问题:那么同步IO和我们前面讲的线程同步有什么关系?
解答:就是老婆和老婆饼的关系,只是名字像,没有其它关系
1.3 非阻塞IO
我们以前使用recv的时候,第四个参数一般是设置成0的,表示阻塞等待;如果我们想非阻塞等待,就把第四个参数设置成MSG_DONTWAIT:
但是这个用起来不方便,所以下面是一种更通用的方法:
先来看下这个接口:
这个函数作用就是设置一个文件描述符的属性,就是设置文件描述符在底层的struct_file结构体的标志位,从而告诉内阁这个指定的文件描述符,以其它的方式来操作:
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)
下面我们简单使用下fcntl,将标准输入设置成非阻塞,这样之后通告read读取标准输入时就是非阻塞了:
//非阻塞轮询
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include<string.h>using namespace std;void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL); //获取struct_file的标记位if(fl < 0) //获取失败{perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK); //设置非阻塞标志位
}int main()
{char buffer[1024];SetNonBlock(0); //将标准输入流设置为非阻塞形式sleep(1);while(true){ ssize_t n = read(0, buffer, sizeof(buffer)-1); //阻塞式等待标准输入if(n > 0){buffer[n - 1] = 0; //-1是去掉换行cout << "echo: " << buffer << endl; }else if(n == 0) //一般是读到结尾,网络中读到0是对方把连接关了,来Linux中也可以Ctrl + D结束读取{cout << "read done" << endl;break;}else //读取出错{//1,fd设置成非阻塞,如果底层fd没有数据,recv/read/write/send,返回值会以错误的形式返回//2,出错也有两种情况:a,真的出错了 b,底层没有就绪//3,我怎么区分?通过errno错误码去区分if(errno == EWOULDBLOCK){cout << "0 fd data not ready, try again!" << endl;//这里就可以做其它事情}else //真正意义上的读取出错{cerr << "read error, n:" << n << " errno code: " << errno << " error str: " << strerror(errno) << endl;}sleep(1);}}return 0;
}
二,select
2.1 关于select
前面我们说过,IO = 等 + 拷贝,我们现在要处理的就是等,所以select的作用就是负责“等”,它用的是我们前面五种IO模型中的第四个模型,也就是select可以一次同时等待多个fd
2.2 select接口参数解释
select在man手册中的说明如下:
下面解释一下参数:
- nfds:表示未来要等待的多个文件描述符最大的哪个+1,架设有12345五个描述符需要关心,那么这个参数就设置为6
- readfds,writefds,exceptfds:中间三个参数都是作为输出型参数,用户通过这三个参数告诉内核需要监视的文件描述符的读/写/异常事件,而用户也通过这三个参数告诉用户对应的读/写/异常事件是否就绪(有点绕,但是没关系,后面会展开讲)
- timeout:也是输入输出型参数,调用时表示用户设置的select的等待事件,当有事件就绪后返回时,内核也通过该参数告诉用户本次等待的剩余事件
参数timeout的取值:
- nullptr / NULL:表示让select进行阻塞等待,就比如把模型四变成了模型一 ,直到被监视的某个文件描述符上的某个事件就绪才返回
- 0:表示让select非阻塞等待,无论被监视的文件是否有事件就绪,只要检测后就直接返回
- 特定的时间:让select调用后在指定的事件内进行阻塞等待,如果超过设置的事件,select将进行超时返回
返回值说明:
- 如果函数调用成功,则返回值代表有事件就绪的文件描述符个数
- 如果是超时返回,则返回0,表示没有事件就绪
- 调用失败返回-1,错误码被设置
select调用失败后,错误码设置:
- EBADF:文件描述符已失效或文件已关闭
- EINTR:被信号中断
- EINVAL:参数nfds设置为负数
- ENOMEM:内核内存不足
2.3 timeval结构体和fd_set类型
我们先来看下timeval结构体类型:
在select接口介绍中,timeval作为第四个参数的结构体指针被传参,这个timeval也是一个结构体,我们可以在man 2 gettimeofday获取时间的接口介绍看到:
- 这个就是系统提供的时间结构体,time_t就是无符号整数,以秒为单位,代表时间戳;第二个代表微秒
- select最后一个参数表示给select设置等待方式,假设我们struct timeval timeout = {5, 0}; 表示让select每隔5秒去timeout(超时返回)一次,就是在5秒内没有任何一个文件描述符就绪,就返回,重新开始;如果有文件描述符就绪了,就立即返回,如果我们设为 {0 ,0},那么就是立马返回,非阻塞的一种;也可以设为NULL,也就是阻塞等待
- 如果这个函数设置了,就是一个输入输出型参数,比如我设置5秒timeout一次,如果过了2秒,就有文件描述符就绪了,就会返回,timeout就会变成[3, 0]返回,表示还剩 3 秒就超时了
下面我们再来看下fd_set类型:
看到set,就要想到“位图”,所以fd_set是操作系统给我们提供的一种数据类型,如下图:
- 我们目前关心的fd事件:读,写,异常
- 如果我想关心读事件是否就绪,就把文件描述符设置进readfds,如果我想关心写事件,就设置进writefds,异常事件同理
- 如果想即关心读又关心写,就设置两个;如果我想先关心读再关心写,我们可以把描述符先设置进readfds,等读完了,再添加进writefds
- 在后面我们只学习readfds读事件,只要懂了这一个,就能举一反三,其它的就都会了
问题:为什么是位图呢?
解答:已readfds为例
- 在输入时,表示用户告诉内核,我给你的一个或者多个fd,你要帮我关心fd上面的读事件,如果读事件就绪了,你要告诉我!
- 在输出时(返回时),内核告诉用户,你让我关心的多个fd中有哪些已经就绪了,用户赶紧来读
- 假设这个位图是8位的0000 0000,我想添加0 1 2 3这四个fd,就需要把位图改为0000 1111,所以比特位的位置表示文件描述符编号,比特位的内容,0和1就是表示内核是否需要关心
- 假设当2号fd就绪了,所以操作系统在返回的时候,把没有就绪的比特位清零,返回的就是0000 0010,这样用户就知道2号fd就绪了,可以去读了
- 所以返回时,比特位的位置还是文件描述符编号,但是比特位的内容,0和1就是表示哪些用户关心的fd上面的读事件已经就绪了
所以fd_set是一张位图,本质是为了让用户与内核相互传递fd是否就绪的信息的,所以这也注定了,使用select的时候,一定会有大量的位图操作,所以系统也给我们提供了位图操作的接口:
2.4 socket就绪条件
读就绪前置条件:
- socket内核中, 接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
- 监听的socket上有新的连接请求
- socket上有未处理的错误
写就绪前置条件:
- socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
- socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
- socket使用非阻塞connect连接成功或失败之后
- socket上有未读取的错误
异常就绪前置条件:
- socket上收到带外数据。关于带外数据,和TCP紧急模式相关(TCP协议头中, 有一个紧急指针的字段)
2.5 select基本工作流程
我们实现一个简单的基于select服务器,服务器要做的就是读取客户端发来的数据进行处理,这个select服务器的工作流程如下:
- 初始化服务器,创建套接字,并完成绑定端口,开启监听
- 定义一个fd_array数组用于保存监听套接字和已经与客户端建立连接的套接字(没错,我们把两种套接字放一个数组里面)
- 然后服务器循环调用select函数,轮询检测select关心的fd中的读事件是否就绪,如果就绪则执行对应的操作
- 每次调用select函数之前,都需要定义一个fd_set readfds读文件描述符位图,然后每次循环都将fd_array中的fd依次设置进readfds,然后select就可以去检测根据readfds中fd的读事件是否就绪了
- 当select检测到数据就绪后,就可以再次设置readfds,然后返回给我们,这样我们就知道了哪些文件描述符就绪了, 而这步操作后就绪的文件描述符有两种:
- 如果是监听套接字就绪了,说明有新连接到来,于是调用accept获取新连接,然后再添加到fd_array中
- 如果是读事件就绪了,就调用read函数从套接字从套接字中读取数据并打印输出
- 当客户端把连接关掉后,服务器这边也应该close掉对应的fd,并且将该fd从fd_array中去掉
注意:
- 每次循环都要设置readfds,是因为select返回时readfds里面的参数已经被修改过了,所以我们才会用fd_array保存套接字,并在每次循环时重新设置readfds,writefds和exceptfds同理
- 我们目前是只让服务器关心读事件,如果要关心写事件和异常事件,需要再定义writefds和exceptfds,并且再定义两个fd_array数组来做和readfds相同的操作
- 由于select还需要传入被监视的多个文件描述符的最大值+1,所以每次循环调用select前,还需要获取fd_array中最大的文件描述符值
2.6 简单select的服务器代码
这个服务器用到的文件如下:
源码:计算机网络/高级IO/Select · 小堃学编程/Linux学习 - 码云 - 开源中国
Socket.hpp和Log.hpp我们在之前就已经写过了: 计算机网络(五) —— 自定义协议简单网络程序-CSDN博客
Socket.hpp:
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"#include <cstring>enum
{SocketErr = 2,BindErr,ListenErr,
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}public:void Socket() // 创建套接字{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){log(Fatal, "socket error, %s: %d", strerror(errno), errno);exit(SocketErr);}int opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));}void Bind(uint16_t port) // 绑定套接字{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 如果小于0就绑定失败{log(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen() // 监听套接字{if (listen(_sockfd, backlog) < 0) // 如果小于0就代表监听失败{log(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string *clientip, uint16_t *clientport) // 获取连接{struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (newfd < 0) // 获取失败{log(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); // 把网络字节序列转化为字符串保存在ipstr数组里供用户读取*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET; // 粗心忘记加了peer.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));if (n == -1){std::cerr << "connect to " << ip << ":" << port << "error" << std::endl;return false;}return true;}void Close(){close(_sockfd);}int Fd(){return _sockfd;}private:int _sockfd;
};
Log.hpp:
#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};Log log;
Makefile:
select_server:Main.ccg++ -o $@ $^ -std=c++11
.PNONY:clean
clean:rm -rf select_server
然后下面就是select的主逻辑,SelectServer.hpp,主要的逻辑和上面select的基本工作流程是一样的:
#pragma once
#include<iostream>
#include<sys/select.h>
#include<sys/time.h>
#include "Socket.hpp"
#include "Log.hpp"static const uint16_t defaultport = 8081;
static const int fd_max_num = (sizeof(fd_set)); //表示一个位图能存多少个比特位,也就是一个fd_set能存多少个文件描述符
int defaultfd = -1;class SelectServer
{
public:SelectServer(uint16_t port = defaultport): _port(port){for(int i = 0; i < fd_max_num; i++) //初始化辅助数组{fd_array[i] = defaultfd;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();}void Accepter(){//走到这里,就是监听套接字就绪了,底层有新连接上来了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 不会阻塞在这里,因为走到着一步就是因为底层已经有连接了,已经可以直接拿上来if (sock < 0) return;log(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// 在获取到新连接后也不能直接读,因为只是把连接读上来了,但是对方可能没有发数据,所以当对方没立即发数据的时候,文件里就是空的,这时候读取会出问题哦int pos = 1;for(; pos < fd_max_num; pos++){if (fd_array[pos] != defaultfd)continue; // 说明这个位置是已经被占用的位置elsebreak;}// 走到这里有两种结果if (pos == fd_max_num) // 1,说明辅助数组已经被合法文件描述符占满了{log(Warning, "server is full, close %d now ", sock);close(sock); // 满了直接关了}else // 2,说明当前pos的位置可以被使用{fd_array[pos] = sock; // 把新获取的连接搞到数组里去PrintFd();}}void Recver(int fd, int pos){//走到这里代表套接字的读事件就绪了,客户端给我发数据过来了char buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 有bug,因为目前不能保证,对方通过网络发过来的报文可能不是完整的if (n > 0) //读成功{buffer[n] = 0;std::cout << "get a messge: " << buffer << std::endl;}else if (n == 0) //对方把连接关了,那么我服务器也要把套解析关了{log(Info, "client quit, me too, close fd is : %d", fd);close(fd);fd_array[pos] = defaultfd; // 这里本质是从select中移除}else //读出错{log(Warning, "recv error: fd is : %d", fd);close(fd);fd_array[pos] = defaultfd; // 这里本质是从select中移除,因为start第一个循环会根据这个数组重新设置fd_set}}void Dispatcher(fd_set &rfds){for (int i = 0; i < fd_max_num; i++){int fd = fd_array[i];if (fd == defaultfd) continue; //判断文件描述符是否就绪//所有合法的文件描述符放在辅助数组里,select一返回,所有就绪的文件描述符就会在rfds里//判断我们数组里的合法的文件描述符是否也在rfds里,如果在,代表该文件描述符已经就绪:1,读事件就绪 2,连接事件就绪if (FD_ISSET(fd, &rfds)){//读事件就绪后也有两种情况if (fd == _listensock.Fd()) //1,如果等于listen套接字,就是连接事件就绪,就获取新连接,就把这个新的文件描述符继续添加进数组里{Accepter(); // 连接管理器}else //2,如果不是linsten,那么就是别的文件描述符就绪了,也就是读事件就绪了{Recver(fd, i);}}}}bool Start(){int fd = _listensock.Fd();//struct timeval timeout = {1, 0};fd_array[0] = fd;while(true){fd_set rfds; //读文件描述符位图FD_ZERO(&rfds); //是一个宏,负责把位图清空//每次调用select前都对rfds进行重新设定int maxfd = fd_array[0];for(int i = 0; i < fd_max_num; i++) {if(fd_array[i] == defaultfd) continue;FD_SET(fd_array[i], &rfds); //把指定的文件描述符添加到rfds中,并且此时没有设置进内核里if(maxfd < fd_array[i]);{maxfd = fd_array[i];log(Info, "max fd update, max fd is: %d", maxfd);}}//开始监听后,不能"直接"accept,因为accept就是在检测并获取listensock上面的事件,是阻塞,那么就不能一次等待多个文件描述符了,那么select就没作用了//是什么事件呢?就是新连接到来的事件,就是操作系统底层把三次握手完成,把新连接放到了全连接队列里,然后通过select从底层把文件拿上来//新连接到来,就相当于读事件就绪struct timeval timeout = {0, 0}; //输入输出,可能要进行周期性的重复设置//设为0的话,就是非阻塞了,如果不设置timeout,就是永久性阻塞,直到有事件就绪int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);//如果select告诉你就绪了,那么接下来的一次读取fd的时候,不会被阻塞,就是不会再“等”了,会直接读switch(n){case 0:std::cout << "timeout: " << timeout.tv_sec << "." <<timeout.tv_usec << std::endl;break;case -1:std::cerr << "select error" << std::endl;break;default://有事件就绪了,但是如果上层不处理,select就会一直通知你std::cout << "get a new link " << std::endl;Dispatcher(rfds); //处理事件break;}}}void PrintFd(){std::cout << "online fd list: ";for (int i = 0; i < fd_max_num; i++){if (fd_array[i] == defaultfd)continue;std::cout << fd_array[i] << " ";}std::cout << std::endl;}~SelectServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;int fd_array[fd_max_num]{defaultfd}; //辅助数组,用来保存监听套接字和已经和客户端建立好连接的套接字
};
然后就是 Main.cc:
#include "SelectServer.hpp"
#include<memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << "port[1024+]\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}//单进程演示同时接收多个连接的消息uint16_t port = std::stoi(argv[1]);std::unique_ptr<SelectServer> svr(new SelectServer(port));svr->Init();svr->Start(); return 0;
}
主要逻辑如下图:
2.7 select优缺点
select优点:
- 可以同时等待多个文件描述符,并且只负责等待,实际的拷贝动作由read,write等接口完成,并且最大的好处就是select之后再调用read这些接口不会再被阻塞
- select同时等待多个文件描述符,可以将“等”的时间重叠,提高IO效率
select缺点:
- 到的fd是有上限的,这个和操作系统有关系,不同的操作系统对位图的实现不同
- select的输入输出型参数比较多,所有可能会有比较频繁的用户层与内核层的数据拷贝
- 输入输出型参数较多,每次都要对关心的fd进行重置,就会有很多次的循环遍历,容易出bug或者其它问题
- 用户层,使用第三方数组管理用户的fd,用户层需要很多次遍历,内核中检测fd事件就绪也要遍历,效率比较低
所以select有好有坏,简单来说就是“还不够完美”,所以针对select的缺点,推出了poll
三,poll
3.1 关于poll
poll也是操作系统提供的一个多路转接接口,poll可以让我们的程序同时监视多个文件描述符上的事件是否就绪,和select的定位是一样的,poll是为了解决select的几个缺点
3.2 poll接口介绍
参数说明:
- fds:表示一个poll函数监视的结构列表,是一个结构体,有三个元素,分别为文件描述符,监视的事件集合,就绪的事件集合
- nfds:表示fds数组的长度,也就是第一个参数的长度
- timeout:这个和select的那个是一样的,这里不再赘述
- 返回值和错误码也是和select一样的
pollfd结构体
下面是结构体的三个字段
- fd:就是一个特定的文件描述符,如果设为负值则忽略events字段并revents返回0
- events:表示需要监视该文件描述符上的哪些事件
- revents:内核通过revents告诉用户该文件描述符哪些事件已经就绪
事件宏 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据可读(包括普通和优先数据) | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如Tcp紧急指针 | 是 | 是 |
POLLOUT | 数据可写(包括普通和优先数据) | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级数据可写 | 是 | 是 |
POLLRDHUP | 对方三次挥手,或者对方变比了写操作,由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起,比如管道写端关闭后,读端描述符会收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
- 这个结构体的最大特点是,将输入与输出进行了分离,用户通过这个结构体告诉内核“你要帮我关系fd文件描述符上的event事件”,然后内核就设置一下revent,告诉用户“fd文件描述符上的event已经就绪了”
- 如果我要同时关心多个文件描述符,就只要构建结构体数组,把数组指针传过去就OK了,然后只需要遍历这个数组即可
- select有三个类似参数,把事件类型做了区分,那poll呢?结构体是short类型,Linux很喜欢用比特位来进行传参,所以就把事件设置成位图
3.3 poll服务器
源码:计算机网络/高级IO/poll · 小堃学编程/Linux学习 - 码云 - 开源中国
poll的工作流程和select是基本类似的,所以只需要改下SelectServer.hpp就可以了,重命名为PollServer.hpp:可以发现poll的使用比select简单很多,因为接口的进步就是越来越简单
#pragma once
#include<iostream>
#include<poll.h>
#include<sys/time.h>
#include "Socket.hpp"
#include "Log.hpp"static const uint16_t defaultport = 8081;
static const int fd_max_num = 64; //表示一个evevt_fds数组能存多少个pollfd结构体
int defaultfd = -1;
int long non_event = 0;class PollServer
{
public:PollServer(uint16_t port = defaultport): _port(port){for(int i = 0; i < fd_max_num; i++) //初始化辅助数组{event_fds[i].fd = defaultfd;event_fds[i].events = non_event;event_fds[i].revents = non_event;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();}void Accepter(){//走到这里,就是监听套接字就绪了,有新连接来了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 不会阻塞在这里,因为走到着一步就是因为底层已经有连接了,已经可以直接拿上来if (sock < 0) return;log(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// 在获取到新连接后也不能直接读,因为只是把连接读上来了,但是对方可能没有发数据,所以当对方没立即发数据的时候,文件里就是空的,这时候读取会出问题哦int pos = 1;for(; pos < fd_max_num; pos++){if (event_fds[pos].fd != defaultfd)continue; // 说明这个位置是已经被占用的位置elsebreak;}// 走到这里有两种结果if (pos == fd_max_num) // 1,说明辅助数组已经被合法文件描述符占满了{log(Warning, "server is full, close %d now ", sock);close(sock); //select满了就直接把新链接关了,但是现在满了的时候,我们可以直接进行扩充}else // 2,说明当前pos的位置可以被使用{event_fds[pos].fd = sock;event_fds[pos].events = POLLIN; //关心读事件,如果还想关心读和写,就| POLLOUTevent_fds[pos].revents = non_event;PrintFd();}}void Recver(int fd, int pos){//读事件就绪了char buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 有bug,因为不能保证,对方通过网络发过来的报文可能不是完整的if (n > 0) //读成功{buffer[n] = 0;std::cout << "get a messge: " << buffer << std::endl;}else if (n == 0) //对方把连接关了,那么我服务器也要把套解析关了{log(Info, "client quit, me too, close fd is : %d", fd);close(fd);event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}else //读出错,原因可能{log(Warning, "recv error: fd is : %d", fd);close(fd);event_fds[pos].fd = defaultfd; // 这里本质是从select中移除,因为start第一个循环会根据这个数组重新设置fd_set}}void Dispatcher(){for (int i = 0; i < fd_max_num; i++){int fd = event_fds[i].fd;if (fd == defaultfd) continue; //判断文件描述符是否就绪//判断我们数组里的合法的文件描述符是否也在rfds里,如果在,代表该文件描述符已经就绪,接下来就是检测:1,读事件就绪 2,连接事件就绪if (event_fds[i].revents & POLLIN) //revent按位与上POLLIN,如果为真,就是读事件就绪了{//读事件就绪后也有两种情况if (fd == _listensock.Fd()) //1,如果等于listen套接字,就是连接事件就绪,就获取新连接,就把这个新的文件描述符继续添加进数组里{Accepter(); // 连接管理器}else //2,如果不是linsten,那么就是别的文件描述符就绪了,也就是读事件就绪了{Recver(fd, i);}}}}bool Start(){//首先要清楚文件描述符是代表文件的,一个文件有很多属性,我们现阶段以读写属性为例event_fds[0].fd = _listensock.Fd();event_fds[0].events = POLLIN; //用户告诉内核要等待的事件int timeout = 3000;while(true){int n = poll(event_fds, fd_max_num, timeout); //可以发现确实比select简单很多,正常,因为接口的进步就是越来越简单switch(n){case 0:std::cout << "time out... " << std::endl;break;case -1:std::cerr << "poll error" << std::endl;break;default://有事件就绪了,但是如果上层不处理,select就会一直通知你std::cout << "get a new link " << std::endl;Dispatcher(); //处理事件break;}}}void PrintFd(){std::cout << "online fd list: ";for (int i = 0; i < fd_max_num; i++){if (event_fds[i].fd == defaultfd)continue;std::cout << event_fds[i].fd << " ";}std::cout << std::endl;}~PollServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;struct pollfd event_fds[fd_max_num]; //表示事件对应的文件描述符集合
};
Main.cc:
#include "PollServer.hpp"
#include<memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << "port[1024+]\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}//单进程演示同时接收多个连接的消息uint16_t port = std::stoi(argv[1]);std::unique_ptr<PollServer> svr(new PollServer(port));svr->Init();svr->Start(); return 0;
}
makefile:
poll_server:Main.ccg++ -o $@ $^ -std=c++11
.PNONY:clean
clean:rm -rf poll_server
3.4 poll优缺点
poll优点:
- struct pollfd结构体可以将select的输入输出型参数分离,减少编程成本,使接口使用更简单
- poll可监控的文件描述符没有限制,因为数组大小是用户定的,相当于没有限制
- poll的优点,就是解决了select的缺点
poll缺点:
- 和select一样,poll也需要遍历fds数组来获取就绪的文件描述符
- 每次调用poll,都伴随着大量的struct pollfd在用户态和内核态之间的转换,并且当poll监视的文件描述符很多时,代价会很大
- 总结一下,就是poll需要“遍历”,当监视的套接字很多时,效率会下降,所以poll的缺点不再是功能上的了,而是效率上的了
四,epoll
4.1 关于epoll
epoll也是系统提供的一个多路转接接口:
- epoll比poll多了一个e,这个e是extend也就是加强的意思,是为了解决处理大量句柄而做了改进的poll
- epoll在2.5.44内核中被加入,几乎具备了select和poll的所有优点,所以epoll是被公认为的Linux 2.6 下性能最好的I/O就绪通知方法
- 但是高性能也是有代价的,epoll和poll虽然只差了一个字母,但是二者的实现方式大不相同,epoll的内部实现方式比较复杂
4.2 epoll的系列系统调用
4.2.1 epoll_create
- 该接口作用是创建epoll模型,其中参数size已被废弃,随便设置成大于0的数即可
- 创建模型成功后,打开epoll_file,返回它的文件描述符,所以可以初见端倪:epoll模型有文件描述符
4.2.2 epoll_wait
- epfd:就是epoll_create返回值,表示epoll模型的文件描述符
- events: 是一个epoll_event结构体数组,内核会将已经就绪的事件拷贝到events数组中
- maxevents:表示events数组中的元素个数,该值不能大于创建epoll模型时传入的size大小
- timeout:和select和poll一样,单位是毫秒(ms)
epoll_events结构体
struct_epoll结构体有两个成员变量,events表示需要关心的事件,第二个成员data是一个联合体结构,一般使用结构中的fd,用来保存需要监视的文件描述符,关于联合体,我们之前已经介绍过:C语言进阶——自定义类型_c语言自义类型-CSDN博客
events的常用取值如下:
事件宏 | 描述 |
---|---|
EPOLLIN | 文件描述符可读 |
EPOLLOUT | 文件描述符可写 |
EPOLLPRI | 文件描述符有紧急数据可读 |
EPOLLERR | 文件描述符发送错误 |
EPOLLHUP | 对方把文件描述符close掉了 |
EPOLLET | 将epoll的工作模式设置为ET(后面会讲) |
EPOLLONESHOT | 只监视一次事件,当这次事件监听完之后,如果还想继续监听该文件描述符,需要重新将文件描述符添加到epoll模型中 |
4.2.3 epoll_ctl
该接口作用是对epoll模型进行相关的管理工作,参数:
- epfd:指定的epoll模型
- op:表示具体的动作,有三个选项
- fd:表示需要监视的文件描述符
- event:表示需要监视该文件描述符上的哪些事件
op的三个选项如下:
EPOLL_CTL_ADD | 表示添加新的文件描述符到epoll模型中 |
EPOLL_CTL_MOD | 修改已经注册的文件描述符的监听事件 |
EPOLL_CTL_DEL | 表示从epoll模型中删除指定的文件描述符 |
4.3 epoll模型
- select和poll,都是用数组来管理文件描述符和事件的
- 网卡上面是网卡驱动,再上面就是OS,而OS不相信任何用户,所以就有了系统接口层system call,在上面就是用户了
- select和poll只有一个接口,在操作系统上就是只有一个进程,遍历去文件描述符表,去遍历多个数组,当一个文件描述符都没有就绪的时候,进程就会被挂起等待,放到等待队列当中,在这之后,遍历啥的操作就是操作系统做的了,那么操作系统就被绑在这里了,不管有没有就绪,操作系统都得自己去查
问题:网卡是外设,那么操作系统在硬件层面,怎么知道网卡上有数据了呢?
解答:
- 所有的外设最大的特点,就是会以硬件中断的方式告诉我,就是外设一旦有事件就绪了,就给CPU发一个异步信号,称为硬件中断
- 每个设备都有自己的中断请求(IRQ),CPU根据IRQ将请求通过操作系统分发给相应的硬件驱动程序。硬件驱动通常是内核中的子程序,而不是独立的进程;例如,当网卡接收到数据包时,它会发出中断信号,CPU暂停当前任务,保存当前状态,并执行中断服务程序来处理这个事件。
- 硬件中断可以直接中断CPU的当前活动,并触发内核中相关代码的执行。对于需要时间处理的任务,中断代码本身也可能被其他硬件中断所中断。例如,时钟中断会导致内核调度代码挂起当前正在运行的代码,以便其他任务可以执行。
当某一个进程调用epoll_create接口时,Linux内核会为我们创建一个eventpoll结构体,也就是我们说的epoll模型:
struct eventpoll{ .... /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ struct rb_root rbr; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ struct list_head rdlist; ....
};
操作系统为了支持epoll,为我们提供了三种机制:
①红黑树
操作系统在自身内部为我们维护了一颗红黑树,每个节点包含了一个结构体:
struct rb_node {int fd; //表示内核要关心的fduint32_t event; //表示以位图形式呈现的要等待就绪的事件//还有其它的,比如链接事件
}
epoll模型中的红黑树本质就是在告诉内核,需要监视哪些文件描述符上的哪些事件,epoll_ctl函数本质就是在对这颗红黑树进行对应的增删改操作
②就绪队列
操作系统会为我们维护一个就绪队列ready_queue,一般是双链表实现的,一旦红黑树有一个节点里面的事件就绪了,操作系统就生成一个队列节点:
struct list_node{int fd; //表示已经就绪的fduint32_t event; //已经就绪的事件
}
在epoll中,对于每一个事件都有一个对应的epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构体中的rbn成员和rbllink成员,ffd记录的就是指定的文件描述符,event成员记录的就是对应的事件:
struct epitem{ struct rb_node rbn;//红黑树节点 struct list_head rdllink;//双向链表节点 struct epoll_filefd ffd; //事件句柄信息 struct eventpoll *ep; //指向其所属的eventpoll对象 struct epoll_event event; //期待发生的事件类型
}
③回调机制
网卡允许操作系统注册一些回调机制,操作系统在底层就会提供一个回调函数callback,比如网卡通过驱动把数据搬到了数据链路层,而一旦数据链路层有数据了,就会自动调用callback:
void callback()
{1,向上交付2,交付给Tcp的接收缓冲区3,查找红黑树,确认接收队列和哪一个是关联的,有没有关心比如EPOLLIN/RPOLLOUT4,如果查到了3,并且3号fd关心了EPOLLIN,并且这次到来的数据真的是我们3号fd对应的struct_file5,构建就绪节点,插入到就绪队列中
}
对于用户,只要从就绪队列里拿就好了,上面三套机制,我们就叫做epoll模型,在系统里,会把上面三个放到一块,这样后续要创建多个epoll模型时,方便先描述再组织
问题: 进程如何找到epoll模型?
解答:
- 给epoll模型再创建一个struct_file,Linux下一切皆文件,所以把模型也当成文件
- strucy_file中有指针指向这个epoll模型,所以未来把这个file对象添加到进程的文件描述符表里,这之后进程只要去找自己文件描述表,就能找到struct_file的文件描述符,然后再去找这个struct_file的指针,找到epoll模型的struct_file,再找到epoll模型,就可以访问所有东西了,
- 所以epoll_create返回值也是文件描述符,这时因为epoll模型也统一被接入到了struct_file里,所以poll和epoll是两个完全不同的东西
问题:epoll的三个接口的作用是什么?
解答:
- epoll_create就是在操作系统内部创建struct_file,里面有指针指向epoll模型,然后把struct_file的文件描述符放到进程的文件描述符表里就OK了
- epoll_ctl根据epfd找到epoll模型,找到红黑树,然后op增加修改或删除红黑树的节点
- epoll_wait也是找到epoll模型,找到之后,第二个参数是一个输出型参数,是一个数组指针,它会把就绪队列中就绪的节点一个一个全放到数组里,这样用户就拿到了已经就绪的文件描述符
4.4 epoll服务器编写
源码:计算机网络/高级IO/epoll · 小堃学编程/Linux学习 - 码云 - 开源中国
为了方便,这里就使用cmake生成makefile文件了
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(EpollServer)
add_executable(epoll_server Main.cc)
由于epoll的接口有三个,所以我们先把这三个接口都给封装一下,也就是Epoller.hpp:
#pragma once
#include <cerrno>
#include<sys/epoll.h>
#include<cstring>
#include "Log.hpp"class nocopy //让Epoller不要拷贝
{
public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy& operator=(const nocopy &) = delete;
};static const int size = 128;
class Epoller : public nocopy
{
public:Epoller(){_epfd = epoll_create(size); //创建epoll模型if(_epfd == -1){log(Error, "epoll_create error: %s", strerror(errno));}else{log(Info, "epollcreate success: %d, _epfd");}}int EpollerWait(struct epoll_event revents[], /*timeout*/int num){int n = epoll_wait(_epfd, revents, num, -1); //阻塞等待,return n; //n表示有多少个文件描述符就绪了}int EpollUpdate(int oper, int sock, uint32_t event){int n = 0;if(oper == EPOLL_CTL_DEL){n = epoll_ctl(_epfd, oper, sock, nullptr);if (n != 0){log(Error, "epoll_ctl delete error!");}}else{struct epoll_event ev; //构建要添加的结构体ev.events = event;ev.data.fd = sock; //方便后面处理事件时知道是哪个文件描述符的哪个事件就绪了,因为这个data是一个联合体n = epoll_ctl(_epfd, oper, sock, &ev); //注册进去if (n != 0){log(Error, "epoll_ctl add error!");}}return n; }~Epoller(){if (_epfd >= 0) close(_epfd);}private:int _epfd; //创建的epoll模型int _timeout{3000};
};
下面就是EpollServer.hpp的代码:
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"
#include "Epoller.hpp"static const int num = 64;class EpollServer : public nocopy
{
public:EpollServer(uint16_t port): _port(port), _listensock(new Sock()), _epoller(new Epoller()){}void Init(){_listensock->Socket();_listensock->Bind(_port);_listensock->Listen();log(Info, "sock create success: %d\n", _listensock->Fd());}void Accepter(){std::string clientip;uint16_t clientport;int sock = _listensock->Accept(&clientip, &clientport); //不会再阻塞了if(sock > 0){//不能直接读取,我们再添加到红黑树里,统一处理_epoller->EpollUpdate(EPOLL_CTL_ADD, sock, EPOLLIN);log(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);}}void Recver(int fd){// 简单模拟事件处理char buffer[1024];// 从套接字里面读信息ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;std::cout << "get a messge: " << buffer << std::endl;//再把数据返回给客户端std::string echo_str = "server echo & ";echo_str += buffer;write(fd, echo_str.c_str(), echo_str.size());}else if (n == 0){log(Info, "client quit, close fd is : %d", fd);//链接一旦断开,直接把红黑树里对应的节点去掉,节省资源//移除时,要想保证该文件描述符是合法的,所以要想移除再close关闭_epoller->EpollUpdate(EPOLL_CTL_DEL, fd, 0);close(fd);}else{log(Warning, "recv error: fd is : %d", fd);_epoller->EpollUpdate(EPOLL_CTL_DEL, fd, 0);close(fd);}}void Dispatcher(struct epoll_event revs[], int num){for(int i = 0;i < num; i++) //根据num,就可以一次性遍历获取并处理所有已经就绪的事件{uint32_t events = revs[i].events; //表示已经就绪的事件int fd = revs[i].data.fd; //获取已经就绪的文件描述符//现在就知道了,哪个文件描述符的哪个事件就绪了if (events & EPOLLIN) //读事件就绪了{if(fd == _listensock->Fd()) //说明来了一个新链接,需要accept获取上来{Accepter(); //事件派发} else //说明其它fd上面的读事件就绪{Recver(fd); //事件处理} }else if(events & EPOLLOUT) //写事件就绪了{//可以在这里扩充写的处理}else //其它事件就绪了{}}}void Start(){//将listensock添加到epoll中,这句话本质是:将listensock和它关心的事件,都给添加到内核epoll模型中的红黑树中_epoller->EpollUpdate(EPOLL_CTL_ADD, _listensock->Fd(), EPOLLIN); //我想把“读事件”,“添加”进去struct epoll_event revs[num];while(true){int n = _epoller->EpollerWait(revs, num);if(n > 0) //有事件就绪了{log(Debug, "event happened, fd is : %d", revs[0].data.fd);Dispatcher(revs, n); //处理事件逻辑 }else if(n = 0) //超时了{log(Info, "time out ...");}else //错误了{log(Info, "epoll wait error");}}}~EpollServer(){}
private:std::shared_ptr<Sock> _listensock;std::shared_ptr<Epoller> _epoller;uint16_t _port; //端口号
};
Main.cc:
#include "EpollServer.hpp"void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << "port[1024+]\n"<< std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return 1;}auto port = std::stoi(argv[1]);std::unique_ptr<EpollServer> svr(new EpollServer(port));svr->Init();svr->Start();return 0;
}
下面是测试结果:
可以看到,我们是单进程,但是却可以同时接收多个客户端发来的请求,这就是多路转接的好处,相比多进程和多线程,多路转接所占用的资源更少
4.5 epoll优点
- 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开
- 检测就绪时间复杂度为O(1),因为检测步骤只有一个,就是看就绪队列是否为空,一个if就可以解决;获取就绪就是O(n),就是把队列里的节点一个一个拷贝到用户层,一个while解决
- fd和event没有上限,因为所有文件描述符和要关心的事件都是红黑树管理的,这颗红黑树有多大和epoll没关系
- 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度O(1),即使文件描述 符数目很多,效率也不会受到影响
- 没有数量限制:文件描述符数目无上限
- 有select,poll的所有优势,也解决了它们的缺点,epoll_wait获取就绪队列数组后,返回值n,表示有几个fd就绪了,而且会把就绪队列的节点一个一个弹出,这也意味着,返回给用户的就绪事件是连续的,意味着上层用户处理所有已经就绪的事件,不用再一个个检测fd是否非法的,或者是否就绪,只要拿着n就可以按指定次数处理了,省去了用户层很多的多余动作
五,epoll两种工作模式
5.1 LT,ET工作模式
epoll有两种工作模式,分别是水平触发模式和边缘触发模式
水平触发(LT,Level Triggered)
- 只要底层有数据,epoll就会一直通知用户,就像数字电路的高电平触发一样,只要一直处于高电平,则会一直触发
epoll默认状态下就是LT模式
- 所哟当epoll检测到事件就绪时,可以不立即进行处理,因为只要底层数据没处理完,下一次epoll还会通知用户
- select和poll就是LT模式的
- 支持阻塞读写和非阻塞读写
边缘触发(ET,Edge Triggered)
- 只有底层就绪事件数量发生变化时,epoll才会通知用户,就像数字电路中的上升沿触发一样,只有当电平由低到高那一瞬间才会触发
如果要将epoll改为工作模式,则需要在epoll_create设置EPOLLET选项
- 由于ET模式只通知一次,所以当epoll检测到底层有事件就绪了,必须立即进行处理,而且必须全部处理完毕,因为ET下的epoll只会通知一次,如果数据没有处理完相当于丢失了
- ET模式下的epoll通知用户次数比LT少,因此ET的性能一般比LT高,Nginx就是默认采用ET模式的epoll
- 只支持非阻塞的读写
问题:ET模式下应该如何进行读写?
解答:倒逼用户读事件必须一次性读完,写事件必须一次性把缓冲区写满,所以读数据时必须循环调用recv,写事件时必须循环调用send
- 循环调用recv,直到某次recv实际读到的字节数小于期望独到的字节数,则说明本子底层数据已经读取完毕
- 但有可能最后一次调用recv时,刚好实际读取的字节数和期望值相同,此时底层没有数据了,再次调用recv就会阻塞住
- 这里的阻塞是很致命的,比如我们的服务器是单进程服务器,只要这里阻塞住,那么以后该数据再也不就绪,那么我们的服务器将会永远阻塞在这里,相当于挂掉了
- 所以我们在ET模式下调用recv函数进行读取,必须将对应的文件描述符设置为非阻塞,send函数同理
面试题:为什么ET的效率更高?
解答:
- 因为相同时间内,ET通知的数量比LT多
- ET也会让我们IO的效率也变高,因为我每次通知上层有数据来了之后,倒逼程序员,每次通知都必须把本轮数据全部取走 --> 循环读取,直到读取出错 --> fd默认是阻塞的 --> ET下的所有fd必须是非阻塞的,因为如果是阻塞的,那么循环读取到最后没有数据时依然会阻塞,导致服务器挂起了
- 当把数据全取走之后,Tcp会向对方通告一个更大的窗口,从而从概率上让对方一次发送更多数据,能提升网络传输效率
问题:ET的效率一定比LT高吗?
解答: 上面的情况是普遍的,也有特殊情况,LT不必将所有的fd设置成非阻塞然后循环读取,比如只要LT第一次通知的时候就把数据全取走,就和ET一样了,所以ET和LT谁效率高?不一定,要看具体的代码怎么写
5.2 ET工作模式的epoll_server服务器(较难,了解即可)
源码:计算机网络/高级IO/Reactor · 小堃学编程/Linux学习 - 码云 - 开源中国
相关文章:
计算机网络(十二) —— 高级IO
#1024程序员节 | 征文# 目录 一,预备 1.1 重新理解IO 1.2 五种IO模型 1.3 非阻塞IO 二,select 2.1 关于select 2.2 select接口参数解释 2.3 timeval结构体和fd_set类型 2.4 socket就绪条件 2.5 select基本工作流程 2.6 简单select的服务器代…...
电力行业 | 等保测评(网络安全等级保护)工作全解
电力行业为什么要做网络安全等级保护? 电力行业是关系到国家安全和社会稳定的基础性行业,电力行业信息化程度相对较高,是首批国家信息安全等级保护的重点行业。 01 国家法律法规的要求 1994《计算机信息系统安全保护条例》(国务…...
总裁主题CeoMax-Pro主题7.6开心版
激活方式: 1.授权接口源码ceotheme-auth-api.zip搭建一个站点,绑定www.ceotheme.com域名,并配置任意一个域名的 SSL 证书。 2.在 hosts 中添加:127.0.0.1 www.ceotheme.com 3.上传class-wp-http.php到wp-includes目录ÿ…...
深入探讨编程的核心概念、学习路径、实际应用以及对未来的影响
在当今这个数字化时代,编程已成为连接现实与虚拟世界的桥梁,它不仅塑造了我们的生活方式,还推动了科技的飞速发展。从简单的网页制作到复杂的人工智能系统,编程无处不在,其重要性不言而喻。本文旨在深入探讨编程的核心…...
IDEA如何将一个分支的代码合并到另一个分支(当前分支)
前言 我们在使用IDEA开发Java应用时,经常是和git一起使用的。我们对于git常用的操作包括提交,推送,拉取代码等。还有一个重要的功能是合并代码。 那么,我们应该如何合并代码呢? 如何合并代码 首先,我们…...
Python实现基于WebSocket的stomp协议调试助手工具
stomp协议很简单,但是搜遍网络竟没找到一款合适的客户端工具。大多数提供的都是客户端库的使用。可能是太简单了吧!可是即便这样,假如有一可视化的工具,将方便的对stomp协议进行抓包调试。网上类似MQTT的客户端工具有很多…...
基于neo4j的旅游知识图谱维护与问答系统
你还在为毕业设计发愁吗?试试这个基于Neo4j的旅游知识图谱维护与问答系统吧!这套系统不仅功能强大,而且几乎涵盖了你需要的一切,完美助力你的毕业项目! 系统介绍 该系统是专门针对旅游景点信息的知识图谱工具&#x…...
竞赛学习路线推荐(编程基础)
关于学习路线的推荐,总体上,分两步学习,第一步学习编程语言(C、C、java),第二步是学习数据结构和算法 不少初学者会选择C语言或C作为首选,笔者这里也推荐C或C作为入门,需要注意的是&…...
webRTC搭建:STUN 和 TURN 服务器 链接google的有点慢,是不是可以自己搭建
如果使用 Google 提供的 STUN/TURN 服务器速度较慢,你完全可以自己搭建 STUN 和 TURN 服务器。这有助于提升网络连接速度和稳定性,特别是在需要穿透 NAT 或防火墙的网络环境下。 下面是如何自己搭建 STUN 和 TURN 服务器的具体步骤: 1. 选择…...
利用Pix4D和ArcGIS计算植被盖度
除了水文分析和沟道形态分析之外,在实际工作中还要计算植被盖度! 植被盖度,也称为植被覆盖率或植物覆盖度,是指某一地表面积上植物冠层垂直投影面积占该地表面积的比例。它通常以百分比的形式表示,是描述地表植被状况的…...
用docker Desktop 下载使用thingsboard/tb-gateway
1、因为正常的docker pull thingsboard/tb-gateway 国内不行了,所以需要其它工具来下载 2、在win下用powershell管理员下运行 docker search thingsboard/tb-gateway 可以访问到了 docker pull thingsboard/tb-gateway就可以下载了 3、docker Desktop就可以看到…...
从视频中学习的SeeDo:VLM解释视频并生成规划、代码(含通过RGB视频模仿的人形机器人OKAMI、DexMV)
前言 在此文《UMI——斯坦福刷盘机器人:从手持夹持器到动作预测Diffusion Policy(含代码解读)》的1.1节开头有提到 机器人收集训练数据一般有多种方式,比如来自人类视频的视觉演示 有的工作致力于从视频数据——例如YouTube视频中进行策略学习 即最常见…...
项目集群部署定时任务重复执行......怎么解决???
项目集群部署在不同服务器,导致定时任务重复执行 1、可以在部署时只让一个服务器上有定时任务模块,不过这样如果这台服务器宕机,就会导致整个定时任务崩溃 2、使用分布式锁,使用redis setNX命令加lua脚本在定时任务执行的时候只…...
使用JUC包的AtomicXxxFieldUpdater实现更新的原子性
写在前面 本文一起来看下使用JUC包的AtomicXxxxFieldUpdater实现更新的原子性。代码位置如下: 当前有针对int,long,ref三种类型的支持。如果你需要其他类型的支持的话,也可以照葫芦画瓢。 1:例子 1.1:普…...
vue3组件通信--props
目录 1.父传子2.子传父 最近在做项目的过程中发现,props父子通信忘的差不多了。下面写个笔记复习一下。 1.父传子 父组件(FatherComponent.vue): <script setup> import ChildComponent from "/components/ChildComp…...
leetcode-75-颜色分类
题解(方案二): 1、初始化变量n0,代表数组nums中0的个数; 2、初始化变量n1,代表数组nums中0和1的个数; 3、遍历数组nums,首先将每个元素赋值为2,然后对该元素进行判断统…...
【嵌入式原理设计】实验三:带报警功能的数字电压表设计
目录 一、实验目的 二、实验环境 三、实验内容 四、实验记录及处理 五、实验小结 六、成果文件提取链接 一、实验目的 熟悉和掌握A/D转换及4位数码管、摇杆、蜂鸣器的联合工作方式 二、实验环境 Win10ESP32实验开发板 三、实验内容 1、用摇杆传感器改变接口电压&…...
C#中的接口的使用
定义接口 public interface IMyInterface {int MyProperty { get; set; }void MyMethod(); } 实现类 internal class MyClass : IMyInterface {public int MyProperty { get; set; }public void MyMethod(){Console.WriteLine("MyMethod is called");} } 目录结构…...
记一次真实项目的性能问题诊断、优化(阿里云redis分片带宽限制问题)过程
前段时间,接到某项目的压测需求。项目所有服务及中间件(redis、kafka)、pg库全部使用的阿里云。 压测工具:jmeter(分布式部署),3组负载机(每组1台主控、10台linux 负载机) 问题现象࿱…...
LeetCode - 4. 寻找两个正序数组的中位数
. - 力扣(LeetCode) 题目 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 示例 1: 输入:nums1 …...
算法设计与分析——动态规划
1.动态规划基础 1.1动态规划的基本思想 动态规划建立在最优原则的基础上,在每一步决策上列出可能的局部解,按某些条件舍弃不能得到最优解的局部解,通过逐层筛选减少计算量。每一步都经过筛选,以每一步的最优性来保证全局的最优性…...
【实战篇】GEO是什么?还可以定义新的数据类型吗?
背景 之前,我们学习了 Redis 的 5 大基本数据类型:String、List、Hash、Set 和 Sorted Set,它们可以满足大多数的数据存储需求,但是在面对海量数据统计时,它们的内存开销很大,而且对于一些特殊的场景&…...
SpringBoot最佳实践之 - 项目中统一记录正常和异常日志
1. 前言 此篇博客是本人在实际项目开发工作中的一些总结和感悟。是在特定需求背景下,针对项目中统一记录日志(包括正常和错误日志)需求的实现方式之一,并不是普适的记录日志的解决方案。所以阅读本篇博客的朋友,可以参考此篇博客中记录日志的…...
【Flutter】状态管理:高级状态管理 (Riverpod, BLoC)
当项目变得更加复杂时,简单的状态管理方式(如 setState() 或 Provider)可能不足以有效地处理应用中状态的变化和业务逻辑的管理。在这种情况下,高级状态管理框架,如 Riverpod 和 BLoC,可以提供更强大的工具…...
OAK相机的RGB-D彩色相机去畸变做对齐
▌低畸变标准镜头的OAK相机RGB-D对齐的方法 OAK相机内置的RGB-D管道会自动将深度图和RGB图对齐。其思想是将深度图像中的每个像素与彩色图像中对应的相应像素对齐。产生的RGB-D图像可以用于OAK内置的图像识别模型将识别到的2D物体自动映射到三维空间中去,或者产生的…...
smartctl硬盘检查工具
一、smartctl工具简介 Smartmontools是一种硬盘检测工具,通过控制和管理硬盘的SMART(Self Monitoring Analysis and Reporting Technology),自动检测分析及报告技术)技术来实现的,SMART技术可以对硬盘的磁头单元、盘片电机驱动系统、硬盘…...
清空MySQL数据表
要清空 MySQL 数据表,您可以使用 TRUNCATE 或 DELETE 命令 使用 TRUNCATE 命令 TRUNCATE 命令用于删除表中的所有数据,并重置自增 ID(如果存在): TRUNCATE TABLE table_name;将 table_name 替换为您要清空的表的名称…...
2024年妈杯MathorCup大数据竞赛A题超详细解题思路
2024年妈杯大数据竞赛初赛整体难度约为0.6个国赛。A题为台风中心路径相关问题,为评价预测问题;B题为库存和销量的预测优化问题。B题难度稍大于A题,可以根据自己队伍情况进行选择。26日早六点之前发布AB两题相关解题代码论文。 下面为大家带来…...
Kafka系列之:Kafka集群磁盘条带划分和Kafka集群磁盘扩容详细方案
Kafka系列之:Kafka集群磁盘条带划分和Kafka集群磁盘扩容详细方案 一、lsblk命令二、Kafka节点磁盘条带化方案一三、Kafka节点磁盘条带化方案二四、理解逻辑区块LE五、查看kafka节点磁盘条带划分情况六、Kafka节点磁盘扩容一、lsblk命令 lsblk命令用于列出块设备的信息,包括磁…...
【LeetCode】修炼之路-0007- Reverse Integer (整数反转)【python】
题目 Reverse Integer Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range [-231, 231 - 1], then return 0. Assume the environment does not allow you to store 64-b…...
做网站seo优化/东莞seo计费
常用命令: profile文件立即生效:source /etc/profile source /home/kituser/.bash_profile 修改文件夹子文件权限:chmod -R 777 /home/kituser/jdk1.6.0_37 查看位置:whereis java、which java 防火墙:service iptable…...
门户网站建设需要多少钱/seo优化网站教程百度
C语言与程序设计大学教程 介绍:C语言是国内外广泛使用的一种计算机语言。“C语言编程”被认为是计算机专业学生必备的基本技能。同时,也被公认为对于后续课程“C”、“数据结构”等非常重要。本书是作者十余年的C语言编程及教学经验的一个反映。全书的最大特点在于以…...
微信上可以做网站吗/seo属于技术还是营销
FAIL_ON_SYMBOL_HASH_OVERFLOW 这个类型不存在。 问题的原因是jar包版本冲突。这个是在jackson的 since 2.4版本的包里面才有。2.3之前的版本是没有得。 这个存在2.4及以上才出现的,所以,实际运行的项目中jackson的jar冲突了。 我这儿的问题是flume的l…...
前端开发一个月多少钱/seo建站营销
转自:http://blog.csdn.net/helencoder/article/details/50328629 近期项目中,遇到数据表去重要求,对于ThinkPHP的去重有了更加准确的认识和体会。 两种去重方式: $test_data M(hot); //实例化数据表 $data $test_data->Di…...
商务网站开发工具/app开发平台
前言 小编在spring的beanPostProcessor详解以及模拟Aop中讲到springBean创建的最后一步调用BeanPostProcessor的postProcessAfterInitialization来进行Aop的处理,Aop的模拟是使用了JDK的动态代理,这次小编使用cglib来实现。 问题 spring在面试的时候总…...
wordpress 的客户端/百度在线咨询
公众号 全网唯一一个从0开始帮助Java开发者转做大数据领域的公众号~ 大数据技术与架构或者搜索import_bigdata关注~ 海量【java和大数据的面试题+视频资料】整理在公众号,关注后可以下载~ JVM内存回收机制简述 JVM内存回收机制涉及的知识点太多了,了解越多越迷糊,汗一个,这…...