How to use epoll? A complete example in C

本文介绍Linux epoll技术在构建高性能TCP服务器时的应用,通过非阻塞IO和可读通知机制优化资源使用和提高并发处理能力,提供与select和poll功能相比更高效的事件监听方式。

网络服务器传统上对每个连接用一个单独的进程或线程来实现.对于高性能应用需要同时处理大量客户端,这种方法工作显然不好,因为如此的资源使用和上下文切换时间影响了在某一时刻并发处理客户端的能力.一个可选的方法是在一个单线程用非阻塞IO,用一些可读方法来通知你在一个socket上可读或者可写.

这篇文章介绍linux 的epoll,它是linux下最好的可读通知.我们将写一个简单的完整TCP服务器实现.我想你已经有C语言编程经历,知道如何在linux运行C程序,并且能读一下你用到的多个C函数.

epoll在linux2.6被介绍进来,在其他类unix操作系统下不可用.它提供与select和poll类似的功能:

  • select能同时监控FD_SETSIZE个描述符,特别的决定与libc编译期的一个确定数目.
  • poll没有select这样的限制,但是我们必须对所有传进来的描述符进行现行扫描来检测可读通知,复杂度是O(N),所用低效.

epoll没有这种限制,并不是线性扫描.因此能够表现地更好并且处理大量事件.

一个epoll实例可以被epoll_create创建.epoll_ctl是一个增加或者移除描述符来监视epoll实例.epoll_wait来等待事件,它将阻塞到有事件到来.

当一个描述符被添加到epoll实例,他们能被以边缘触发和水平触发来添加.当你用水平触发模式,数据可读时,epoll_wait将一直返回可读事件.如果你没读完数据,调用epoll_wait来继续监视描述符,它将再次返回一个读事件因为数据已经准备好了.

在边缘触发模式,你将只得到一个可读通知.如果你没有读完数据,可以调用epoll_wait来再次监视描述符,它将阻塞因为可读事件已经通知了.

传给epoll_wait的事件结构在下面.对于每个被监视的描述符,你可以与一个整数或者指针作为用户数据:

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 */
};

来让我们写代码.我们将会实现一个小型TCP服务器,它会打印传输到这个socket上的数据到标准输出.我们可以开始写一个create_and_bind.

static int
create_and_bind (char *port)
{
  struct addrinfo hints;
  struct addrinfo *result, *rp;
  int s, sfd;

  memset (&hints, 0, sizeof (struct addrinfo));
  hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */
  hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
  hints.ai_flags = AI_PASSIVE;     /* All interfaces */

  s = getaddrinfo (NULL, port, &hints, &result);
  if (s != 0)
    {
      fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
      return -1;
    }

  for (rp = result; rp != NULL; rp = rp->ai_next)
    {
      sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
      if (sfd == -1)
        continue;

      s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
      if (s == 0)
        {
          /* We managed to bind successfully! */
          break;
        }

      close (sfd);
    }

  if (rp == NULL)
    {
      fprintf (stderr, "Could not bind\n");
      return -1;
    }

  freeaddrinfo (result);

  return sfd;
}

create_and_bind函数包含一个标准IPV4或者IPV6 socket.

struct addrinfo
{
  int              ai_flags;
  int              ai_family;
  int              ai_socktype;
  int              ai_protocol;
  size_t           ai_addrlen;
  struct sockaddr *ai_addr;
  char            *ai_canonname;
  struct addrinfo *ai_next;
};

下面我们将用一个函数来设置非阻塞socket.

static int
make_socket_non_blocking (int sfd)
{
  int flags, s;

  flags = fcntl (sfd, F_GETFL, 0);
  if (flags == -1)
    {
      perror ("fcntl");
      return -1;
    }

  flags |= O_NONBLOCK;
  s = fcntl (sfd, F_SETFL, flags);
  if (s == -1)
    {
      perror ("fcntl");
      return -1;
    }

  return 0;
}

现在,main函数包含eventloop.

static int
make_socket_non_blocking (int sfd)
{
  int flags, s;

  flags = fcntl (sfd, F_GETFL, 0);
  if (flags == -1)
    {
      perror ("fcntl");
      return -1;
    }

  flags |= O_NONBLOCK;
  s = fcntl (sfd, F_SETFL, flags);
  if (s == -1)
    {
      perror ("fcntl");
      return -1;
    }

  return 0;
}

Now, on to the main() function of the program which contains the event loop. This is the bulk of the program:

#define MAXEVENTS 64

int
main (int argc, char *argv[])
{
  int sfd, s;
  int efd;
  struct epoll_event event;
  struct epoll_event *events;

  if (argc != 2)
    {
      fprintf (stderr, "Usage: %s [port]\n", argv[0]);
      exit (EXIT_FAILURE);
    }

  sfd = create_and_bind (argv[1]);
  if (sfd == -1)
    abort ();

  s = make_socket_non_blocking (sfd);
  if (s == -1)
    abort ();

  s = listen (sfd, SOMAXCONN);
  if (s == -1)
    {
      perror ("listen");
      abort ();
    }

  efd = epoll_create1 (0);
  if (efd == -1)
    {
      perror ("epoll_create");
      abort ();
    }

  event.data.fd = sfd;
  event.events = EPOLLIN | EPOLLET;
  s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
  if (s == -1)
    {
      perror ("epoll_ctl");
      abort ();
    }

  /* Buffer where events are returned */
  events = calloc (MAXEVENTS, sizeof event);

  /* The event loop */
  while (1)
    {
      int n, i;

      n = epoll_wait (efd, events, MAXEVENTS, -1);
      for (i = 0; i < n; i++)
	{
	  if ((events[i].events & EPOLLERR) ||
              (events[i].events & EPOLLHUP) ||
              (!(events[i].events & EPOLLIN)))
	    {
              /* An error has occured on this fd, or the socket is not
                 ready for reading (why were we notified then?) */
	      fprintf (stderr, "epoll error\n");
	      close (events[i].data.fd);
	      continue;
	    }

	  else if (sfd == events[i].data.fd)
	    {
              /* We have a notification on the listening socket, which
                 means one or more incoming connections. */
              while (1)
                {
                  struct sockaddr in_addr;
                  socklen_t in_len;
                  int infd;
                  char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

                  in_len = sizeof in_addr;
                  infd = accept (sfd, &in_addr, &in_len);
                  if (infd == -1)
                    {
                      if ((errno == EAGAIN) ||
                          (errno == EWOULDBLOCK))
                        {
                          /* We have processed all incoming
                             connections. */
                          break;
                        }
                      else
                        {
                          perror ("accept");
                          break;
                        }
                    }

                  s = getnameinfo (&in_addr, in_len,
                                   hbuf, sizeof hbuf,
                                   sbuf, sizeof sbuf,
                                   NI_NUMERICHOST | NI_NUMERICSERV);
                  if (s == 0)
                    {
                      printf("Accepted connection on descriptor %d "
                             "(host=%s, port=%s)\n", infd, hbuf, sbuf);
                    }

                  /* Make the incoming socket non-blocking and add it to the
                     list of fds to monitor. */
                  s = make_socket_non_blocking (infd);
                  if (s == -1)
                    abort ();

                  event.data.fd = infd;
                  event.events = EPOLLIN | EPOLLET;
                  s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
                  if (s == -1)
                    {
                      perror ("epoll_ctl");
                      abort ();
                    }
                }
              continue;
            }
          else
            {
              /* We have data on the fd waiting to be read. Read and
                 display it. We must read whatever data is available
                 completely, as we are running in edge-triggered mode
                 and won't get a notification again for the same
                 data. */
              int done = 0;

              while (1)
                {
                  ssize_t count;
                  char buf[512];

                  count = read (events[i].data.fd, buf, sizeof buf);
                  if (count == -1)
                    {
                      /* If errno == EAGAIN, that means we have read all
                         data. So go back to the main loop. */
                      if (errno != EAGAIN)
                        {
                          perror ("read");
                          done = 1;
                        }
                      break;
                    }
                  else if (count == 0)
                    {
                      /* End of file. The remote has closed the
                         connection. */
                      done = 1;
                      break;
                    }

                  /* Write the buffer to standard output */
                  s = write (1, buf, count);
                  if (s == -1)
                    {
                      perror ("write");
                      abort ();
                    }
                }

              if (done)
                {
                  printf ("Closed connection on descriptor %d\n",
                          events[i].data.fd);

                  /* Closing the descriptor will make epoll remove it
                     from the set of descriptors which are monitored. */
                  close (events[i].data.fd);
                }
            }
        }
    }

  free (events);

  close (sfd);

  return EXIT_SUCCESS;
}



epoll的优点 支持一个进程打开大数 目的socket描述符(FD) select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 IO 效率不随FD数目增加而线性下降 传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。 使用mmap加速内核 与用户空间的消息传递。
<think>好的,我现在需要帮助用户解答如何在Linux中使用epoll进行事件处理的问题。首先,我得回顾一下epoll的基本知识,确保自己理解正确。epoll是Linux中用于处理大量文件描述符的高效I/O多路复用机制,相比select和poll,它在处理高并发连接时表现更好,因为它避免了线性扫描所有描述符的开销。 用户的问题是关于epoll的用法,所以我应该先解释epoll的基本使用步骤,比如创建实例、注册事件、等待事件等。记得按照用户提供的系统级指令,所有行内数学表达式要用$...$,但这里可能用不上太多数学公式,更多是代码和步骤说明。 接下来,我需要参考用户提供的引用内容。引用[1]提到了epoll的三个关键步骤:创建实例、使用epoll_ctl添加或修改事件、使用epoll_wait等待事件。这部分应该作为基础结构来介绍。引用[2]讨论了边缘触发(ET)模式下的优化,可以结合ET和水平触发(LT)模式的区别来说明,这也是用户可能关心的性能优化点。 用户可能需要具体的代码示例,所以需要提供一个简单的示例代码,比如用C语言展示epoll的基本用法。同时,要解释各个函数的作用,比如epoll_create1、epoll_ctl、epoll_wait的参数和用法。要注意代码的正确性,比如处理错误情况,检查返回值。 另外,用户提供的引用中有提到epoll的ET模式如何通过一次性注册读写事件来避免频繁调用epoll_ctl,这点应该在优化部分提到。同时,可能需要注意ET模式下的非阻塞I/O处理,避免数据未读取完导致的问题。 还需要考虑用户可能的后续问题,比如epoll与select/poll的区别,ET和LT的选择,常见错误处理等,所以在相关问题的生成部分要涵盖这些方面。 现在整理一下结构: 1. 分步骤介绍epoll的基本用法,包括创建实例、注册事件、等待事件。 2. 代码示例,展示一个简单的TCP服务器如何使用epoll处理连接。 3. 解释边缘触发和水平触发的区别,以及使用时需要注意的地方。 4. 可能的优化技巧,如结合EPOLLINEPOLLOUT一次性注册。 5. 生成相关问题,帮助用户进一步学习。 需要确保回答符合中文要求,语法正确,代码用正确的格式标记。同时,引用部分要自然添加,比如在提到关键函数时引用[^1],在讨论ET模式优化时引用[^2]。 可能还需要检查是否有其他引用内容相关,比如引用[3]和[4]似乎与epoll无关,可能涉及其他系统调用或权限设置,但当前问题不涉及这些,所以暂时忽略。 最后,确保回答结构清晰,步骤分明,代码正确,并且相关问题能引导用户深入理解epoll的使用和优化。</think>### 如何使用epoll实现Linux事件处理? epoll是Linux中高效处理I/O事件的机制,特别适合高并发场景(如网络服务器)。以下是其核心使用步骤: --- #### **1. 创建epoll实例** 使用`epoll_create1()`创建epoll实例,返回文件描述符: ```c int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } ``` `epoll_create1(0)`的参数为标志位,通常设为0即可[^1]。 --- #### **2. 注册事件到epoll** 通过`epoll_ctl()`向内核注册需要监控的文件描述符及其事件。 **示例:监听TCP套接字的可读事件** ```c struct epoll_event event; event.events = EPOLLIN; // 关注可读事件 event.data.fd = sockfd; // 关联的套接字 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event) == -1) { perror("epoll_ctl"); close(epoll_fd); exit(EXIT_FAILURE); } ``` - `EPOLL_CTL_ADD`:添加新描述符 - `EPOLL_CTL_MOD`:修改已注册描述符 - `EPOLL_CTL_DEL`:删除描述符[^1] --- #### **3. 等待并处理事件** 使用`epoll_wait()`阻塞等待事件触发,返回活跃事件列表: ```c #define MAX_EVENTS 10 struct epoll_event events[MAX_EVENTS]; int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // -1表示无限等待 if (num_events == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < num_events; i++) { if (events[i].events & EPOLLIN) { // 处理可读事件(如接收数据) handle_read(events[i].data.fd); } if (events[i].events & EPOLLOUT) { // 处理可写事件(如发送数据) handle_write(events[i].data.fd); } } ``` --- #### **4. 触发模式:水平触发(LT)与边缘触发(ET)** - **水平触发(默认)**:只要文件描述符处于就绪状态,`epoll_wait()`会持续报告该事件。 - **边缘触发**:仅当描述符状态变化时(如从不可读变为可读)触发一次。 **优化技巧**:在ET模式下,可一次性注册`EPOLLIN|EPOLLOUT`以减少`epoll_ctl`调用。 **示例**: ```c event.events = EPOLLIN | EPOLLOUT | EPOLLET; // 边缘触发模式 ``` --- #### **完整代码示例(简化版TCP服务器)** ```c #include <sys/epoll.h> #include <sys/socket.h> #include <unistd.h> int main() { // 创建socket并绑定监听(省略部分代码) int sockfd = create_tcp_server(8080); int epoll_fd = epoll_create1(0); struct epoll_event event; event.events = EPOLLIN; event.data.fd = sockfd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event); while (1) { struct epoll_event events[10]; int num_events = epoll_wait(epoll_fd, events, 10, -1); for (int i = 0; i < num_events; i++) { if (events[i].data.fd == sockfd) { // 接受新连接 int conn_fd = accept(sockfd, NULL, NULL); event.events = EPOLLIN | EPOLLET; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &event); } else { // 处理客户端数据 char buffer[1024]; read(events[i].data.fd, buffer, sizeof(buffer)); } } } return 0; } ``` --- ### **关键注意事项** 1. **非阻塞I/O**:在ET模式下,必须将文件描述符设为非阻塞,并循环读取数据直到`EAGAIN`错误。 2. **性能优化**:合并事件类型(如`EPOLLIN|EPOLLOUT`)可减少`epoll_ctl`调用次数。 3. **错误处理**:需检查所有系统调用的返回值,避免资源泄漏。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值