EPOLL使用的简单总结

0. 为什么要用epoll

既然用到epoll,一定对select和poll有一定的了解。
Select需要与fd_set结构体配合使用,并在用户空间维护一个客户端描述符,且管理句柄时有数目的限制。
Poll解决了句柄数目的限制(链表实现),同时维护一个pollfd结构体的客户端事件的集合。

这来俩性能局限点为:
Select和POLL都会遍历整个集合来确定活跃描述符
与内核交互时会把所有句柄拷贝到内核

注意的是:
服务器性能四大杀手:
1.数据拷贝-> 缓存方案
2.环境切换(线程切换)->单核单线程,多核多线程
3.内存分配->内存池
4.锁竞争->减少锁的使用

Poll每次需要从用户态将所有的句柄复制到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。使用epoll时你只需要调用epoll_ctl事先添加到对应红黑树,真正用epoll_wait时不用传递socket句柄给内核,节省了拷贝开销。

以上此段出自阿里云《epoll全面讲解:从实现到应用》https://www.aliyun.com/jiaocheng/122174.html

Epoll在内核的实现使用了mmap共享内存,红黑树和锁,所以在一定条件下提升机器的性能:
大量链接的/不是所有的句柄都很活跃 条件下使用epoll

1. 为什么要使用非阻塞模式

ET模式需要非阻塞。
为此我们需要知道什么是阻塞模式,非阻塞模式,IO复用模型。
此外,在服务器程序中发生阻塞一般是读写数据和accept等待链接的时候。

以下图和思想,来源于《Unix网络编程 第二版》第一卷 第二部分 第六章 第二节

阻塞模式:
阻塞IO模型
正如原文所说,一开始写的网络编程代码都是阻塞模式,直观一点的意思就是没有用到select/poll,直接使用socket-> sockaddr_in ->bind->listen->while(1)->accept模型的简单回射服务器就是阻塞IO模型应用。此模型的局限是一个线程或者进程只能同时处理一个描述符。

非阻塞模式:
非阻塞IO模型
也就是应用层一直检查内核是否准备好数据,直到完成。可以做个简单的实验,就上面说过的回射服务器,直接设置成非租塞,accept会一直返回-1。原因是一直在等待链接,当链接到来读写完数据,再次疯狂返回-1。

IO复用模式:
IO复用模型
如图,IO复用其实就是select/poll/epoll这类的函数,它们们帮我们完成了内核的监控,并可以监控多个,当内核某个IO准备好后通知我们,我们在调用。与上面的非阻塞模式配合使用就不会反会-1的错误(当数据准备好后再accept,举例select也就是if (pollfds[0].revents & POLLIN){… accept …})。

我在使用第一次使用epoll时候(就是写这完文档的前一天)使用的是<非阻塞+IO复用+LT模式>,其实LT模式下非阻塞性能不高,但是好写。
之后会改ET。
先放个图。
epoll触发模式

图片来源图片来源优快云《epoll EPOLLL、EPOLLET模式与阻塞、非阻塞》https://blog.youkuaiyun.com/zxm342698145/article/details/80524331

2. epoll使用(c++,面向过程)

先说一下用epoll和不用IO复用网络服务器编程的区别
首先是阻塞的编程流程(个人总结不是很严谨):
阻塞IO编码流程

然后就是epoll 的IO复用编程模型:
epoll的IO复用模型
代码中的体现如下:

int main()
{
	/*Socket(AF_INET, SOCK_STREAM, 0),我这里设置了非阻塞模式,下面的accept4也是。*/
	int listenfd;
	listenfd = Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // fei zu se IO fu yong
	/*设置服务器的sockaddr_in结构体,IPv4,当前地址,8000端口*/
	struct sockaddr_in serveraddr;
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(8000);
	/*重连处理*/
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	/*Bind绑定描述符和服务器结构体*/
	Bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	/*Listen监听描述符*/
	Listen(listenfd, 20);
	/*准备客户端sockaddr_in结构体,以及accept的返回值*/
	struct sockaddr_in clientaddr;
	socklen_t clientlen;
	int connfd;

	/*准备epoll的epoll_event结构体集,用的是c++的向量,为了方便*/
	typedef std::vector<struct epoll_event> EpollList;     
	/*epoll_create1(EPOLL_CLOEXEC)生成用于处理accept的epoll专用的文件描述符,创建一个epoll的句柄*/
	int epollfd;
	epollfd = epoll_create1(EPOLL_CLOEXEC);
	//Creates a handle to epoll, the size of which tells the kernel how many listeners there are.
	/*设置epoll_event结构体监听事件,epoll_event结构体的变量,epfd用于注册事件*/
	struct epoll_event epfd;
	epfd.data.fd = listenfd;
	epfd.events = EPOLLIN/*| EPOLLET */;
	/*epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	epollfd为epoll_create1返回,epfd为epoll_event结构体监听事件的结构体
	epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型*/
	epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	/*设置epoll_event结构体集的大小*/
	EpollList events(16);//You can listen for 16 at first

	int nready;//活跃描述符个数

	while(1)
	{	
		/*nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1); 
			等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,
			这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。
			该函数返回需要处理的事件数目,如返回0表示已超时。*/
		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(int  i=0; i < nready; ++i)*/
		for(int  i=0; i < nready; ++i)
		{
			/*if (events[i].data.fd == listenfd)监听活跃*/
			if (events[i].data.fd == listenfd)
			{
				/*Accept客户端结构体和监听描述符,返回一个客户描述符,accept4比accept定义一个参数*/
				clientlen = sizeof(clientaddr);
				connfd = Accept4(listenfd, (struct sockaddr*)&clientaddr, &clientlen, 
									SOCK_NONBLOCK | SOCK_CLOEXEC);// fei zu se IO fu yong

				std::cout << connfd << "is come!" << std::endl;
				/*有客户访问到来,修改结构体事件,把监听描述符改为客户链接描述符,写入内核*/
				epfd.data.fd = connfd;
				epfd.events = EPOLLIN/* | EPOLLET*/;
				epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &epfd);

			}
			/*if (events[i].events & EPOLLIN)客户描述符活跃,客户链接描述符有可读事件*/
			else if (events[i].events & EPOLLIN)
			{
				connfd = events[i].data.fd;//取出链接描述符使用
				if (connfd < 0)
				{
					continue;
				}
				/*用于读写的准备*/
				char buf[100];
				bzero(buf, sizeof(buf));
				int n;
				if ((n = read(connfd, buf, 100)) > 0)
				{	
					std::cout << "::" << connfd <<" Date: ["<< buf <<"]" << std::endl;
					write(connfd, buf, n);
				}
				/*关闭描述符,就是客户断开连接后处理*/
				else if (n == 0)
				{
					std::cout << connfd << "is go" << std::endl;
					close(connfd);
					epfd = events[i];
					epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epfd);
				}

			}
		}

	}

	return 0;
}

3. epoll接口和结构体

Epoll的头文件

#include <sys/epoll.h>

Epoll的函数接口

	Int epoll_create(int size);

参数size为设置可以连接的多少,老的create函数,实例epoll,现在参数size被忽略,大小取决于内核的处理能力。

Int epoll_create1(int flags);

推荐使用的新版本, flags参数的值为EPOLL_CLOEXEC ;

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

事件注册函数

  • epfd:epoll_create返回的实例;

  • op:表示动作
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;

  • fd:监听的文件描述符;

  • event:通知内核的结构体下页说明

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

相当于select函数等待事件产生maxevents:通知内核event的大小;timeout:超时时间,-1为永远等待;

EPOLL结构体

Typedef union epoll_date{
		void *ptr;
		int fd;
		unit32_t u32;
		unit64_t u64;
}epoll_data_t

联合体,用户数据变量,一般使用fd文件描述符

Struct epoll_event{
	unit32_t events;
	epoll_data_t date;
}

events可以是以下几个宏的集合:
**EPOLLIN **:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

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

参考博客
博主:lvyilong316
http://blog.chinaunix.net/uid/28541347.html
epoll专栏,共10篇

<think>嗯,用户想了解epoll在多线程场景下的使用。首先,我得回忆一下epoll的基本概念。epoll是Linux下的一种I/O多路复用机制,处理大量文件描述符时效率很高,相比select和poll更有优势。那多线程环境下如何使用epoll呢?可能需要考虑线程间的分工和协作。 首先,用户可能想知道如何结合多线程来充分利用多核CPU,同时处理多个网络连接。常见的模式可能是一个主线程负责accept新连接,然后将连接分发给工作线程处理。或者每个工作线程都有自己的epoll实例,独立处理事件。 接下来要考虑的是线程安全问题。epoll相关的函数比如epoll_ctl,如果在多个线程中同时操作同一个epoll实例,可能会引发竞争条件。所以需要加锁,或者确保每个线程操作自己的epoll实例。例如,主线程接收新连接,然后通过某种方式分配给工作线程的epoll,这时候可能需要用线程安全的队列或者其他同步机制。 另外,边缘触发(ET)和水平触发(LT)模式的区别也很重要。ET模式更高效,但需要一次处理所有可用数据,否则可能会丢失事件。在多线程中,如果使用ET,必须确保正确读取或写入数据,避免遗漏。而LT模式可能会重复通知,但编程相对简单。 还有,负载均衡的问题。如何将新的连接或事件均匀分配给各个工作线程,避免某个线程过载。比如使用轮询、哈希或者基于当前负载情况动态分配。 然后想到一些具体的实现模型。比如Reactor模式,主线程负责监听和分发事件,工作线程处理具体的I/O操作。或者是每个工作线程自己负责监听一组文件描述符,使用epoll_wait独立处理。这种情况下,可能需要线程池来管理工作线程。 还要注意,文件描述符在添加到epoll实例后,如果在不同线程中操作,是否需要同步。例如,如果一个线程正在处理某个socket的读事件,而另一个线程要关闭这个socket,这可能会导致问题。所以需要合理的同步机制,比如引用计数或者互斥锁。 另外,多线程中使用epoll的性能优化点。比如避免惊群效应,当多个线程等待同一个epoll实例时,新事件到来会唤醒所有线程,导致资源竞争。Linux的EPOLLEXCLUSIVE选项可以解决这个问题,让只有一个线程被唤醒。 用户可能的实际应用场景,比如高并发的Web服务器,游戏服务器等。需要处理成千上万的并发连接,这时候多线程配合epoll可以显著提高吞吐量。 可能还需要考虑错误处理,比如某个工作线程崩溃,如何不影响其他线程,以及如何优雅地关闭线程和释放资源。 总结一下,回答的结构应该包括epoll的基本介绍,多线程下的使用模式,线程安全注意事项,触发模式的选择,负载均衡策略,常见模型(如Reactor),性能优化点,以及实际应用场景。要确保内容清晰,层次分明,同时给出示例代码或模型图可能更直观。</think>在多线程场景中使用 `epoll` 是构建高性能网络服务器的常见手段,通常结合 **I/O 多路复用**和**多线程分工协作**来充分利用多核 CPU 资源。以下是关键点及典型使用场景: --- ### **1. epoll 基础回顾** - **epoll** 是 Linux 的高效 I/O 事件通知机制,支持 **LT(水平触发)**和 **ET(边缘触发)**模式。 - 核心 API:`epoll_create`、`epoll_ctl`、`epoll_wait`。 - 优势:适用于管理大量文件描述符(如 Socket),避免轮询开销。 --- ### **2. 多线程与 epoll 的协作模式** #### **模式 1:单 epoll 实例 + 多线程处理** - **分工**: - **主线程**:负责 `epoll_wait` 监听事件,将就绪事件分发给工作线程。 - **工作线程**:通过线程池处理具体 I/O(如读取、业务逻辑、写入)。 - **注意点**: - 主线程需快速分发事件,避免成为瓶颈。 - 使用线程安全的任务队列(如无锁队列)传递事件。 - 适合 CPU 密集型任务,但需避免工作线程阻塞。 #### **模式 2:多 epoll 实例 + 多线程(Reactor 模型)** - **分工**: - 每个线程独立运行一个 `epoll` 实例,监听一组 Socket。 - 主线程通过 `accept` 接收新连接,按负载均衡策略分配给子线程的 epoll。 - **优势**: - 避免全局锁竞争(每个线程独立操作自己的 epoll)。 - 减少上下文切换,适合高并发连接。 - **实现关键**: - 使用 `SO_REUSEPORT` 或连接分发逻辑(如 Round-Robin、哈希)。 - 需要处理线程间通信(如通过管道或 eventfd 唤醒)。 --- ### **3. 线程安全注意事项** 1. **epoll_ctl 的同步**: - 若多线程操作同一个 epoll 实例,需对 `epoll_ctl` 加锁(如互斥锁)。 - 推荐每个线程维护独立的 epoll 实例,避免锁竞争。 2. **文件描述符的关闭**: - 确保一个 Socket 不会被多个线程同时操作(如关闭时需通过引用计数)。 3. **事件分发的一致性**: - 使用 ET 模式时,需确保数据一次性处理完毕,避免事件遗漏。 --- ### **4. 触发模式选择** - **LT(水平触发)**: - 事件未处理时会持续通知,编程简单,但可能频繁唤醒线程。 - 适合多线程场景,容错性高。 - **ET(边缘触发)**: - 事件仅在状态变化时通知一次,需非阻塞 I/O 且必须一次性处理完数据。 - 性能更高,但需谨慎处理边界条件。 --- ### **5. 性能优化技巧** - **避免惊群(Thundering Herd)**: - 使用 `EPOLLEXCLUSIVE` 标志(Linux 4.5+),防止多个线程同时唤醒处理同一事件。 - **绑定 CPU 亲和性**: - 将线程绑定到特定 CPU 核心,减少缓存失效。 - **零拷贝技术**: - 结合 `sendfile` 或 `splice` 减少数据拷贝。 --- ### **6. 典型代码结构示例** ```c // 模式2:每个线程独立运行 epoll void worker_thread(int epoll_fd) { struct epoll_event events[MAX_EVENTS]; while (1) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i = 0; i < n; i++) { int fd = events[i].data.fd; if (events[i].events & EPOLLIN) { // 非阻塞读取 + 处理数据 } // 其他事件处理... } } } // 主线程:accept 连接并分发给子线程的 epoll void dispatch_connection(int new_fd) { static int next_thread = 0; int target_epoll = worker_epolls[next_thread]; next_thread = (next_thread + 1) % num_workers; epoll_ctl(target_epoll, EPOLL_CTL_ADD, new_fd, &event); } ``` --- ### **7. 适用场景** - **高并发服务器**:如 Web 服务器(Nginx)、即时通讯服务。 - **实时系统**:游戏服务器、高频交易系统。 - **代理/网关**:需要高效转发大量连接的中间件。 --- ### **总结** 通过 `epoll` 结合多线程,可以显著提升服务器的并发处理能力。核心在于合理分工(事件监听 vs. I/O 处理)、避免锁竞争(如多 epoll 实例)、选择适合的触发模式(LT/ET),并结合线程池等技术优化资源利用率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值