Linux epoll 多路复用机制

本文深入探讨了epoll技术的优越性、工作原理、数据结构、API详解及完整C++代码示例。重点阐述了如何解决select的多个缺点,包括描述符上限、CPU开销和数据拷贝问题。同时,详细解释了epoll的工作过程,包括创建、控制和阻塞阶段。文章还对比了epoll的两种模式——水平触发和边缘触发,并提供了正确的读写方法。此外,介绍了epoll的两个关键数据结构和完整的C++代码示例,帮助读者理解和应用epoll技术。

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

1. epoll的优越性

上一节介绍的select有几个缺点:

  • 存在最多监听的描述符上限FD_SETSIZE
  • 每次被唤醒时必须遍历才能知道是哪个描述符上状态ready,CPU随描述符数量线性增长
  • 描述符集需要从内核copy到用户态

这几个缺点反过来正是epoll的优点,或者说epoll就是为了解决这些问题诞生的:

  • 没有最多监听的描述符上限FD_SETSIZE,只受最多文件描述符的限制,在系统中可以使用ulimit -n设置,运维一般会将其设置成20万以上
  • 每次被唤醒时返回的是所有ready的描述符,同时还带有ready的类型
  • 内核态与用户态共享内存,不需要copy

2. 简述epoll的工作过程

2.1 创建

首先由epoll_create创建epoll的实例,返回一个用来标识此实例的文件描述符。

2.2 控制

通过epoll_ctl注册感兴趣的文件描述符,这些文件描述符的集合也被称为epoll set

2.3 阻塞

最后调用epoll_wait阻塞等待内核通知。

3. 水平触发(LB)和边缘触发(EB)

epoll的内核通知机制有水平触发和边缘触发两种表现形式,我们在下面例子中看一下两者的区别。

  • 有一个代表读的文件描述符(rfd)注册在epoll

  • 在管道的写端,写者写入了2KB数据

  • 调用epoll_wait会返回rfd作为ready的文件描述符

  • 管道读端从rfd读取了1KB数据

  • 再次调用epoll_wait

如果rfd文件描述符以ET的方式加入epoll的描述符集,那么上边最后一步就会继续阻塞,尽管rfd上还有剩余的数据没有读完。相反LT模式下,文件描述符上数据没有读完就会一直通知下去。


epoll的两种模式LT和ET
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

所以,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

正确的读

 
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
}
if (nread == -1 && errno != EAGAIN) {
    perror("read error");
}

正确的写

 
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
        if (nwrite == -1 && errno != EAGAIN) {
            perror("write error");
        }
        break;
    }
    n -= nwrite;
}


man手册:

 When  used  as an edge-triggered interface, for performance reasons, it  is possible to add the  file  descriptor  inside  the  epoll  interface (EPOLL_CTL_ADD) once by specifying (EPOLLIN|EPOLLOUT).  This allows you to avoid continuously switching between EPOLLIN  and  EPOLLOUT  calling  epoll_ctl(2) with EPOLL_CTL_MOD.

ET模式:高速模式,不需要不停的调用Epoll_ctl的EPOLL_CTL_MOD进行切换in\out.连接描述符需要设置为非阻塞.需要考虑数据未读完的情况.

LT模式:不需要考虑数据是否读完,但是需要不停的切换in\out,避免cpu空转.

一般情况下,LT就够了,Redis只支持LT模式.


4. epoll的两个数据结构

4.1 epoll_event

struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

参数events

此参数是一个位集合,可能有以下几种中的组合:

  • EPOLLIN:适用read操作,包括对端正常关闭

  • EPOLLOUT:适用write操作

  • EPOLLRDHUP :TCP对端关闭了连接

  • EPOLLPRI:对于read操作有紧急的数据到来

  • EPOLLERR:文件描述符上的错误,不需要设置在events上,因为epoll总是会等待错误

  • EPOLLHUP:与上边EPOLLERR相同

  • EPOLLET:设置边缘触发方式

  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

4.2 epoll_data

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

这个结构体有些tricky,是四种不同数据类型的union,实际上设计者的意思是内容是什么交给使用者决定,相当于一个上下文。一般使用int fd来区分是哪个socket发生的事件。

5. API详解

5.1 epoll_create

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create创建了一个epoll的实例,请求内核为size大小的文件描述符分配一个事件通知对象。实际上size只是一个提示,并没有什么实际的作用。此函数返回用来标识epoll实例的文件描述符,此后所有对epoll的请求都要通过这个文件描述符。当不需要使用epoll时需要使用close关闭这个文件描述符,告诉内核销毁实例。

5.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll实例epfd上的控制操作,op的取值有以下三种:

  • EPOLL_CTL_ADD: 将fd带着事件参数event注册到epfd

  • EPOLL_CTL_MOD: 改变事件

  • EPOLL_CTL_DEL: 从epfd上删除

返回的错误码请参阅man手册。

5.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待注册在epfd上的事件,事件再events参数中带出。对于timeout参数:

  • -1:永远等待
  • 0:不等待直接返回
  • 其他:在超时时间内没有事件发生,返回0

6. 完整C++代码示例


server
/*
 * Serverepoll2.cpp
 *
 *  Created on: 2015-1-19
 *      Author: shuyan
 */

#include "Serverepoll2.h"

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#include <vector>
#include <algorithm>
void activate_nonblock(int fd);
typedef std::vector<struct epoll_event> EventList;

/* 相比于select与poll,epoll最大的好处是不会随着关心的fd数目的增多而降低效率 */
int main(void) {
	int count = 0;
	int listenfd;
	if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)

		perror("socket");

	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(1234);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY );

	int on = 1;
	if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
		perror("setsockopt");
	if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
		perror("bind");
	if (listen(listenfd, SOMAXCONN) < 0)

		perror("listen");

	std::vector<int> clients;
	int epollfd;
	epollfd = epoll_create1(EPOLL_CLOEXEC); //epoll实例句柄

	struct epoll_event event;
	event.data.fd = listenfd;
	event.events = EPOLLIN | EPOLLET; //边沿触发
	epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);

	EventList events(16);
	struct sockaddr_in peeraddr;
	socklen_t peerlen;
	int conn;
	int i;

	int nready;
	while (1) {
		nready = epoll_wait(epollfd, &*events.begin(),
				static_cast<int>(events.size()), -1);
		if (nready == -1) {
			if (errno == EINTR)
				continue;
			perror("epoll_wait");
		}
		if (nready == 0)
			continue;

		if ((size_t) nready == events.size())
			events.resize(events.size() * 2);

		for (i = 0; i < nready; i++) {
			if (events[i].data.fd == listenfd) {
				peerlen = sizeof(peeraddr);
				conn = accept(listenfd, (struct sockaddr *) &peeraddr,
						&peerlen);
				if (conn == -1)
					perror("accept");
				printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
						ntohs(peeraddr.sin_port));
				printf("count = %d\n", ++count);
				clients.push_back(conn);

				activate_nonblock(conn);

				event.data.fd = conn;
				event.events = EPOLLIN | EPOLLET;
				epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event);
			} else if (events[i].events & EPOLLIN) {
				conn = events[i].data.fd;
				if (conn < 0)
					continue;

				char recvbuf[1024] = { 0 };
				int ret = recv(conn, recvbuf, 1024, 0);
				if (ret == -1)

					perror("readline");
				if (ret == 0) {
					printf("client close\n");
					close(conn);

					event = events[i];
					epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event);
					clients.erase(
							std::remove(clients.begin(), clients.end(), conn),
							clients.end());
				}

				printf("id:%d,data:%s", events[i].data.fd, recvbuf);
				send(conn, recvbuf, strlen(recvbuf), 0);
			}

		}
	}

	return 0;
}

/* activate_nonblock - 设置IO为非阻塞模式
 * fd: 文件描述符
 */
void activate_nonblock(int fd) {
	int ret;
	int flags = fcntl(fd, F_GETFL);
	if (flags == -1)

		perror("fcntl error");

	flags |= O_NONBLOCK;
	ret = fcntl(fd, F_SETFL, flags);
	if (ret == -1)
		perror("fcntl error");
}

client端同上篇

另见:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值