目录
2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
一,五种IO模型
1,阻塞IO
在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式。
2,非阻塞IO
在内核将数据未准备好之前,系统调用仍然会返回,并且返回EWOULDBLOCK错误码,非阻塞IO一般需要程序员以循环的方式反复读写文件描述符,这个过程称为轮询,这对CPU是极大的浪费,一般特定的场景使用。
3, 信号驱动IO
在内核将数据准备好以后,通过SIGIO信号通知应用程序进行IO操作
4,IO多路转接
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态。
5,异步IO
在内核将数据拷贝完成后,通知应用程序(而信号驱动是告诉应用程序何时开始拷贝数据).
小结:
任何IO过程中都包含两个步骤,第一是等待,第二是拷贝,而在实际应用的过程中,等待时间远远高于拷贝的过程。让IO更高效,最核心的办法就是让等待的时间尽可能的长。
二,同步通信和异步通信
同步和异步关心的是消息通信机制
1,同步,在系统调用时,在没有的得到结果之前,该调用不返回,直到调用返回结果。
2,异步,调用在发出以后,这个调用就直接返回了,没有返回结果,调用者通过信号,状态,来通知调用者。
//此同步非互斥和同步里面的同步
1,进程/线程同步也是进程/线程之间直接的制约关系
2,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候。
三,IO模型的设置
1,阻塞IO
一个文件描述符,默认都是阻塞IO。
2,非阻塞IO
通过fcntl设置文件描述符,函数原型如下
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同. 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).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
实现函数setnoblock()
void setnoblock(int fd)
{
int f1=fctl(fd,F_GETFL);
if(f1<0)
{
cerr<<"fcntl"<<endl;
return ;
}
fcntl(fd,f_SETFL,f1|O_NONBLOCK);
}
a,使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
b,然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数
1 #include<iostream>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 void setnoblock(int fd)
6 {
7 int f1=fcntl(fd,F_GETFD);
8 if(f1<0)
9 {
10 std::cerr<<"fcntl"<<std::endl;
11 return ;
12 }
13 fcntl(fd,F_SETFD,f1|O_NONBLOCK);
14
15 }
16 int main()
17 {
18 setnoblock(0);
19 while (1) {
20 char buf[1024] = {0};
21 ssize_t read_size = read(0, buf, sizeof(buf) - 1);
22 if (read_size < 0) {
23 std::cout<<"read"<<std::endl;
24 sleep(1);
25 continue;
26 }
27 std::cout<<buf<<std::endl;
28 }
29 return 0;
30 }
3,I/O多路转接之select
1,系统提供select函数来实现多路复用输入/输出模型.
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
select时间复杂度---》O(n)
2,函数原型
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
1,nfds,代表最大文件描述符+1。
2,rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;
3,参数timeout为结构timeval,用来设置select()的等待时间
a,参数timeout取值
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
b,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的全部位
3,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的第一个参数
4,select服务端举例
#include"sock.hpp"
#define NUM 1024
#define DFL_FD -1
namespace so_sever
{
class Select_Sever
{
private:
int listen_sock;
unsigned short port;
public:
Select_Sever(unsigned short _port):port(_port)
{}
//先初始化fd,起任务,填充select变量
void InitSelectSever()
{
listen_sock= so_Sock::so_sock::Socket();
so_Sock::so_sock::Bind(listen_sock,port);
so_Sock::so_sock::Listen(listen_sock);
}
void Run()
{
fd_set rfds;
int fd_array[NUM]={0};
//让数组中所有数据都变成-1,然后填充对应的监听套接字
clearArrar(fd_array,NUM,DFL_FD);
fd_array[0]=listen_sock;
for( ; ;)
{
//select时间也需要设置,输入输出型参数
struct timeval timeout={5,0};
//对所有的合法fd重新设置
int maxfd=DFL_FD;
FD_ZERO(&rfds);//对select中读描述符进行重新设置
//对文件描述符数组进行判断
for(int i=0;i<NUM;i++)
{
if(fd_array[i]==DFL_FD)
{
continue;
}
//合法的文件描述符
FD_SET(fd_array[i],&rfds);
if(fd_array[i]>maxfd)
{
maxfd=fd_array[i];
}
}
//select 阻塞等待
switch (select(maxfd+1,&rfds,nullptr,nullptr,/*&timeout*/ nullptr))
{
case 0:
std::cout<<"timeout....... "<<timeout.tv_sec<<std::endl;
break;
case -1:
std::cout<<"select error "<<std::endl;
break;
default:
// std::cout<<"select wait success"<<std::endl;
Hander(rfds,fd_array,NUM);
break;
}//end switch
}//end for
}
void Hander(const fd_set &rfds,int fd_array[],int num)
{
//读取套接字
//如何判断套接字已经等待成功 在fd数组里&&rfds里面这个已经存在
for(int i=0;i<num;i++)
{
if(fd_array[i]==DFL_FD)
{
continue;
}
//说明这个文件描述符已存在
if(FD_ISSET(fd_array[i],&rfds) && fd_array[i]==listen_sock)
{
//说明等待成功
//接受套接字等待成功,读事件还没有就绪
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//这里会不会阻塞,不会,已经有套接字加入到数组里面,
int sock=accept(fd_array[i],(struct sockaddr*)&peer,&len);
if(sock<0)
{
std::cout<<"accept error"<<std::endl;
continue;
}
//端口转换
uint16_t peer_port=htons(peer.sin_port);
//ip转换
std::string peer_ip=inet_ntoa(peer.sin_addr);
std::cout<<"get a new link "<<" port "<<peer_port<<" ip "<<peer_ip<<std::endl;
//走到这里 能否读取数据?? 不能 recv是IO,select只是等
//要将文件描述符添加到fd——fd_array
if(!AddFdTorray(fd_array,num,sock))
{
//说明没添加成功
close(sock);
std::cout << "select server is full, close fd : " << sock << std::endl;
}
}//end if
else
{
//说明可以读取数据了
if(FD_ISSET(fd_array[i],&rfds))
{
//是一个合法的fd,并且可以读取了
//是一个合法的fd,并且已经就绪了,是读数据事件就绪
//实现读写,会阻塞吗??绝对不会
char buffer[1024];
//能确定你读完了请求吗???
//如果我一条链接给你发了多个请求数据,但是每个都只有10字节, 粘包?
//如果没有读到一个完整的报文,数据可能丢失
//这里我们怎么保证自己能拿到完整的数据呢??
//1. 定制协议
//2. 还要给每一个sock定义对应的缓冲区
//ssize_t s=read(fd_array[i],buffer,sizeof(buffer)-1);
ssize_t s=recv(fd_array[i],buffer,sizeof(buffer)-1, 0);
if(s>0)
{
buffer[s]=0;
std::cout<<buffer<<std::endl;
}
else if(s == 0)
{
std::cout<<" client close "<<std::endl;
//对端关闭
close(fd_array[i]);
fd_array[i]=DFL_FD;//清除文件描述符
}
else
{
std::cout<<" recv error "<<std::endl;
close(fd_array[i]);
fd_array[i]=DFL_FD;
}
}
else
{
//todo
}
}//end if
}//end for
}
~Select_Sever()
{}
private:
void clearArrar(int fd_array[],int num,int default_fd)
{
for(int i=0;i<num;i++)
{
fd_array[i]=default_fd;
}
}
bool AddFdTorray(int fd_array[],int num,int sock)
{
for(int i=0;i<num;i++)
{
if(fd_array[i]==DFL_FD)
{
fd_array[i]=sock;
return true;
}
}
return false;
}
};
}
5,select缺点
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小
4,poll多路转接
poll==>时间复杂度O(n),
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
poll与select函数基本一致,只是把事件集合封装了一下,使得提前处理参数,后续也不用每次轮询时查找。
#include"sock.hpp"
#include<poll.h>
namespace Poll_etta
{
class poll_sever
{
private:
int listen_sock;
unsigned short port;
public:
poll_sever(int _port):port(_port)
{}
//初始化
void InitSever()
{
listen_sock=so_Sock::so_sock::Socket();
so_Sock::so_sock::Bind(listen_sock,port);
so_Sock::so_sock::Listen(listen_sock);
}
//RUN 任务
void Run()
{
struct pollfd rfds[64];
//初始化参数
for(int i=0;i<64;i++)
{
rfds[i].fd=-1;
rfds[i].events=0; //我所关心的事件
rfds[i].revents=0; //操作系统对我关心的事件 做出回应
}
//填充我所关心的事件
rfds[0].fd=listen_sock;
rfds[0].events=POLLIN;//关心读事件
rfds[0].revents=0;//内核填充
for(; ;)
{
switch(poll(rfds,64,-1))
{
case 0:
std::cout<<"time out"<<std::endl;
break;
case -1:
std::cerr<<"poll error"<<std::endl;
break;
default:
//处理逻辑,有事件到来
for(int i=0;i<64;i++)
{
if(rfds[i].fd==-1)
{
continue;
}
if(rfds[i].revents&POLLIN)
{
//能accept吗 不能 要填充就绪事件
if(listen_sock==rfds[i].fd)
{
std::cout<<" get a new link"<<std::endl;
}
else
{
//recv数据
}
}
}
break;
}
}
}
~poll_sever()
{}
};
}
5,epoll多路转接
手册上说:为处理大批量句柄而作改进的POLL,
其主要有三个函数接口。
1,int epoll_create(int size)
创建出一个epoll的句柄,size一般被忽略。
调用完成以后,必须close掉。
2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数.
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注 册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事.
第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构如下:
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.
3.epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
参数events是分配好的epoll_event结构体数组. epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个
events数组中,不会去帮助我们在用户态中分配内存). maxevents告之内核这个events有多 大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小 于0表示函数失败
6.epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有 两个成员与epoll的使用方式密切相关。
每一个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_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪
epoll的优点(和 select 的缺点对应)
1,接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开 数据拷贝轻量: 只在合适的时候调用 2,EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)
3,事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.
4,没有数量限制: 文件描述符数目无上限
7,epoll代码示例
#include"sock.hpp"
#include<sys/epoll.h>
#define MAX_NUM 64
namespace ns_epoll
{
class EpollSever
{
private:
int epfd; //epoll_ctl 的第一个参数,表示当前有几个文件描述符存在epoll里面
int listen_sock;
uint16_t port;
public:
//构造
EpollSever(uint16_t _port):port(_port)
{}
//初始化套接字
void InitSever()
{
listen_sock=so_Sock::so_sock::Socket();
so_Sock::so_sock::Bind(listen_sock,port);
so_Sock::so_sock::Listen(listen_sock);
//监听完毕,打印出来kankan
std::cout<<"deBug test "<<" Listen sock "<<listen_sock<<std::endl;
//初始化epoll函数的第一个参数
if((epfd=epoll_create(256))<0)
{
//创建失败
std::cout<<"epoll create fail"<<std::endl;
exit(4);
}
}
void AddEvent(int sock,uint16_t event)
{
struct epoll_event ev;
ev.events=0;//初始化
ev.events|=event;
ev.data.fd=sock;
//添加等待队列失败,继续等待
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0)
{
std::cout<<" epoll_ctl add fail "<<" sock "<<sock<<std::endl;
}
}
void DeleteEvent(int sock)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0)
{
std::cout<<" delete events fail "<<std::endl;
}
}
//事件跑起来
void Run()
{
//走到这里,至少有一个套接字,把套接字加到等待队列中,让epoll_wait “等”个文件描述符就绪
AddEvent(listen_sock,EPOLLIN);
int timeout=-1;
struct epoll_event revs[MAX_NUM];//定义最大等待数
//循环等待
for(;;)
{
//返回值代表有几个事件准备好
int num=epoll_wait(epfd,revs,MAX_NUM,timeout);
if(num>0)
{
//说明等待成功,但是那个文件描述符就绪 不知道 遍历
for(int i=0;i<MAX_NUM;i++)
{
int sock = revs[i].data.fd;
if(revs[i].events & EPOLLIN)//读事件就绪
{
if(sock==listen_sock)
{
//连接事件就绪
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sk=accept(sock,(struct sockaddr*)&peer,&len);
if(sk<0)
{
std::cout<<"accept fail "<<std::endl;
continue;
}
//得到一个新连接
std::cout<<"get a new link "<<std::endl;
AddEvent(sk,EPOLLIN);
}
//彻底就绪,直接读取文件
else
{
char buffer[1024];
ssize_t s=recv(sock,buffer,sizeof(buffer)-1,0);
if(s>0)
{
buffer[s]=0;
std::cout<<buffer<<std::endl;
}
else
{
std::cout<<"Client close "<<std::endl;
close(sock);
//移除这个套接字在epfd中
DeleteEvent(sock);
}
}
}
else if(revs[i].events&EPOLLOUT)
{
// todo
}
else
{
//do some thing
}
}
}
else if(num==0)
{
std::cout<<" time out"<<std::endl;
}
else
{
std::cout<<" epoll error "<<std::endl;
}
}
}
~EpollSever()
{
if(listen_sock>=0)close(listen_sock);
if(epfd>=0)close(epfd);
}
};
}