如何使用 epoll? 一个 C 语言实例
epoll 是在 Linux 2.6 才引进的,而且它并不适用于其它 Unix-like 系统。它提供了一个与select 和 poll 函数相似的功能:
- select 可以在某一时间监视最大达到 FD_SETSIZE 数量的文件描述符, 通常是由在 libc 编译时指定的一个比较小的数字。
- poll 在同一时间能够监视的文件描述符数量并没有受到限制,即使除了其它因素,更加的是我们必须在每一次都扫描所有通过的描述符来检查其是否存在己就绪通知,它的时间复杂度为 O(n) ,是缓慢的。
一个 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 | }; |
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为非阻塞的函数。 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们