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

如何使用 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事件结构体如下所示。对每一个被监听的描述符,你可以关联到一个整数或一个作为用户数据的指针。 

01typedef union epoll_data
02{
03  void        *ptr;
04  int          fd;
05  __uint32_t   u32;
06  __uint64_t   u64;
07} epoll_data_t;
08 
09struct 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. 
01static int
02create_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结构体如下: 

01struct 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描述符参数。 
01static int
02make_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 
003int
004main (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发现. 感谢你的评论。 :)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值