在epoll中,将相应节点挂上epoll树,除了events事件以外,还可以设置data的值,data是一个联合共用体union,提供了一个很好的成员变量,void *ptr
这个万能指针,相当于可以让我们传任何想要的值挂在树上,等有事件发生时,又会原封不动的把节点上的内容返回回来,以供我们操作,就给了我们无限操作的可能性。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
`void *ptr;`
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
Reactor 释义“反应堆”,是一种事件驱动机制。简单来看就是靠这个万能指针传递了含有函数指针和这个函数需要的参数等等的结构体,当相应fd发生事件时,执行fd对应的函数。相当于Reactor主动调用应用程序注册的接口,这些接口又称为“回调函数”。
Reactor是一种高并发服务器模型,是一种框架,一个概念,所以reactor没有一个固定的代码,可以有很多变种。
Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的 I/O 事件注册到一个中心 I/O 多路复用器
上,同时主线程/进程阻塞在多路复用器上;一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发
到对应的处理器
中。
Reactor 模型有三个重要的组件:
多路复用器:由操作系统提供,在 linux 上一般是 select, poll, epoll 等系统调用。
事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
事件处理器:负责处理特定事件的处理函数。
具体流程如下:
- 注册读就绪事件和相应的事件处理器;
- 事件分离器等待事件;
- 事件到来,激活分离器,分离器调用事件对应的处理器;
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
这里做一个简易的针对读事件的单线程reactor模型,根据每个人的想法不同,可以进行进一步的完善。
struct eventstruct{
int fd;
int (* callback )(int ,void *);
};
int accept_cb(int fd,void *arg){
int confd=accept(fd,NULL,NULL);
if(confd<0){
return -1;
}
printf("have new con\n");
struct eventstruct *ev= (struct eventstruct *)malloc(sizeof(struct eventstruct));
ev->fd=confd;
ev->callback=read_cb;
struct epoll_event temp;
temp.events=EPOLLIN;
temp.data.ptr=ev;
epoll_ctl(epofd,EPOLL_CTL_ADD,confd,&temp);
return 0;
}
int read_cb(int fd,void *arg){
memset(buf,0,sizeof(buf));
int n=read(fd,buf,sizeof(buf));
if(n<=0) {
printf("have con break\n");
epoll_ctl(epofd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
free(arg);
return -1;
}else {
printf("buf=[%s]\n",buf);
write(fd,"OK\n",4);
}
return 0;
}
以下是简易模型的完整代码:
int epofd;
char buf[1024]={0};
//set eventstruct
struct eventstruct{
int fd;
int (* callback )(int ,void *);
};
//set not socketfd callback
int read_cb(int fd,void *arg){
memset(buf,0,sizeof(buf));
int n=read(fd,buf,sizeof(buf));
if(n<=0) {
printf("have con break\n");
epoll_ctl(epofd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
free(arg);
return -1;
}else {
printf("buf=[%s]\n",buf);
write(fd,"OK\n",4);
}
return 0;
}
//set socketfd callback
int accept_cb(int fd,void *arg){
int confd=accept(fd,NULL,NULL);
if(confd<0){
return -1;
}
printf("have new con\n");
struct eventstruct *ev= (struct eventstruct *)malloc(sizeof(struct eventstruct));
ev->fd=confd;
ev->callback=read_cb;
struct epoll_event temp;
temp.events=EPOLLIN;
temp.data.ptr=ev;
epoll_ctl(epofd,EPOLL_CTL_ADD,confd,&temp);
return 0;
}
int main()
{
//socket()
int socketfd= socket(AF_INET,SOCK_STREAM,0);
if(socketfd<0){
return -1;
}
//bind()
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port= htons(8002);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
int bindret=bind(socketfd,(struct sockaddr *)&addr,sizeof(addr));
if(bindret<0){
return -2;
}
//listen()
int listenret=listen(socketfd,128);
if(listenret<0){
return -3;
}
//set epoll
epofd= epoll_create(1);
if(epofd<0){
return -1;
}
struct epoll_event node[1024];
memset(&node,0,sizeof(node));
struct epoll_event temp;
struct eventstruct *ev= (struct eventstruct*)malloc(sizeof(struct eventstruct));
temp.events=EPOLLIN;
ev->fd=socketfd;
ev->callback=accept_cb;
temp.data.ptr=ev;
int retc= epoll_ctl(epofd,EPOLL_CTL_ADD,socketfd,&temp);
if(retc<0) {
return -1;
}
while(1){
int nready= epoll_wait(epofd,node,1024,-1);
if(nready<0){
return -9;
}
for(int i=0;i<nready;i++){
struct eventstruct *snacks=(struct eventstruct*)node[i].data.ptr;
if(node[i].events & EPOLLIN){
snacks->callback(snacks->fd,snacks);
}
}
}
close(socketfd);
return 0;
}
注意:返回的节点的ptr的值只是指向一块地方地址,必须要将这块地址开辟一个堆区,不然的话被回收了之后ptr就指向的是一块不知道什么东西,之后的操作就会操控非法内存。
当与多线程多进程等结合时候,reactor模型效率就是质一般的飞跃。
Reactor 模型开发效率上比起直接使用 IO 复用要高,它通常是单线程的,设计目标是希望单线程使用一颗 CPU 的全部资源,但也有附带优点,即每个事件处理中很多时候可以不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定律,CPU 的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力,当程序需要使用多核资源时,Reactor 模型就会悲剧, 为什么呢?
如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开启多个反应堆,每个反应堆对应一颗 CPU 核心,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如 Nginx 这样的 http 静态服务器。