您的位置:首页 > 其它

如何使用 epoll? 一个 C 语言实例

2013-03-11 16:54 716 查看

如何使用 epoll? 一个 C 语言实例

通常的网络服务器实现, 是对每一个连接使用一个单独的线程或进程。对高性能应用而言,由于需要同时处理非常多的客户请求, 所以这种方式并不能工作得很好,因为诸如资源使用和上下文切换所需的时间影响了在一时间内对多个客户端进行处理。另一个可选的途径是在一个单独的线程里采用非阻塞的I/O, 这样当可以从一个socket中读取或写入更多数据时,由一些已经准备就绪的通知方式来告知我们。这篇文章介绍Linux 的 epoll方法, 它是Linux上最好的就绪通知方式。我们会写一个用C语言的TCP服务器的完全实现的简单程序。假设你已有C编程的经验,知道在Linux
下编译和运行程序, 并且会用 manpages 来查看所使用的 C 函数。 

epoll 是在 Linux 2.6 才引进的,而且它并不适用于其它 Unix-like 系统。它提供了一个与select 和 poll 函数相似的功能: 
select 可以在某一时间监视最大达到 FD_SETSIZE 数量的文件描述符, 通常是由在 libc 编译时指定的一个比较小的数字。 
poll 在同一时间能够监视的文件描述符数量并没有受到限制,即使除了其它因素,更加的是我们必须在每一次都扫描所有通过的描述符来检查其是否存在己就绪通知,它的时间复杂度为 O(n) ,是缓慢的。 

epoll 没有以上所示的限制,并且不用执行线性扫描。因此, 它能有更高的执行效率且可以处理大数量的事件。

一个 epoll 实例可以通过返回epoll 实例的 epoll_create 或者 epoll_create1 函数来创建。 epoll_ctl 是用来在epoll实例中 添加/删除 被监视的文件描述符的。 epoll_wait是用来等待所监听描述符事件的,它会阻塞到事件到达。 可以在 manpages上查看更多信息。

当描述符被添加到epoll实例中, 有两种添加模式: level triggered(水平触发) 和 edge triggered(边沿触发) 。 当使用 level triggered 模式并且数据就绪待读, epoll_wait总是会返加就绪事件。如果你没有将数据读取完, 并且调用epoll_wait 在epoll 实例上再次监听这个描述符, 由于还有数据是可读的,它会再次返回。在 edge triggered 模式时, 你只会得一次就绪通知。 如果你没有将数据读完, 并且再次在 epoll实例上调用 epoll_wait
, 由于就绪事件已经被发送所以它会阻塞。 
传递到 epoll_ctl 的epoll事件结构体如下所示。对每一个被监听的描述符,你可以关联到一个整数或一个作为用户数据的指针。 

01
typedef
union
epoll_data
02
{
03
  
void
       
*ptr;
04
  
int
         
fd;
05
  
__uint32_t   u32;
06
  
__uint64_t   u64;
07
} epoll_data_t;
08
 
09
struct
epoll_event
10
{
11
  
__uint32_t   events;
/* Epoll events */
12
  
epoll_data_t data;  
/* User data variable */
13
};
马上实践写代码。我们会实现一个小的TCP服务器,它会将所有SOCKET上收到的数据输出到标准输出。 首先写一个 create_and_bind() 函数,它创建并绑定一个TCP socket. 

01
static
int
02
create_and_bind (
char

*port)
03
{
04
  
struct

addrinfo hints;
05
  
struct

addrinfo *result, *rp;
06
  
int

s, sfd;
07
 
08
  
memset

(&hints, 0,
sizeof
(
struct
addrinfo));
09
  
hints.ai_family = AF_UNSPEC;    
/* Return IPv4 and IPv6 choices */
10
  
hints.ai_socktype = SOCK_STREAM;
/* We want a TCP socket */
11
  
hints.ai_flags = AI_PASSIVE;    
/* All interfaces */
12
 
13
  
s = getaddrinfo (NULL, port, &hints, &result);
14
  
if

(s != 0)
15
    
{
16
      
fprintf

(stderr,
"getaddrinfo: %s\n"
, gai_strerror (s));
17
      
return

-1;
18
    
}
19
 
20
  
for

(rp = result; rp != NULL; rp = rp->ai_next)
21
    
{
22
      
sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
23
      
if

(sfd == -1)
24
        
continue
;
25
 
26
      
s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
27
      
if

(s == 0)
28
        
{
29
          
/* We managed to bind successfully! */
30
          
break
;
31
        
}
32
 
33
      
close (sfd);
34
    
}
35
 
36
  
if

(rp == NULL)
37
    
{
38
      
fprintf

(stderr,
"Could not bind\n"
);
39
      
return

-1;
40
    
}
41
 
42
  
freeaddrinfo (result);
43
 
44
  
return

sfd;
45
}
create_and_bind函数包含了一种可移植方式来获取IPv4或IPv6套接字的标准代码段。它接受一个port的字符串参数,port是从argv[1]中传入的。其中,getaddrinfo函数返回一群addrinfo到result,其中它们跟传入的hints参数是兼容的。 addrinfo结构体如下: 

01
struct
addrinfo
02
{
03
  
int
             
ai_flags;
04
  
int
             
ai_family;
05
  
int
             
ai_socktype;
06
  
int
             
ai_protocol;
07
  
size_t
          
ai_addrlen;
08
  
struct

sockaddr *ai_addr;
09
  
char
           
*ai_canonname;
10
  
struct

addrinfo *ai_next;
11
};
我们依次遍历这些结构体并用其来创建结构体,直到我们可以同时创建和绑定到socket。如果我们成功,create_and_bind() 会返回一个socket描述符。失败则返回 -1. 

接下来,我们写一个用来设置socket为非阻塞的函数。 make_socket_non_blocking() 设置 O_NONBLOCK 标志给传入的sfd描述符参数。 

01
static
int
02
make_socket_non_blocking (
int

sfd)
03
{
04
  
int

flags, s;
05
 
06
  
flags = fcntl (sfd, F_GETFL, 0);
07
  
if

(flags == -1)
08
    
{
09
      
perror

(
"fcntl"
);
10
      
return

-1;
11
    
}
12
 
13
  
flags |= O_NONBLOCK;
14
  
s = fcntl (sfd, F_SETFL, flags);
15
  
if

(s == -1)
16
    
{
17
      
perror

(
"fcntl"
);
18
      
return

-1;
19
    
}
20
 
21
  
return

0;
22
}
现在,有一个包含事件循环的main()函数,下面就是代码:

001
#define MAXEVENTS 64
002
 
003
int
004
main (
int

argc,
char
*argv[])
005
{
006
  
int

sfd, s;
007
  
int

efd;
008
  
struct

epoll_event event;
009
  
struct

epoll_event *events;
010
 
011
  
if

(argc != 2)
012
    
{
013
      
fprintf

(stderr,
"Usage: %s [port]\n"
, argv[0]);
014
      
exit

(EXIT_FAILURE);
015
    
}
016
 
017
  
sfd = create_and_bind (argv[1]);
018
  
if

(sfd == -1)
019
    
abort

();
020
 
021
  
s = make_socket_non_blocking (sfd);
022
  
if

(s == -1)
023
    
abort

();
024
 
025
  
s = listen (sfd, SOMAXCONN);
026
  
if

(s == -1)
027
    
{
028
      
perror

(
"listen"
);
029
      
abort

();
030
    
}
031
 
032
  
efd = epoll_create1 (0);
033
  
if

(efd == -1)
034
    
{
035
      
perror

(
"epoll_create"
);
036
      
abort

();
037
    
}
038
 
039
  
event.data.fd = sfd;
040
  
event.events =EPOLLIN | EPOLLET;
041
  
s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
042
  
if

(s == -1)
043
    
{
044
      
perror

(
"epoll_ctl"
);
045
      
abort

();
046
    
}
047
 
048
  
/* Buffer where events are returned */
049
  
events =

calloc
(MAXEVENTS,

sizeof
event);
050
 
051
  
/* The event loop */
052
  
while

(1)
053
    
{
054
      
int

n, i;
055
 
056
      
n = epoll_wait (efd, events, MAXEVENTS, -1);
057
      
for

(i = 0; i < n; i++)
058
    
{
059
      
if

((events[i].events & EPOLLERR) ||
060
              
(events[i].events & EPOLLHUP) ||
061
              
(!(events[i].events & EPOLLIN)))
062
        
{
063
              
/* An error has occured on this fd, or the socket is not
064
                 
ready for reading (why were we notified then?) */
065
          
fprintf

(stderr,
"epoll error\n"
);
066
          
close (events[i].data.fd);
067
          
continue
;
068
        
}
069
 
070
      
else

if
(sfd == events[i].data.fd)
071
        
{
072
              
/* We have a notification on the listening socket, which
073
                 
means one or more incoming connections. */
074
              
while

(1)
075
                
{
076
                  
struct

sockaddr in_addr;
077
                  
socklen_t in_len;
078
                  
int

infd;
079
                  
char

hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
080
 
081
                  
in_len =
sizeof
in_addr;
082
                  
infd = accept (sfd, &in_addr, &in_len);
083
                  
if

(infd == -1)
084
                    
{
085
                      
if

((
errno
== EAGAIN) ||
086
                          
(
errno

== EWOULDBLOCK))
087
                        
{
088
                          
/* We have processed all incoming
089
                             
connections. */
090
                          
break
;
091
                        
}
092
                      
else
093
                        
{
094
                          
perror

(
"accept"
);
095
                          
break
;
096
                        
}
097
                    
}
098
 
099
                  
s = getnameinfo (&in_addr, in_len,
100
                                   
hbuf,
sizeof
hbuf,
101
                                   
sbuf,
sizeof
sbuf,
102
                                   
NI_NUMERICHOST | NI_NUMERICSERV);
103
                  
if

(s == 0)
104
                    
{
105
                      
printf
(
"Accepted connection on descriptor %d "
106
                             
"(host=%s, port=%s)\n"
, infd, hbuf, sbuf);
107
                    
}
108
 
109
                  
/* Make the incoming socket non-blocking and add it to the
110
                     
list of fds to monitor. */
111
                  
s = make_socket_non_blocking (infd);
112
                  
if

(s == -1)
113
                    
abort

();
114
 
115
                  
event.data.fd = infd;
116
                  
event.events =EPOLLIN | EPOLLET;
117
                  
s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
118
                  
if

(s == -1)
119
                    
{
120
                      
perror

(
"epoll_ctl"
);
121
                      
abort

();
122
                    
}
123
                
}
124
              
continue
;
125
            
}
126
          
else
127
            
{
128
              
/* We have data on the fd waiting to be read. Read and
129
                 
display it. We must read whatever data is available
130
                 
completely, as we are running in edge-triggered mode
131
                 
and won't get a notification again for the same
132
                 
data. */
133
              
int

done = 0;
134
 
135
              
while

(1)
136
                
{
137
                  
ssize_t count;
138
                  
char

buf[512];
139
 
140
                  
count = read (events[i].data.fd, buf,
sizeof
buf);
141
                  
if

(count == -1)
142
                    
{
143
                      
/* If errno == EAGAIN, that means we have read all
144
                         
data. So go back to the main loop. */
145
                      
if

(
errno
!= EAGAIN)
146
                        
{
147
                          
perror

(
"read"
);
148
                          
done = 1;
149
                        
}
150
                      
break
;
151
                    
}
152
                  
else

if
(count == 0)
153
                    
{
154
                      
/* End of file. The remote has closed the
155
                         
connection. */
156
                      
done = 1;
157
                      
break
;
158
                    
}
159
 
160
                  
/* Write the buffer to standard output */
161
                  
s = write (1, buf, count);
162
                  
if

(s == -1)
163
                    
{
164
                      
perror

(
"write"
);
165
                      
abort

();
166
                    
}
167
                
}
168
 
169
              
if

(done)
170
                
{
171
                  
printf

(
"Closed connection on descriptor %d\n"
,
172
                          
events[i].data.fd);
173
 
174
                  
/* Closing the descriptor will make epoll remove it
175
                     
from the set of descriptors which are monitored. */
176
                  
close (events[i].data.fd);
177
                
}
178
            
}
179
        
}
180
    
}
181
 
182
  
free

(events);
183
 
184
  
close (sfd);
185
 
186
  
return

EXIT_SUCCESS;
187
}
main() 首先调用 create_and_bind()来新建一个socket。然后将其设置为非阻塞,再调用 listen (2)。之后,我们新建一个epoll实例inefd,并将监听套接字sfd以采用边沿触发的方式加入它,用以监听输入事件。

在外面的while循环是主要的事件循环。它调用epoll_wait(2),它所在线程以阻塞的方式来等待事件的到来。当事件就绪,epoll_wait(2)在其epoll_event类型的参数中返回相应的事件。

当我们添加新的传入连接,当他们终止时我们删除现有的连接,epoll 的实例 inefdis 的事件循环不断更新。

当事件的状态为可用的时候,他们有以下三种类型:

错误:当错误情况发生时,或者事件是不是一个有关数据可以被读取的通知,我们只需关闭相关的描述符。关闭描述符会自动移除其 epoll instanceefd。

新的连接:当监听到 descriptorsfdis 已经准备好用于读取的时候,这意味着已经到达一个或多个新的连接。当有新连接时,accept(2)连接,打印关于连接的信息,使传入的 socket 不被阻断,并将其添加到 epoll instanceefd 监听事件。

客户端数据:当数据在客户端描述符上为可读状态,我们在 read(2) 中使用 while 循环来读去存储在512位数据块中的数据。这是因为我们现在要读取所有可用的数据,在 edge-triggered 模式下,我们不会进一步获取事件描述符。使用
write(2) 将读取的数据被写入到 stdout (fd=1),如果 read(2) 返回 0,这意味着到达了一个 EOF(End of File),这时我们就可以断开与客户端的连接。如果
read(2) 返回 -1,anderrnois 设置为 EAGAIN,这意味着该事件所有的数据已读完,我们可以返回主循环了。

就是这样,它在一个循环中一遍又一遍地执行,在监听的集合中添加和删除描述。
下载 epoll-example.c 代码.

更新1: 水平和边缘触发的定义被错误的颠倒使用(尽管代码是正确的)。首先被Reddit用户bodski发现. 现在文章已经修正。 我应该在发布之前校读一次。向读者致歉并感谢你们指出错误。 :)

更新2: 代码被改为直到提示将被阻塞时才执行 accept(2) ,这样如果多个连接到达,我们可以接收全部请求。首先被Reddit用户cpitchford发现. 感谢你的评论。 :)

本文地址:http://www.oschina.net/translate/how-to-use-epoll-a-complete-example-in-c

原文地址:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接

我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐