服务器之家:专注于服务器技术及软件下载分享
分类导航

Linux|Centos|Ubuntu|系统进程|Fedora|注册表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服务器之家 - 服务器系统 - Linux - IO多路复用之epoll全面总结(必看篇)

IO多路复用之epoll全面总结(必看篇)

2021-12-13 20:57Linux教程网 Linux

下面小编就为大家带来一篇IO多路复用之epoll全面总结(必看篇)。小编觉得挺不错的。现在就分享给大家。也给大家做个参考。一起跟随小编过来看看吧

1、基本知识

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

2、epoll接口

epoll操作过程需要三个接口,分别如下:

?
1
2
3
4
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1) int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

epoll_ctl_add:注册新的fd到epfd中;
epoll_ctl_mod:修改已经注册的fd的监听事件;
epoll_ctl_del:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

?
1
2
3
4
struct epoll_event {
 __uint32_t events; /* epoll events */
 epoll_data_t data; /* user data variable */
};

events可以是以下几个宏的集合:

epollin :表示对应的文件描述符可以读(包括对端socket正常关闭);

epollout:表示对应的文件描述符可以写;

epollpri:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

epollerr:表示对应的文件描述符发生错误;

epollhup:表示对应的文件描述符被挂断;

epollet: 将epoll设为边缘触发(edge triggered)模式,这是相对于水平触发(level triggered)来说的。

epolloneshot:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到epoll队列里

(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

3、工作模式

epoll对文件描述符的操作有两种模式:lt(level trigger)和et(edge trigger)。lt模式是默认模式,lt模式与et模式的区别如下:

lt模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

et模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

et模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比lt模式高。epoll工作在et模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

4、测试程序

编写一个服务器回射程序echo,练习epoll过程。

服务器代码如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
 
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
 
#define ipaddress  "127.0.0.1"
#define port    8787
#define maxsize   1024
#define listenq   5
#define fdsize   1000
#define epollevents 100
 
//函数声明
//创建套接字并进行绑定
static int socket_bind(const char* ip,int port);
//io多路复用epoll
static void do_epoll(int listenfd);
//事件处理函数
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd);
//读处理
static void do_read(int epollfd,int fd,char *buf);
//写处理
static void do_write(int epollfd,int fd,char *buf);
//添加事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//删除事件
static void delete_event(int epollfd,int fd,int state);
 
int main(int argc,char *argv[])
{
  int listenfd;
  listenfd = socket_bind(ipaddress,port);
  listen(listenfd,listenq);
  do_epoll(listenfd);
  return 0;
}
 
static int socket_bind(const char* ip,int port)
{
  int listenfd;
  struct sockaddr_in servaddr;
  listenfd = socket(af_inet,sock_stream,0);
  if (listenfd == -1)
  {
    perror("socket error:");
    exit(1);
  }
  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family = af_inet;
  inet_pton(af_inet,ip,&servaddr.sin_addr);
  servaddr.sin_port = htons(port);
  if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
  {
    perror("bind error: ");
    exit(1);
  }
  return listenfd;
}
 
static void do_epoll(int listenfd)
{
  int epollfd;
  struct epoll_event events[epollevents];
  int ret;
  char buf[maxsize];
  memset(buf,0,maxsize);
  //创建一个描述符
  epollfd = epoll_create(fdsize);
  //添加监听描述符事件
  add_event(epollfd,listenfd,epollin);
  for ( ; ; )
  {
    //获取已经准备好的描述符事件
    ret = epoll_wait(epollfd,events,epollevents,-1);
    handle_events(epollfd,events,ret,listenfd,buf);
  }
  close(epollfd);
}
 
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
  int i;
  int fd;
  //进行选好遍历
  for (i = 0;i < num;i++)
  {
    fd = events[i].data.fd;
    //根据描述符的类型和事件类型进行处理
    if ((fd == listenfd) &&(events[i].events & epollin))
      handle_accpet(epollfd,listenfd);
    else if (events[i].events & epollin)
      do_read(epollfd,fd,buf);
    else if (events[i].events & epollout)
      do_write(epollfd,fd,buf);
  }
}
static void handle_accpet(int epollfd,int listenfd)
{
  int clifd;
  struct sockaddr_in cliaddr;
  socklen_t cliaddrlen;
  clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
  if (clifd == -1)
    perror("accpet error:");
  else
  {
    printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
    //添加一个客户描述符和事件
    add_event(epollfd,clifd,epollin);
  }
}
 
static void do_read(int epollfd,int fd,char *buf)
{
  int nread;
  nread = read(fd,buf,maxsize);
  if (nread == -1)
  {
    perror("read error:");
    close(fd);
    delete_event(epollfd,fd,epollin);
  }
  else if (nread == 0)
  {
    fprintf(stderr,"client close.\n");
    close(fd);
    delete_event(epollfd,fd,epollin);
  }
  else
  {
    printf("read message is : %s",buf);
    //修改描述符对应的事件,由读改为写
    modify_event(epollfd,fd,epollout);
  }
}
 
static void do_write(int epollfd,int fd,char *buf)
{
  int nwrite;
  nwrite = write(fd,buf,strlen(buf));
  if (nwrite == -1)
  {
    perror("write error:");
    close(fd);
    delete_event(epollfd,fd,epollout);
  }
  else
    modify_event(epollfd,fd,epollin);
  memset(buf,0,maxsize);
}
 
static void add_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_add,fd,&ev);
}
 
static void delete_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_del,fd,&ev);
}
 
static void modify_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_mod,fd,&ev);
}

客户端也用epoll实现,控制stdin_fileno、stdout_fileno、和sockfd三个描述符,程序如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
 
#define maxsize   1024
#define ipaddress  "127.0.0.1"
#define serv_port  8787
#define fdsize    1024
#define epollevents 20
 
static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);
 
int main(int argc,char *argv[])
{
  int         sockfd;
  struct sockaddr_in servaddr;
  sockfd = socket(af_inet,sock_stream,0);
  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family = af_inet;
  servaddr.sin_port = htons(serv_port);
  inet_pton(af_inet,ipaddress,&servaddr.sin_addr);
  connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
  //处理连接
  handle_connection(sockfd);
  close(sockfd);
  return 0;
}
 
 
static void handle_connection(int sockfd)
{
  int epollfd;
  struct epoll_event events[epollevents];
  char buf[maxsize];
  int ret;
  epollfd = epoll_create(fdsize);
  add_event(epollfd,stdin_fileno,epollin);
  for ( ; ; )
  {
    ret = epoll_wait(epollfd,events,epollevents,-1);
    handle_events(epollfd,events,ret,sockfd,buf);
  }
  close(epollfd);
}
 
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
  int fd;
  int i;
  for (i = 0;i < num;i++)
  {
    fd = events[i].data.fd;
    if (events[i].events & epollin)
      do_read(epollfd,fd,sockfd,buf);
    else if (events[i].events & epollout)
      do_write(epollfd,fd,sockfd,buf);
  }
}
 
static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
  int nread;
  nread = read(fd,buf,maxsize);
    if (nread == -1)
  {
    perror("read error:");
    close(fd);
  }
  else if (nread == 0)
  {
    fprintf(stderr,"server close.\n");
    close(fd);
  }
  else
  {
    if (fd == stdin_fileno)
      add_event(epollfd,sockfd,epollout);
    else
    {
      delete_event(epollfd,sockfd,epollin);
      add_event(epollfd,stdout_fileno,epollout);
    }
  }
}
 
static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
  int nwrite;
  nwrite = write(fd,buf,strlen(buf));
  if (nwrite == -1)
  {
    perror("write error:");
    close(fd);
  }
  else
  {
    if (fd == stdout_fileno)
      delete_event(epollfd,fd,epollout);
    else
      modify_event(epollfd,fd,epollin);
  }
  memset(buf,0,maxsize);
}
 
static void add_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_add,fd,&ev);
}
 
static void delete_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_del,fd,&ev);
}
 
static void modify_event(int epollfd,int fd,int state)
{
  struct epoll_event ev;
  ev.events = state;
  ev.data.fd = fd;
  epoll_ctl(epollfd,epoll_ctl_mod,fd,&ev);
}

5、测试结果

IO多路复用之epoll全面总结(必看篇)

IO多路复用之epoll全面总结(必看篇)

IO多路复用之epoll全面总结(必看篇)

以上就是小编为大家带来的io多路复用之epoll全面总结(必看篇)全部内容了,希望大家多多支持服务器之家~

延伸 · 阅读

精彩推荐
  • Linuxlinux驱动程序开发详细介绍

    linux驱动程序开发详细介绍

    前提,一般来说内核代码的错误可能会引起一个用户进程的死亡,或者整个系统的瘫痪,更严重的后果,可能导致磁盘损伤~因此建议最好有一台实验机进行...

    Linux教程网5392019-12-17
  • LinuxLinux系统下无法卸载挂载的目录怎么办?

    Linux系统下无法卸载挂载的目录怎么办?

    我们在日常运维中经常性会遇到需要进行磁盘的扩容、卸载、挂载等操作,但是有时候这个系统上跑的应用并没有停止或者有其他的运维同事在操作这个目...

    今日头条10302020-12-30
  • Linuxlinux top命令详解

    linux top命令详解

    这篇文章主要介绍了linux top命令详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    sparkdev5622022-03-01
  • Linux在Linux系统中创建新的亚马逊AWS访问密钥的方法

    在Linux系统中创建新的亚马逊AWS访问密钥的方法

    如何在Linux系统中创建新的亚马逊AWS访问密钥?我在配置一个需要访问我的亚马逊AWS帐号的应用时被要求提供AWS访问密钥ID和秘密访问密钥,我怎样创建一个...

    Linux教程网6182019-10-30
  • Linuxssh 登录很慢该如何解决

    ssh 登录很慢该如何解决

    这篇文章主要介绍了ssh 登录很慢该如何解决的相关资料,这里提供了两种方法,DNS反向解析及关闭ssh的gssapi认证的解决办法,需要的朋友可以参考下...

    linuxeye9922021-12-16
  • Linux将 Linux 终端与 Nautilus 文件管理器结合起来

    将 Linux 终端与 Nautilus 文件管理器结合起来

    Nautilus 是 GNOME 桌面环境中的图形化文件浏览器。你可以使用它来访问和管理系统中的文件和文件夹。 尽管并非所有人都喜欢使用终端来管理文件和目录,...

    未知812023-08-08
  • Linuxlinux设置tomcat自启动的方法

    linux设置tomcat自启动的方法

    这篇文章主要介绍了linux设置tomcat自启动的方法,需要的朋友可以参考下...

    Linux教程网8512021-10-10
  • Linux理解 Linux/Unix 登录脚本的技巧

    理解 Linux/Unix 登录脚本的技巧

    有一些常见的情况,例如从Debian的包管理程序到Iaas的管理中,很多任务需要设置环境变量才能正常运行。 有时,程序通常只需要在 登陆时运行一次,例如...

    未知1042023-05-12