Socket网络编程--epoll小结

本文深入探讨了Epoll的工作原理及优势,详细介绍了Epoll的关键API,并通过实例演示了Epoll在多路I/O复用场景下的高效实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的。因为它所支持的并发连接数是有限的(一般小于1024),因为用户处理的数组是使用硬编码的。这个最大值为FD_SETSIZE,这是在<sys/select.h>中的一个常量,它说明了最大的描述符数。但是对于大多数应用程序而言,这个数是够用的,而且有可能还是太大的,多数应用程序只使用3~10个描述符。而如今的网络服务器小小的都有几万的连接,虽然可以使用多线程多进程(也就有N*1024个)。但是这样处理起来既不方面,性能又低。

  同时期有I/O多路复用的还有一个poll函数,这个函数类似于select,但是其应用程序接口有所不用。原型如下

  #include <poll.h>
  int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

  //返回值: 准备就绪的描述符数,若超时则返回0,出错返回-1

  如果只是考虑性能的话,poll()也是不合适的,尽管它可以支持较高的TCP并发连接数,但是由于其采用“轮询”机制(遍历数组而已),但并发数较高时,其运行效率相当低(如果有10k个连接,单用于轮询的时间就需要1~10ms了),同时还可能存在I/O事件分配不均,导致部分TCP连接上的I/O出现“饥饿”现象。基于种种原因在Linux 2.5.44版本后poll被epoll取代。
  支持一个进程打开最大数目的 socket 描述符(FD)。select 最不能忍受的是一个进程所打开的FD 是有一定限制的,由 FD_SETSIZE 设置,默认值是 2048。对于那些需要支持的上万连接数目的 IM 服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache 方案Process Per Connection,TPC方案 Thread Per Connection),不过虽然 linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。
由于epoll这个函数是后增加上的,造成现在很少有资料提及到,我看了APUE,UNPv1等书都没有找到相关的函数原型。所以我只能从网络上抄一些函数原型过来了。

   epoll用到的所有函数都是在头文件sys/epoll.h中声明,有什么地方不明白或函数忘记了可以去看一下或者man epoll。epoll和select相比,最大不同在于:

  epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对(轮询)。这样就提高了效率。select的FD_SETSIZE是有限制的,而epoll是没有限制的只与系统资源有关。

  epoll_create函数

1 /* Creates an epoll instance.  Returns an fd for the new instance.
2    The "size" parameter is a hint specifying the number of file
3    descriptors to be associated with the new instance.  The fd
4    returned by epoll_create() should be closed with close().  */
5 extern int epoll_create (int __size) __THROW;

  该函数生成一个epoll专用的文件描述符。它其实是在内核申请空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。这个数没有select的1024约束。

  epoll_ctl函数

复制代码
 1 /* Manipulate an epoll instance "epfd". Returns 0 in case of success,
 2    -1 in case of error ( the "errno" variable will contain the
 3    specific error code ) The "op" parameter is one of the EPOLL_CTL_*
 4    constants defined above. The "fd" parameter is the target of the
 5    operation. The "event" parameter describes which events the caller
 6    is interested in and any associated user data.  */
 7 extern int epoll_ctl (int __epfd, int __op, int __fd,
 8                       struct epoll_event *__event) __THROW;
 9 /* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
10 #define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
11 #define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
12 #define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */
复制代码

  该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。 

  参数: epfd:由 epoll_create 生成的epoll专用的文件描述符; 
      op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除

      fd:关联的文件描述符; 
      event:指向epoll_event的指针; 
      返回值:如果调用成功返回0,不成功返回-1

  用到的数据结构

复制代码
 1 typedef union epoll_data
 2 {
 3   void *ptr;
 4   int fd;
 5   uint32_t u32;
 6   uint64_t u64;
 7 } epoll_data_t;
 8 
 9 struct epoll_event
10 {
11   uint32_t events;      /* Epoll events */
12   epoll_data_t data;    /* User data variable */
13 };
复制代码

  设置实例

复制代码
 1 struct epoll_event ev;
 2 //设置与要处理的事件相关的文件描述符
 3 ev.data.fd=listenfd;
 4 //设置要处理的事件类型
 5 ev.events=EPOLLIN|EPOLLET;
 6 //注册epoll事件
 7 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
 8 //常用的事件类型:
 9 EPOLLIN :表示对应的文件描述符可以读;
10 EPOLLOUT:表示对应的文件描述符可以写;
11 EPOLLPRI:表示对应的文件描述符有紧急的数据可读
12 EPOLLERR:表示对应的文件描述符发生错误;
13 EPOLLHUP:表示对应的文件描述符被挂断;
14 EPOLLET:表示对应的文件描述符有事件发生;
15 //具体可以看<sys/epoll.h>
复制代码

  epoll_wait函数

复制代码
 1 /* Wait for events on an epoll instance "epfd". Returns the number of
 2    triggered events returned in "events" buffer. Or -1 in case of
 3    error with the "errno" variable set to the specific error code. The
 4    "events" parameter is a buffer that will contain triggered
 5    events. The "maxevents" is the maximum number of events to be
 6    returned ( usually size of "events" ). The "timeout" parameter
 7    specifies the maximum wait time in milliseconds (-1 == infinite).
 8    This function is a cancellation point and therefore not marked with
 9    __THROW.  */
10 extern int epoll_wait (int __epfd, struct epoll_event *__events,
11                        int __maxevents, int __timeout);
12 
13 
14 /* Same as epoll_wait, but the thread's signal mask is temporarily
15    and atomically replaced with the one provided as parameter.
16    This function is a cancellation point and therefore not marked with
17    __THROW.  */
18 extern int epoll_pwait (int __epfd, struct epoll_event *__events,
19                         int __maxevents, int __timeout,
20                         __const __sigset_t *__ss);
复制代码

  该函数用于轮询I/O事件的发生;
  参数: epfd:由epoll_create 生成的epoll专用的文件描述符;
      epoll_event:用于回传代处理事件的数组;
      maxevents:每次能处理的事件数;
      timeout:等待I/O事件发生的超时值(单位应该是ms);-1相当于阻塞,0相当于非阻塞。一般用-1即可

      返回值:返回发生事件数。如出错则返回-1。

   下面给一个man手册里面的例子

复制代码
 1 #define MAX_EVENTS 10
 2 struct epoll_event ev, events[MAX_EVENTS];
 3 int listen_sock, conn_sock, nfds, epollfd;
 4 
 5 /* Set up listening socket, 'listen_sock' (socket(),
 6    bind(), listen()) */
 7 
 8 epollfd = epoll_create(10);
 9 if (epollfd == -1) {
10     perror("epoll_create");
11     exit(EXIT_FAILURE);
12 }
13 
14 ev.events = EPOLLIN;
15 ev.data.fd = listen_sock;
16 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
17     perror("epoll_ctl: listen_sock");
18     exit(EXIT_FAILURE);
19 }
20 
21