一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

浅析linux中epoll入门实例

时间:2016-04-23 编辑:简简单单 来源:一聚教程网


epoll是目前进行服务器端编程的普遍选择,好处很多,这里不再赘述,本文主要描述如何在c语言中使用epoll的完整样例程序。
首先介绍用到的数据结构和三个api说明,然后通过编写一个打印所有输入到socket的字符输出到终端的服务器端的程序来完成整个例子。

epoll_event是用来对要监控的socket描述, 它包括epoll_data_t和要监控的事件类型的(一个__uint32_t类型的events)。epoll_data_t里的fd是用来存储要监控的文件描述符。

events 结构体中第一个参数支持的事件类型

– EPOLLIN,读事件

– EPOLLOUT,写事件

– EPOLLPRI,带外数据,与select的异常事件集合对应

– EPOLLRDHUP,TCP连接对端至少写写半关闭

– EPOLLERR,错误事件

– EPOLLET,设置事件为边沿触发

– EPOLLONESHOT,只触发一次,事件自动被删除

epoll在一个文件描述符上只能有一个事件,在一个描述符上添加多个事件,会产生EEXIST的错误。同样,删除epoll的事件,只需描述符就够了

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 */
};
使用epoll的三个api
头文件 /usr/include/sys/epoll.h
1. 生成一个epoll专用的文件描述符
如果调用成功返回0,不成功返回-1

int epoll_create(int size)
epoll_create返回的是一个文件描述符,也就是说epoll是以特殊文件的方式体现给用户
__size提示操作系统,用户可能要使用多少个文件描述符,该参数已经废弃,填写一个大于0的正整数

2.用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
如果调用成功返回0,不成功返回-1

int epoll_ctl(
    int epfd,                    //由 epoll_create 生成的epoll专用的文件描述符
    int op,                      //要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、
                                 //EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除
    int fd,                      //关联的文件描述符
    struct epoll_event *event    //指向epoll_event的指针
    )
3.用于轮询I/O事件的发生,返回发生事件数

int epoll_wait(
    int epfd,                   //由epoll_create 生成的epoll专用的文件描述符
    struct epoll_event * events,//用于回传代处理事件的数组
    int maxevents,              //每次能处理的事件数
    int timeout                 //等待I/O事件发生的超时值
                                //为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件
                                //为任意正整数的时候表示等这么长的时间,如果一直没有事件
                                //一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率
                                //如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
    )
epoll的api使用方式
1.epoll_create 生成的epoll专用的文件描述符
2.使用epoll_ctl注册事件,修改事件,删除事件对应的文件描述符到epollfd指定的epoll内核事件表中
3.使用epoll_wait阻塞等待注册的文件描述符上可读事件的发生
4.当有新客户端的连接或者客户端的数据写入,返回需要处理的事件数目

epoll的两种模式:

1. 水平触发(LT):使用此种模式,当数据可读的时候,epoll_wait()将会一直返回就绪事件。如果你没有处理完全部数据,并且再次在该epoll实例上调用epoll_wait()才监听描述符的时候,它将会再次返回就绪事件,因为有数据可读。ET只支持非阻塞socket。

2. 边缘触发(ET):使用此种模式,只能获取一次就绪通知,如果没有处理完全部数据,并且再次调用epoll_wait()的时候,它将会阻塞,因为就绪事件已经释放出来了。

ET的效能更高,但是对程序员的要求也更高。在ET模式下,我们必须一次干净而彻底地处理完所有事件。LT两种模式的socket都支持。

实例代码

1.创建并绑定服务器端socket
采用一种可移植的方式来生产socket,getaddrinfo返回对应的网卡信息,遍历对应的网络接口生成socket
成功返回socket文件描述符,失败返回-1

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;
}
2.设置socket为非阻塞模式
通过在文件描述符上设置 O_NONBLOCK 表识来实现非阻塞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;
}
3.event 循环处理

#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;
}
main函数的流程是
1. create_and_bind创建服务器端的socket描述符
2. 设置描述符为非阻塞
3. 监听描述符
4. 创建epoll文件描述符efd
5. 使用边缘触发的方式添加sfd输入监听事件

最外层的while循环时主事件循环(event loop)。调用epoll_wait阻塞等待事件发生,当事件到达epoll_wait返回事件在事件参数中,一批epoll_event结构体。
epoll事件循环中epoll实例在建立新连接时候添加事件和当断开连接的时候删除事件。

当事件发生时,有如下几种方式
错误:当发生错误,或者不是可读事件通知时,简单关闭文件描述符,关闭文件描述符会自动从efd的监控集中删除。
新连接:当监听描述符可读时,表示有新的连接到达,accept()新连接,设置新连接描述符为非阻塞并添加到efd监控集中。
客户端数据:当任何一个客户端文件描述符可读,使用read()每次读区512字节循环读取。 因为我们需要读取当前所有可读区数据 ,当边缘触发的情况下不会再次通知可读。 读取到的数据调用write写到标准输出stdout (fd=1)。如果read(2)返回0,表示EOF并切可以关闭客户端连接。如果返回-1并且 errno设置为EAGAIN表示这个事件所有可读的数据读取完毕可以返回进入主事件循环。

就这样不断的循环,添加和删除文件描述符到efd的监控集中。

热门栏目