五种IO模型
我们在内存与外设进行数据传输的过程中,会有5种方式来进行不同的传输,在此我们来分别了解一下这五种IO方式
阻塞IO
阻塞 IO: 在内核将数据准备好之前 , 系统调用会一直等待 . 所有的套接字 , 默认都是阻塞方式
我们用钓鱼的例子来理解这个IO方式,我们可以将这种IO方式类比为一个平静的钓鱼人,当没有鱼的时候,就一直在等待中,有鱼了,就钓起来,这种IO方式数据传输的不会有损失,但是效率较低
非阻塞IO
非阻塞 IO: 如果内核还未将数据准备好 , 系统调用仍然会直接返回 , 并且返回 EWOULDBLOCK 错误码
这种IO方式我们可以理解为一个活泼的钓鱼人,当他开始钓鱼后,他就在原地去做别的事情,比如玩手机,会周期性的检查鱼竿是否有鱼,有鱼的话就钓起来,没有鱼就做自己事情
信号驱动IO
信号驱动IO:内核将数据准备好之后,使用SIGIO信号,通知进程进行IO操作
这种方式我们可以理解成 一个懒惰的钓鱼人,给鱼竿上安装信号装置,当有鱼时,发出信号,就把鱼钓上来,没信号的时候就去做别的事情
IO多路转接
IO 多路转接 : 虽然从流程图上看起来和阻塞 IO 类似 . 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态
这种方式我们可以理解成一个有钱的钓鱼人,直接搞了很多个鱼竿,而后不断轮询,当同一时间等待的文件变多了,文件就绪的概率也就增加了,这种方式钓鱼的效率是最高的
异步IO
异步IO:由内核在数据拷贝完成时,通知进程,使用数据
异步IO我们可以理解为一个老板来钓鱼,他直接让别人帮他钓鱼,最后钓完直接将钓到的鱼提走就可以了,不需要自己钓鱼
任何 IO 过程中 , 都包含两个步骤 . 第一是 等待 , 第二是 拷贝 . 而且在实际的应用场景中 , 等待消耗的时间往往都远远高于拷贝的时间. 让 IO 更高效 , 最核心的办法就是让等待的时间尽量少
同步通信 vs 异步通信
所谓同步,就是在发出一个 调用 时,在没有得到结果之前,该 调用 就不返回 . 但是一旦调用返回,就得到返回值了; 换句话说,就是由 调用者 主动等待这个 调用 的结果 ;异步则是相反, 调用 在发出之后,这个调用就直接返回了,所以没有返回结果 ; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在 调用 发出后, 被调用者 通过状态、通知来通知调用者,或通过回调函数处理这个调用.
进程 / 线程同步也是进程 / 线程之间直接的制约关系是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候
阻塞和非阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起 . 调用线程只有在得到结果之后才会返回 .非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
其它高级IO
非阻塞IO
fcntl:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl 函数有 5 种功能 :复制一个现有的描述符( 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
void SetNoBlack(int fd){
//获得文件状态
int fl = fcntl(fd,F_GETFL);
if(fl < 0){
perror("fcntl error\n");
return;
}
//设置文件状态
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
使用 F_GETFL 将当前的文件描述符的属性取出来 ( 这是一个位图 ).然后再使用 F_SETFL 将文件描述符设置回去 . 设置回去的同时 , 加上一个 O_NONBLOCK 参数 .
轮询方式读取标准输入
我们再进行轮询读取的时候,当read条件不满足时实际是阻塞的,当设置为非阻塞时才开始不断的调用read,就是在周期性的检测read条件是否满足
说明:read非阻塞状态下当返回值小于0时,说明是读的条件不满足,而不是read调用失败,当读条件不满足时,不仅read返回值小于0,还会将全局变量错误码errno设置为EAGAIN,宏,值为11
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
void SetNoBlack(int fd){
//获得文件状态
int fl = fcntl(fd,F_GETFL);
if(fl < 0){
perror("fcntl error\n");
return;
}
//设置文件状态
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main(){
//将标准输入设置为非阻塞
SetNoBlack(0);
char c = 0;
//轮询检测read
while(1){
sleep(1);
//printf("begin to read\n");
ssize_t n = read(0, &c, 1);
if(n > 0){
printf("%c\n",c);
}
//并不是read错误,而是read的条件不满足
else if(n < 0 && errno == EAGAIN){
printf("read cond is not met....\n");
}
else{
perror("read error\n");
}
printf("------------------\n");
}
return 0;
}
多路转接IO
我们在之前的学习中可以意识到,多路转接这种方式是最高效率的一种传输方式,而我们如何实现这种多路转接呢?我们可以使用select,poll,epoll函数来进行操作
select函数
select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的 ;程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
select函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数 nfds 是需要监视的最大的文件描述符值 +1 ;rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;参数 timeout 为结构 timeval ,用来设置 select() 的等待时间
作用:select可以用来监视多个文件的状态,当调用select时,程序就会停到select,直至有文件的状态发生改变
NULL :则表示 select ()没有 timeout , select 将一直被阻塞,直到某个文件描述符上发生了事件 ;0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。特定的时间值:如果在指定的时间段里没有事件发生, select 将超时返回
关于fd_set结构:
typedef struct
{
/*XPG4.2requiresthismembername.Otherwiseavoidthename
fromtheglobalnamespace.*/
#ifdef__USE_XOPEN
__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关 fd 的位int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关 fd 的位void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位
比如我们readfds拿8位举例子,当作为输入时,输入10010101时,就是用户想告诉内核,要监视的文件描述符为0,2,4,7文件的读事件状态,作为输出时,输出为10000001时,就是内核想告诉用户,文件描述符为0,7的文件读事件ing就绪,可以进行读事件
timeout
timeout,结构是timeval。用来设置select等待时间。
关于timeval结构
struct timeval
{
time_t tv_sec; /* seconds 秒*/
suseconds_t tv_usec; /* microseconds 微秒*/
};
参数timeout的取值
NULL:表示select在没有文件条件就绪时,会阻塞等待。
0:非阻塞等待,不管条件就没就绪都会返回,用于检测监视的文件的状态。
特定的时间值:等待一段时间,在时间范围内有文件条件就绪,返回,超过时间select返回0。
EBADF 文件描述词为无效的或该文件已关闭EINTR 此调用被信号所中断EINVAL 参数 n 为负值。ENOMEM 核心内存不足
timeout也是一个输入输出参数。当输入时,用户告诉内核等待时间timeout,当输出时,内核等待完毕,等待时间timeout就为0了
理解select执行过程
理解 select 模型的关键在于理解 fd_set, 为说明方便,取 fd_set 长度为 1 字节, fd_set 中的每一 bit 可以对应一个文件描述符fd 。则 1 字节长的 fd_set 最大可以对应 8 个 fd.* ( 1 )执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000 。 * ( 2 )若 fd = 5, 执行 FD_SET(fd,&set);后set 变为 0001,0000( 第 5 位置为 1) * ( 3 )若再加入 fd = 2 , fd=1, 则 set 变为 0001,0011 * ( 4 )执行select(6,&set,0,0,0)阻塞等待 * ( 5 )若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为0000,0011。注意:没有事件发生的 fd=5 被清空
socket就绪条件
socket 内核中 , 接收缓冲区中的字节数 , 大于等于低水位标记 SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符 , 并且返回值大于 0;socket TCP 通信中 , 对端关闭连接 , 此时对该 socket 读 , 则返回 0;监听的 socket 上有新的连接请求 ;socket 上有未处理的错误 ;
socket 内核中 , 发送缓冲区中的可用字节数 ( 发送缓冲区的空闲位置大小 ), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写 , 并且返回值大于 0;socket 的写操作被关闭 (close 或者 shutdown). 对一个写操作被关闭的 socket 进行写操作 , 会触发 SIGPIPE信号;socket 使用非阻塞 connect 连接成功或失败之后 ;socket 上有未读取的错误 ;
select的特点
可监控的文件描述符个数取决与 sizeof(fd_set) 的值 . 我这边服务器上 sizeof(fd_set) = 512 ,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096.将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd一是用于再 select 返回后, array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得fd逐一加入 (FD_ZERO 最先 ) ,扫描 array 的同时取得 fd 最大值 maxfd ,用于 select 的第一个参数。
select缺点
每次调用 select, 都需要手动设置 fd 集合 , 从接口使用角度来说也非常不便 .每次调用 select ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大同时每次调用 select 都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时也很大select 支持的文件描述符数量太小
select的使用
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define BLACKLOG 5
using namespace std;
class Sock{
public:
static int Socket(){
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr << "socket error"<<endl;
exit(1);
}
return sock;
}
static void Bind(int sock, int port){
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){
cerr << "bind error" <<endl;
exit(3);
}
}
static void Listen(int sock){
if(listen(sock, BLACKLOG) < 0){
cerr << "listen error"<<endl;
exit(4);
}
}
static int Accept(int lsock){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
return accept(lsock, (struct sockaddr *)&peer, &len);
}
};
select服务器主体
#pragma once
#include "Sock.hpp"
#define NUM sizeof(fd_set)*8//数组大小=最多能监视文件个数
#define DET_FD -1//数组默认文件描述符
class SelectServer{
private:
int _lsock;//套接字
int _port;//端口号
int array[NUM];//保存要监视的文件描述符
public:
SelectServer(int lsock = -1, int port = 8080)
:_lsock(lsock)
,_port(port)
{}
void InitServer(){
for(size_t i = 0; i < NUM; i++){
array[i] = DET_FD;
}
_lsock = Sock::Socket();
//端口复用
int opt = 1;
setsockopt(_lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
Sock::Bind(_lsock, _port);
Sock::Listen(_lsock);
array[0] = _lsock;
}
void AddtoArray(int index){
//找到没有数组没有占用的位置
size_t i = 0;
for(; i < NUM; i++){
if(array[i] == DET_FD){
break;
}
}
//满了
if(i >= NUM){
cout<<"select is full, close fd"<<endl;
close(index);
}
else{
array[i] = index;
}
}
void Delete(size_t index){
if(index >= 0 && index < NUM){
array[index] = DET_FD;
}
}
void Handle(int i){
//IO条件就绪
char buf[10240];
ssize_t n = recv(array[i], buf, sizeof(buf), 0);
if(n > 0){
buf[n] = 0;
cout<<buf<<endl;
}
else if(n == 0){
//对端关闭
cout<<"client close..."<<endl;
close(array[i]);
//文件已经关闭,还需要将数组文件描述符删除
Delete(i);
}
else{
cerr << "read error"<<endl;
close(array[i]);
Delete(i);
}
}
void Start(){
while(1){
int maxfd = DET_FD;
//重新设定,需要等待的文件
fd_set readfds;
//初始化fd_set
FD_ZERO(&readfds);
//找文件描述符,将要监视的fd_set对应位设置为1
for(size_t i =0; i < NUM; i++){
if(array[i] == DET_FD){
continue;
}
cout <<array[i];
FD_SET(array[i], &readfds);
//找文件描述符最大值
if(maxfd < array[i]){
maxfd = array[i];
}
}
cout<<endl;
//struct timeval timeout = {5, 0};
//调用 select 等待多个文件
//阻塞等待
int fdn = select(maxfd+1, &readfds, nullptr, nullptr, nullptr);
if(fdn > 0){
//有文件就绪
//找哪个文件就绪
for(size_t i =0; i < NUM; i++){
if(array[i] != DET_FD && FD_ISSET(array[i] , &readfds)){
if(array[i] == _lsock){
//有新连接
int sock = Sock::Accept(array[i]);
if(sock >= 0){
cout << "get a link...."<<endl;
//加入到数组中
AddtoArray(sock);
}
}
else{
//进行IO操作
Handle(i);
}
}
}
}
//超时
else if(fdn == 0){
cerr << "select timeout..."<<endl;
}
//异常
else{
cerr <<"fdn:"<<fdn<< "select error"<<endl;
}
}
}
~SelectServer(){
for(size_t i = 0; i < NUM; i++){
if(array[i] != DET_FD){
close(array[i]);
}
}
}
};
#include"selectServer.hpp"
void Notice(string str){
cout<<"Notice\n\t"<<"please enter port"<<endl;
}
int main(int argc, char *argv[]){
if(argc != 2){
Notice(argv[0]);
exit(1);
}
SelectServer *sser = new SelectServer(atoi(argv[1]));
sser->InitServer();
sser->Start();
delete sser;
return 0;
}
poll
我们的poll是针对select的缺点进行了改进
主要是针对select的:
1.select等待的文件描述符有上限,为sizeof(fd_set)*8
2.selectfd_set参数是输入输出参数,每次select前都需要重新设定fd_set变量
所以我们的poll改进为
1.可以等待的文件描述符无上限
2.文件描述符事件输入与输出用的不是同一个变量,每次poll前无需重新设定
poll函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fds 是一个 poll 函数监听的结构列表 . 每一个元素中 , 包含了三部分内容 : 文件描述符 , 监听的事件集合 , 返回的事件集合.nfds 表示 fds 数组的长度 .timeout 表示 poll 函数的超时时间 , 单位是毫秒 (ms)
![](https://img-blog.csdnimg.cn/bfc9a3df25674020896ed5eb01a752bf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bmzICDnlJ8=,size_20,color_FFFFFF,t_70,g_se,x_16)
返回结果
返回值小于 0, 表示出错 ;返回值等于 0, 表示 poll 函数等待超时 ;返回值大于 0, 表示 poll 由于监听的文件描述符就绪而返回 .
那么我们的内核是怎么知道要监视哪个事件呢?
其实,操作系统只需要按位与上对应的宏,如果为真,则说明需要监视对应的事件
那么我们的用户如何确认什么事件就绪呢?
我们判断返回的pollfd结构的revents,与对应事件按位与即可
poll简单使用
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main(){
struct pollfd fds[1];
//监视标准输入的读
fds[0].fd = 0;
fds[0].events = POLLIN;//由用户填写
fds[0].revents = 0;//由内核填写
int timeout = -1;
char c = 0;
while(1){
int n = poll(fds,1,timeout);
if(n > 0){
//就绪
if(fds[0].revents & POLLIN){
printf("read ready...\n");
int s = read(0, &c, 1);
if(s > 0){
printf("%c",c);
}
else{
perror("read error\n");
}
}
}
else if(n == 0){
//超时
printf("poll timeout...\n");
}
else{
//出错
perror("poll error\n");
}
}
return 0;
}
注意:如果一个事件就绪,但是不进行处理,该事件则一直都是就绪的
poll优缺点
poll优点
pollfd 结构包含了要监视的 event 和发生的 event ,不再使用 select“ 参数 - 值 ” 传递的方式 . 接口使用比select更方便poll 并没有最大数量限制 ( 但是数量过大后性能也是会下降 )
poll的缺点
和 select 函数一样, poll 返回后,需要轮询 pollfd 来获取就绪的描述符 .每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中 .同时连接的大量客户端在一时刻可能只有很少的处于就绪状态 , 因此随着监视的描述符数量的增长 , 其效率也会线性下降
poll的使用
#pragma once
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define BACKLOG 5
using namespace std;
class Sock{
public:
static int Socket(){
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr<<"socket error"<<endl;
exit(1);
}
return sock;
}
static void Bind(int sock, int port){
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htons(INADDR_ANY);
if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){
cerr << "bind error"<<endl;
exit(2);
}
}
static void Listen(int sock){
if(listen(sock,BACKLOG) < 0){
cerr << "listen error"<<endl;
exit(3);
}
}
static int Accept(int sock){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
return accept(sock, (struct sockaddr *)&peer, &len);
}
static void SetSockOpt(int sock){
int opt =1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
};
服务器代码
#pragma once
#include "Sock.hpp"
#define SIZE 20
class PollServer{
private:
int _lsock;
int _port;
struct pollfd fds[SIZE];//保存监视的文件
public:
PollServer(int lsock = -1, int port = 8081)
:_lsock(lsock)
,_port(port)
{}
void InitServer(){
_lsock = Sock::Socket();
Sock::SetSockOpt(_lsock);
Sock::Bind(_lsock, _port);
Sock::Listen(_lsock);
//初始化监视的文件
for(int i=0; i < SIZE; i++){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
//加入连接套接字
fds[0].fd = _lsock;
fds[0].events = POLLIN;
fds[0].revents = 0;
}
//找到文件描述符为-1的加入到数组中
void AddSock2Fds(int sock){
int i =0;
for(; i < SIZE; i++){
if(fds[i].fd == -1){
break;
}
}
if(i >= SIZE){
cout<<"full"<<endl;
close(sock);
}
else{
fds[i].fd = sock;
fds[i].events = POLLIN;//设置为读事件
fds[i].revents = 0;
}
}
//删除一个不用监视的文件
void DelSock(int i){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
void Handler(){
for(int i = 0; i < SIZE; i++){
//用revents判断是否就绪
if(fds[i].revents & POLLIN){
if(fds[i].fd == _lsock){
//有连接来了,加入到fds中
int sock = Sock::Accept(_lsock);
if(sock >= 0){
cout <<"get a link..."<<endl;
AddSock2Fds(sock);
}
else{
cerr << "accept error"<<endl;
}
}
else{
//IO就绪
char buf[10240] ={0};
int n = recv(fds[i].fd, buf, sizeof(buf),0);
if(n > 0){
cout << "client# "<<buf<<endl;
}
else if(n == 0){
//对端关闭
cout<<"client close..."<<endl;
close(fds[i].fd);
DelSock(i);
}
else{
cerr << "recv error"<<endl;
close(fds[i].fd);
DelSock(i);
}
}
}
}
}
void Start(){
int timeout = -1;
while(1){
int num = poll(fds, SIZE, timeout);
if(num > 0){
Handler();
}
else if(num == 0){
//超时
cerr << "timeout..."<<endl;
}
else{
cerr << "poll error..."<<endl;
}
}
}
~PollServer(){
for(int i =0; i < SIZE; i++){
if(fds[i].fd != -1){
close(fds[i].fd);
}
}
}
};
#include "PollServer.hpp"
void Notice(){
cout << "Notice:\n\t"<<"please port"<<endl;
}
int main(int argc, char* argv[]){
if(argc != 2){
Notice();
exit(6);
}
PollServer *ps = new PollServer(atoi(argv[1]));
ps->InitServer();
ps->Start();
delete ps;
return 0;
}
epoll
我们之前认识了select与poll,他们两个或多或少都有缺陷,而我们epoll是针对select与poll的不足进行改进,不过它们都具有相同的功能,同时等待多个文件
按照 man 手册的说法 : 是为处理大批量句柄而作了改进的 poll .它是在 2.5.44 内核中被引进的 (epoll(4) is a new API introduced in Linux kernel 2.5.44)它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法 .
epoll的相关系统调用
我们的epoll拥有三个系统调用接口,这与select和poll不一样,这两个都是使用一个接口实现监视文件的输入与输出
epoll将输入与输出功能分开了,用两个接口,epoll_ctr和epoll_wait
epoll_creat
#include <sys/epoll.h>
int epoll_create(int size);
自从 linux2.6.8 之后, size 参数是被忽略的 .用完之后 , 必须调用 close() 关闭返回值:返回一个文件描述符,该文件描述符指向epoll模型,通过该文件描述符来对epoll模型进行管理
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件 , 而是在这里先注册要监听的事件类型 .第一个参数是 epoll_create() 的返回值 (epoll 的句柄 ).第二个参数表示动作,用三个宏来表示 .第三个参数是需要监听的 fd.第四个参数是告诉内核需要监听什么事
EPOLL_CTL_ADD :注册新的fd 到 epfd 中;EPOLL_CTL_MOD:修改已经注册的fd 的监听事件;EPOLL_CTL_DEL :从epfd 中删除一个 fd
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
EPOLLIN : 表示对应的文件描述符可以读 ( 包括对端 SOCKET 正常关闭 );EPOLLOUT : 表示对应的文件描述符可以写 ;EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 ( 这里应该表示有带外数据到来 );EPOLLERR : 表示对应的文件描述符发生错误 ;EPOLLHUP : 表示对应的文件描述符被挂断 ;EPOLLET : 将 EPOLL 设为边缘触发 (Edge Triggered) 模式 , 这是相对于水平触发 (Level Triggered) 来说的 .EPOLLONESHOT:只监听一次事件 , 当监听完这次事件之后 , 如果还需要继续监听这个 socket 的话 , 需要再次把这个socket 加入到 EPOLL 队列里
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数 events 是分配好的 epoll_event 结构体数组 .epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存 ).maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size.参数 timeout 是超时时间 ( 毫秒, 0 会立即返回, -1 是永久阻塞 ).如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时 , 返回小于 0 表示函数失败
epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件.这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来 ( 红黑树的插入时间效率是lgn ,其中 n 为树的高度 ).而所有添加到 epoll 中的事件都会与设备 ( 网卡 ) 驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.这个回调方法在内核中叫 ep_poll_callback, 它会将发生的事件添加到 rdlist 双链表中 .在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem元素即可.如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户 . 这个操作的时间复杂度是O(1).
我们来梳理一下epoll函数调用
调用epoll_creat
1.创建红黑树
2.创建回调机制
3.创建就绪队列
调用epoll_crt工作:
1.操作红黑树,删除/修改/增加红黑树的结点,红黑树的结点代表监视的事件
2.形成回调函数,当事件就绪调用回调函数
调用epoll_wait工作:
1.首先检测就绪队列是否为空
2.不为空,将就绪队列里的就绪事件信息拷贝到用户缓冲区,并且从用户缓冲区数组0号下标开始往后放
epoll的优点(和 select 的缺点对应)
接口使用方便 : 虽然拆分成了三个函数 , 但是反而使用起来更方便高效 . 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开数据拷贝轻量 : 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中 , 这个操作并不频繁( 而 select/poll 都是每次循环都要进行拷贝 )事件回调机制 : 避免使用遍历 , 而是使用回调函数的方式 , 将就绪的文件描述符结构加入到就绪队列中 ,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪 . 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响 .没有数量限制 : 文件描述符数目无上限
注意:epoll中没有使用内存映射机制
epoll工作方式
epoll有两种工作模式,水平触发LT与边缘触发ET
实际上select与poll只支持LT工作模式,epoll即支持LT,也支持ET工作模式
我们的这两种工作模式可以由下面的例子进行说明
你妈妈叫你吃饭。
你妈妈叫你一次,你没有动,你妈妈会继续一直叫你吃饭。对应LT工作模式。
你妈妈叫你一次,你没有动,你妈妈之后不会再叫你了。对应ET工作模式。
水平触发(Level Triggered)工作模式——LT
当 epoll 检测到 socket 上事件就绪的时候 , 可以不立刻进行处理 . 或者只处理一部分 .如上面的例子 , 由于只读了 1K 数据 , 缓冲区中还剩 1K 数据 , 在第二次调用 epoll_wait 时 , epoll_wait仍然会立刻返回并通知socket 读事件就绪 .直到缓冲区上所有的数据都被处理完 , epoll_wait 才不会立刻返回 .支持阻塞读写和非阻塞读写
边缘触发(Edge Triggered)工作模式——ET
当 epoll 检测到 socket 上事件就绪时 , 必须立刻处理 .如上面的例子 , 虽然只读了 1K 的数据 , 缓冲区还剩 1K 的数据 , 在第二次调用 epoll_wait 的时候 ,epoll_wait 不会再返回了 .也就是说 , ET 模式下 , 文件描述符上的事件就绪后 , 只有一次处理机会 .ET 的性能比 LT 性能更高 ( epoll_wait 返回的次数少了很多 ). Nginx 默认采用 ET 模式使用 epoll.只支持非阻塞的读写
对比LT和ET
LT 是 epoll 的默认行为 . 使用 ET 能够减少 epoll 触发的次数 . 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后 , 不会反复被提示就绪 , 看起来就比 LT 更高效一些 . 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话 , 其实性能也是一样的 .另一方面 , ET 的代码复杂程度更高了
epoll使用
Sock.hpp
#pragma once
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <sys/epoll.h>
#define BACKLOG 5
using namespace std;
class Sock{
public:
static int Socket(){
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr<<"socket error"<<endl;
exit(1);
}
return sock;
}
static void Bind(int sock, int port){
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htons(INADDR_ANY);
if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){
cerr << "bind error"<<endl;
exit(2);
}
}
static void Listen(int sock){
if(listen(sock,BACKLOG) < 0){
cerr << "listen error"<<endl;
exit(3);
}
}
static int Accept(int sock){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
return accept(sock, (struct sockaddr *)&peer, &len);
}
static void SetSockOpt(int sock){
int opt =1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
};
EpollServer.hpp
#pragma once
#include "Sock.hpp"
#define NUM 20
class Bucket{
public:
Bucket(int fd)
:_fd(fd)
,_pos(0)
{
_buff[0] = 0;
}
char _buff[20];//数据缓冲区
int _fd;//文件描述符
size_t _pos;//从pos位置开始保存到缓存区
};
class EpollServer{
private:
int _lsock;
int _port;
int _efd;
public:
EpollServer(int lsock = -1, int port = 8080, int efd = -1)
:_lsock(lsock)
,_port(port)
,_efd(efd)
{}
//将sock添加到epoll模型的红黑树中
void AddSock2Epoll(int sock, int event){
struct epoll_event ev;
ev.events = event;
if(sock == _lsock){
//连接套接字不需要开辟桶空间
ev.data.ptr = nullptr;
}
else{
//Bucket保存数据
ev.data.ptr = new Bucket(sock);
}
if(epoll_ctl(_efd, EPOLL_CTL_ADD, sock, &ev) < 0){
cerr << "Add error , close sock"<<endl;
close(sock);
}
}
void DelSock2Epoll(int sock){
epoll_ctl(_efd, EPOLL_CTL_DEL, sock, nullptr);
}
void InitServer(){
_lsock = Sock::Socket();
Sock::SetSockOpt(_lsock);
Sock::Bind(_lsock, _port);
Sock::Listen(_lsock);
//创建epoll模型
_efd = epoll_create(256);
if(_efd < 0){
cerr << "epoll create error"<<endl;
exit(5);
}
AddSock2Epoll(_lsock, EPOLLIN);
}
void Handler(struct epoll_event revent[], int n){
for(int i =0; i < n; i++){
if(revent[i].events & EPOLLIN){
//读
//连接套接字没有创建缓冲区
if(revent[i].data.ptr == nullptr){
//有连接
int sock = Sock::Accept(_lsock);
if(sock < 0){
cerr << "accpet error"<<endl;
}
else{
//加入Epoll模型
cout<<"get a link..."<<endl;
AddSock2Epoll(sock, EPOLLIN);
}
}
else{
Bucket *bk = (Bucket *)revent[i].data.ptr;
//IO就绪
int s = recv(bk->_fd, bk->_buff + bk->_pos, sizeof(bk->_buff)- bk->_pos, 0);
if(s > 0){
bk->_pos += s;
if(bk->_pos >= sizeof(bk->_buff)){
bk->_buff[sizeof(bk->_buff) -1] = 0;
}
else{
bk->_buff[bk->_pos] = 0;
}
cout << "client# "<<bk->_buff<<endl;
if(bk->_pos >= sizeof(bk->_buff)){
//修改事件为写就绪
revent[i].events = EPOLLOUT;
epoll_ctl(_efd, EPOLL_CTL_MOD, bk->_fd, &revent[i]);
}
}
else if(s == 0){
cerr << "client close..."<<endl;
close(bk->_fd);
DelSock2Epoll(bk->_fd);
//销毁bk开辟的空间,防止内存泄漏
delete bk;
}
else{
cerr << "recv error"<<endl;
}
}
}
else if(revent[i].events & EPOLLOUT){
//写
Bucket *bk = (Bucket *)revent[i].data.ptr;
bk->_buff[sizeof(bk->_buff) -1] = '\n';
send(bk->_fd, bk->_buff, sizeof(bk->_buff), 0);
//关闭连接
DelSock2Epoll(bk->_fd);
close(bk->_fd);
delete bk;
}
else{
//其它事件
}
}
}
void Start(){
struct epoll_event revent[NUM];
int timeout = -1;
while(1){
int n =epoll_wait(_efd, revent, NUM, timeout);
switch(n){
case -1:
cerr << "epoll wait error" <<endl;
break;
case 0:
cerr << "epoll timeout" <<endl;
break;
default:
Handler(revent,n);
break;
}
}
}
~EpollServer(){
close(_lsock);
close(_efd);
}
};
EpollServer.cc
#include"EpollServer.hpp"
void Notice(){
cout<<"Notice:\n\t"<<"please port"<<endl;
}
int main(int argc, char *argv[]){
if(argc != 2){
Notice();
exit(4);
}
EpollServer *es = new EpollServer(atoi(argv[1]));
es->InitServer();
es->Start();
delete es;
return 0;
}