初始epoll
- epoll是为处理大批量句柄而做改进的poll(句柄类似遥控器的作用)
- epoll几乎结合了poll的所有优点,并将poll的缺点加以改进.
epoll相关的系统调用
- epoll的使用三个过程:
* 调用epoll_create创建一个epoll句柄。
* 调用epoll_ctl,将要监控的文件描述符进行注册。(用户告诉操作系统)
* 调用epoll_wait,等待文件描述符就绪。(操作系统告诉用户)
- epoll_create
#include<sys/epoll.h>
int epoll_create(int size)
创建一个epoll句柄,也就是在内核创建一个文件描述符集合,也就是epoll对象
size参数是被省略的,但是为了向前兼容,一般不能省
- epoll_ctl
int epoll_ctl(int epfd , int op , int fd , struct epoll_event *event)
功能:epoll的事件注册函数
参数说明:
1>epfd:是epoll的句柄,也就是epoll_creat的返回值
2>op表示动作,有三个宏
* EPOLL_CTL_ADD:注册新的fd到epfd中.
* EPOLL_CTL_MOD:修改已经注册的fd的监听事件.
* EPOLL_CTL_DEL:从epfd中删除一个fd.
3>fd : 表示需要监听的fd
4>event : 告诉内核需要监听什么事
- struct epoll_event结构:
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 */ } __attribute__ ((__packed__));
- events取值
- events取值
事件 描述
EPOLLIN 对应的文件描述符可读
EPOLLOUT 对应的文件描述符可写
EPOLLPRI 对应的文件描述符有紧急的数据可以读
EPOLLERR 对应的文件描述符发生错误
EPOLLHUP 对应的文件描述符被挂断
EPOLLET 将epoll设置为边缘触发模式
EPOLLONESHOT 只监听一次事件
- epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
功能:收集epoll监控的事件中已发生的事件。
* events是已经分配好的epoll_event结构体数组。
* epoll将会把发生的事件赋值到events数组中。(内核只负责将数据复制到这个数组中,不会帮助我们在用户态中分配内存)。
* maxevents的值告诉内核events的大小,但不能超过epoll_create()中的size。
* timeout是超时时间(0立即返回,-1阻塞)。
* 如果函数调用成功,返回对应I/O上已准备好的文件描述符个数(0表示超时,小于0函数失败)。
epoll的工作原理
1.创建一个红黑树
红黑树结点内容,保存了用户想要告诉操作系统要监控的哪些文件描述符上的哪些事件。
2.从驱动到操作系统的回调机制
回调机制不需要操作系统一直在等,在事件就绪时,驱动会告诉操作系统,有事件就绪了,操作系统就会处理眼前的事件。这个回调机制在内核中称为epollcallback,它将发生的事件添加到rdlist
3.创建一个就绪队列
在事件就绪后,操作系统将对应的文件描述符上的事件的结点放在就绪队列中,由用户检查就绪队列,来判断是否有事件就绪。
- 当一个进程调用epoll_create时,linux内核会创建一个eventpoll结构体,这个结构体中有两个成员.
struct eventpoll{
struct rb_root rbr;//红黑树根节点,这棵树存储着所有添加到epoll中需要监控的事件
struct list_head rdlist;//存放epoll_wait返回给用户的满足条件的事件
}
- 所有通过epoll_ctl添加到epoll中的事件都会与设备网卡建立回调关系,即当响应的事件发生时会调用这个回调方法
- 回调方法会将,发生的事件添加到链表中.
- 对于epoll的每一个事件,都会建立一个epitem结构体,当调用epoll_wait时,只要检查eventpoll对象中的链表中是否有epitem元素即可,如果链表不为空,就把发生的事件复制到用户态,同时将事件返回给用户.
epoll的优点
- 文件描述符无上限。通过epoll_ctl()注册一个文件描述符,内核中用红黑树管理所要监控的文件描述符。
- 事件的就绪方式。一旦被监听的文件描述符就绪,内核会采用类似于callback的回调机制,激活该文件描述符,随着文件描述符的增加,也不会影响判定就绪的性能.
- 维护一个就绪队列。当文件描述符就绪,就回去被放到内核中的一个就绪队列,调用epoll_wait获取就绪文件描述符时,只需要取就绪队列中的元素就可以了,时间复杂度是O(1)。
epoll的工作方式
扫描二维码关注公众号,回复:
1880982 查看本文章
- 水平触发(LT)
* 只要有数据,操作系统就会通知用户,epoll会一直处于触发状态。
* epoll可以对数据不做立刻的处理。
* 直到缓冲区上的数据都被处理完,epoll_wait才会返回。
* 支持阻塞读写和非阻塞读写。
- 边缘触发(ET)
* 当epoll检测到socket上事件就绪时,必须立刻处理。
* 文件描述符上的事假就绪后,只有一次处理机会。
* ET的性能相对LT高,但是对用户的要求更高。
* Nginx默认下采用ET模式。
* 只支持非阻塞的读写。
epoll---水平触发
epoll_server.c
1 #include<stdio.h>
2 #include<sys/socket.h>
3 #include<sys/epoll.h>
4 #include<netinet/in.h>
5 #include<arpa/inet.h>
6 #include<stdlib.h>
7 #include<unistd.h>
8 #include<string.h>
9 typedef struct sockaddr_in sockaddr_in;
10 typedef struct sockaddr sockaddr;
11 typedef struct epoll_event epoll_event;
12 int Server_Init(char *ip,short port){
13 int fd = socket(AF_INET,SOCK_STREAM,0);
14 if(fd<0){
15 perror("socket");
16 return 1;
17 }
18 sockaddr_in server;
19 server.sin_family = AF_INET;
20 server.sin_addr.s_addr = inet_addr(ip);
21 server.sin_port = htons(port);
22 int ret = bind(fd,(sockaddr*)&server,sizeof(server));
23 if(ret<0){
24 perror("bind");
25 return -1;
26 }
27 ret = listen(fd,5);
28 if(ret<0){
29 perror("listen");
30 return -1;
31 }
32 return fd;
33 }
34 void ProcessListen_Socket(int epoll_fd,int listen_sock){
35 //调用accept()来获取new_sock
36 sockaddr_in client;
37 socklen_t len = sizeof(client);
38 int new_sock = accept(listen_sock,(sockaddr*)&client,&len);
39 if(new_sock<0){
40 perror("accept");
41 return ;
42 }
43 //把new_sock加入到epoll中
44 epoll_event event;
45 event.events = EPOLLIN;
46 event.data.fd = new_sock;
47 int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
48 if(ret<0){
49 perror("epoll_ctl");
50 return;
51 }
52 printf("[client %d] connect\n",new_sock);
53 return ;
54 }
55 void ProcessNew_Socket(int epoll_fd,int new_sock){
56 //1,对new_sock中读数据
57 char buf[1024] = {0};
58 ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
59 if(read_size<0){
60 perror("read");
61 return;
62 }
63 if(read_size==0){
64 printf("read done\n");
65 //1,关闭对应的客户端文件描述符
66 close(new_sock);
67 //2,把这个文件描述符从epoll中删除掉
68 epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
69 printf("[client %d] disconnect!\n",new_sock);
70 return ;
71 }
72 //2,处理数据,写会客户端
73 printf("[client %d] say %s\n",new_sock,buf);
74 write(new_sock,buf,strlen(buf));
75 return;
76 }
77 int main(int argc,char *argv[]){
78 if(argc!=3){
79 printf("Usage ./server [ip] [port]\n");
80 return 1;
81 }
82 // 1.初始化服务器,创建listen_sock
83 int listen_sock = Server_Init(argv[1],atoi(argv[2]));
84 if(listen_sock<0){
85 printf("Server_Init failed\n");
86 return 1;
87 }
88 printf("Server_Init Ok\n");
89 // 2,创建epoll对象,当一个进程调用epoll_create方法时,linux内核会创建一个eventpoll结构体,这个结构体
90 // 中有两个成员.
91 int epoll_fd = epoll_create(10) ;
92 if(epoll_fd<0){
93 perror("epoll_create failed\n");
94 return 1;
95 }
96 // 3,把listen_sock添加到epoll对象中去
97 epoll_event event;
98 event.events = EPOLLIN; //表示对应的文件描述符可以读
99 event.data.fd = listen_sock;
100 int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);//往红黑树中添加键值对,文件描述符为keyevent作为val>
101 if(ret<0){
102 perror("epoll_ctl");
103 return 1;
104 }
105
106 // 4,进入循环,等待文件描述符就绪
107 while(1){
108 struct epoll_event output_event[100];//结构体数组,提供epoll_wait放置返回值的缓冲区
109 int nfds = epoll_wait(epoll_fd,output_event,100,-1);
110 //-1表示阻塞等待
111 //epoll_wait 返回值是已经就绪了的文件描述符,并且通过event带回,也就是内存中的就绪队列
112 if(nfds<0){
113 perror("epoll_wait");
114 continue;
115 }
116 int i=0;
117 for(;i<nfds;i++){
118 if(output_event[i].data.fd==listen_sock){
119 // a>就绪的文件描述符是listen_sock
120 ProcessListen_Socket(epoll_fd,listen_sock) ;
121 }
122 else{
123 // b>就绪的文件描述符是new_sock
124 ProcessNew_Socket(epoll_fd,output_event[i].data.fd);
125 }
126 }
127 }
128 return 0;
129 }