浅析linux中epoll入门实例

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的监控集中。

时间: 2024-10-11 18:49:26

浅析linux中epoll入门实例的相关文章

Linux中fork()函数实例分析_Linux

一.fork 入门知识  一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事.  一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间.然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同.相当于克隆了一个自己.  我们来看一个例子: /* * fork_test.c * version 1

Linux中echo命令实例

echo是一种最常用的与广泛使用的内置于Linux的bash和C shell的命令,通常用在脚本语言和批处理文件中来在标准输出或者文件中显示一行文本或者字符串. echo命令的语法是: echo [选项][字符串] 1. 输入一行文本并显示在标准输出上 $ echo Tecmintis a community of LinuxNerds 会输出下面的文本: Tecmintis a community of LinuxNerds 2. 输出一个声明的变量值 比如,声明变量x并给它赋值为10. $

浅析Linux中的时间编程和实现原理(一) Linux应用层的时间编程

引子 我们都生活在时间中,但却无法去思考它.什么是时间呢?似乎这是一个永远也不能被回答的问题.然而作为一个程序员,在工作中,总有那么几次我必须思考什么是时间.比如,需要知道一段代码运行了多久:要在 log 文件中记录事件发生时的时间戳:再比如需要一个定时器以便能够定期做某些计算机操作.我发现,在计算机世界中,时间在不同场合也往往有不同的含义,让试图思考它的人感到迷茫.但值得庆幸的是,Linux 中的时间终究是可以理解的.因此我打算讨论一下有关时间的话题,尝试着深入理解 Linux 系统中 C 语

浅析Linux中的时间编程和实现原理(四)Linux 内核的工作

回顾 近年来,随着 Linux 的广泛使用,对时间编程提出了更高的要求.实时应用.多媒体软件对时钟和定时器的精度要求不断提高,在早期 Linux 内核中,定时器所能支持的最高精度是一个 tick.为了提高时钟精度,人们只能提高内核的 HZ 值 (一个内核参数,代表内核时钟中断的频率).更高的 HZ 值,意味着时钟中断更加频繁,内核要花更多的时间进行时钟处理.而内核的任何工作对于应用来说纯粹是无益的开销.当 HZ 值提高到 1000 之后,如果继续提高,Linux 的可用性将下降. 另外一方面,我

Linux中Shell入门教程

Shell入门教程:Shell变量    变量 变量是暂时用来存储数据的地方,是一个内存空间.bash shell和其他的编程语言,没有"数据形态",也就是说默认情况下不区分一个变量是整型还是浮点型等,除非你使用declare语句申明变量类型.在bash shell中,默认只有一种数据型,就是由字符组成的字符串.同时,设定的变量只在当前的shell中存在,也就是,每一个shell都会维护一份他们自己的变量,彼此不会有影响.可以把变量导出成环境变量,这样其他的shell就可以被子shel

服务器 libevent中epoll使用实例demo

名词解释:man epoll之后,得到如下结果: NAME       epoll - I/O event notification facility SYNOPSIS       #include <sys/epoll.h> DESCRIPTION       epoll is a variant of poll(2) that can be used either as Edge or Level       Triggered interface and scales well to l

浅析Linux中的时间编程和实现原理(三) Linux内核的工作

引子 时间系统的工作需要软硬件以及操作系统的互相协作,在上一部分,我们已经看到大多数时间函数都依赖内核系统调用,GlibC 仅仅做了一次请求的转发.因此必须深入内核代码以便了解更多的细节. 内核自身的正常运行也依赖于时钟系统.Linux 是一个典型的分时系统,CPU 时间被分成多个时间片,这是多任务实现的基础.Linux 内核依赖 tick,即时钟中断来进行分时. 为了满足应用和内核自己的需求,内核时间系统必须提供以下三个基本功能: 提供系统 tick 中断(驱动调度器,实现分时) 维护系统时间

浅析Linux中的时间编程和实现原理(二)硬件和GLibC库的细节

引子 熟悉了基本的编程方法之后,我们的兴趣就在于,计算机如何实现这一切的呢?在那些应用层 API 和底层系统硬件之间,操作系统和库函数究竟做了些什么? 首先看下 Linux 时间处理的一般过程: 图 1. 时间处理过程 应用程序部分已经在第一部分详细介绍过了,在第二部分我将介绍硬件和 GlibC 相关实现的一些概况. 硬件 PC 机里常见的时钟硬件有以下这些. RTC (Real Time Clock,实时时钟) 人们需要知道时间的时候,可以看看钟表.计算机系统中钟表类似的硬件就是外部时钟.它依

浅析linux中snapshots, blockcommit,blockpull

基础知识 一个虚拟机快照可被看作是虚拟机的在某个指定时间的视图(包括他的操作系统和所有的程序). 据此,某可以还原到一个之前的完整的状态,或者在guest运行的时候做个备份.所以,在我们 继续深入之前我们必须搞懂两个名词:backing files和overlays . QCOW2 backing files 与 overlays qcow2(qemu copy-on-write)具有创建一个base-image,以及在base-image(即backing file) 的基础上创建多个copy