Bootstrap

[Linux/C/C++ 学习笔记]系列 2.IO多路复用(select\poll\epoll)

[Linux/C/C++ 学习笔记]系列 2. I/O多路复用(select\poll\epoll)

今日碎碎念:
底层代码写得好,上层代码跑得好😊

如有错误,多多包涵,欢迎指正



一、所需环境

二、Linux下的I/O多路复用

上集说到,使用线程可以处理少量client下的,而大量的client如果使用线程会极大的消耗系统资源,那么更好的管理方法epoll就出现了。epoll主要用于处理大量的I/O(不仅限与网络I/O),成为高并发场景下处理 I/O 事件的首选技术。相较于传统的 select poll,epoll 能够高效地监控大量文件描述符的状态变化,而不会因为client数量的增加而线性增长开销。

而epoll表现出色的原因是:epoll是基于事件驱动和内核回调的,通俗来讲,只有需要干活了epoll才通知你,避免了在每次轮询时重复检查所有文件描述符的开销(事件驱动),而且epoll只需要在内核中注册一次,不需要反复向内核空间拷贝需要查询的fd,避免了用户空间到内核空间的复制开销(内核回调)。

1. epoll简述

1.创建epoll实例及事件

使用 epoll 只需要掌握3个函数及1个结构体,首先来看函数:

		int epoll_create(int size);
		int epoll_create1(int flags);	epoll的改进版,可以在执行 exec 时自动关闭该文件描述符

很简单,创建一个epoll的文件描述符(fd, file descriptor), epfd。
接下来先介绍struct epoll_event,前面说到epoll是事件驱动的,那么这个事件指的即是 epoll_event

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

其中的data一般会是所关心的fd。
其中的events,表示的是所需监听事件的类型是什么,经常被设置为下面两种:

  • EPOLLIN :表示对应的文件描述符上有可读数据时触发。
  • EPOLLOUT:表示对应的文件描述符可以写入数据而非阻塞时触发。

2.管理事件
epoll_ctl 便用于管理所有事件,在op出输入你对事件的操作:

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

例如:

  • EPOLL_CTL_ADD:添加事件
  • EPOLL_CTL_MOD:修改事件
  • EPOLL_CTL_DEL:删除事件

3.等待事件触发
epoll_wait 用于等待 epoll 实例中的事件发生,用一个结构体数组events储存发生的事例,maxevents为events的最大数量,timeout为超时时间,返回值表示发生的事件数量(0超时或未发生,-1错误):

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

当事件发生时,便可以在这之后处理所有关心的事件。


2. select和poll简述

selectpoll 都会返回整个文件描述符集合的状态,而不仅仅是同 epoll 一样返回有事件发生的文件描述符。这意味着应用程序需要遍历并检查每个文件描述符的状态,以确定哪些描述符有事件发生,导致高并发下遍历开销显著增加。

此外,select 在每次调用时都要将整个文件描述符集合从用户空间复制到内核空间,因此当文件描述符数量较多时,这一过程会消耗大量系统资源。poll 通过传递指向文件描述符集合的指针到内核,减少了部分复制开销,但同样需要应用程序逐一遍历每个文件描述符,因此二者在大量文件描述符的情况下均会带来较大性能消耗。

  1. select简述

select直接通过以下方式,建立了所关心的fd_set,readfds、writefds、exceptfds分别是当前可读、可写和异常的文件符集合。

		int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

通过一个个查询 FD_ISSET(),确定fd是否在对应集合中,然后再进行相应操作。
所有对fd的操作分别如下:

  • void FD_CLR(int fd, fd_set *set):将fd从set中清除
  • int FD_ISSET(int fd, fd_set *set):查询fd是否在set中
  • void FD_SET(int fd, fd_set *set):将fd添加到set中
  • void FD_ZERO(fd_set *set):清空整个set(叫做ZERO是因为底层是用bitmap表示集合的)
  1. poll简述

poll引入了pollfd的结构体,定义如下:

		struct pollfd {
		    int   fd;        
		    short events;
		    short revents;
		}
  • fd:需要监视的文件描述符
  • events: 需要监视的事件,指定监视可读、可写、异常等
  • revents:poll实际返回的事件,表示实际事件的状态

通过构造传递结构数组fds,来查询就绪的事件,返回值和eooll_wait相似,表示发生的事件数量(0超时或未发生,-1错误)。

		int poll(struct pollfd *fds, nfds_t nfds, int timeout);

可以看到,poll和select一样,需要对集合中的所有fd进行遍历,但不同于select,poll只用了不需要重复复制set,减少了用户空间到内核空间的开销。而epoll则更进一步,返回的全是需要去处理的事件,减少了对所有事件的遍历操作。


总结

简单来说,select要复制来复制去,还需要对每一个fd进行检查;poll虽然不需要复制这么多,但也要遍历检查;epoll则直接告诉你哪些发生了,直接操作所关心的fd即可。
那么,能用epoll的情况下,不用epoll用什么?
当然,本文只是简单的称述了三种i/o方法,具体里面还有一些较多的实现细节,方法之间的区分,可以参考以下优秀博文:
epoll 详解
Linux–多路转接之epoll
还有git资料:
git资料链接


epoll实现tcp server代码

#define MAX_EVENTS 1024 
#include <stddef.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>



int main(int argc, char* argv){
	int sockfd = socket(AF_INET,SOCK_STREAM
,0);
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(2000);
	
	if(-1 == bind(sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr))){
		printf("%s bind failed\n", strerror(errno));
	}
	listen(sockfd, 10);
	
	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);

	int epfd = epoll_create(1);
	
	struct epoll_event ev;
	
	ev.data.fd = sockfd;
	ev.events = EPOLLIN;

	printf("epoll create!");

	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

	while (1) {

		struct epoll_event events[MAX_EVENTS] = {0};
		int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);

		int i = 0;
		for (i = 0;i < nready;i ++) {

			int connfd = events[i].data.fd;

			if (connfd == sockfd) {
				int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
				printf("accept finshed: %d\n", clientfd);

				ev.events = EPOLLIN;
				ev.data.fd = clientfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
				
			} else if (events[i].events & EPOLLIN) {

				char buffer[1024] = {0};
				
				int count = recv(connfd, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("client disconnect: %d\n", connfd);
					close(connfd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
					continue;
				}

				printf("RECV: %s\n", buffer);

				count = send(connfd, buffer, count, 0);
				printf("SEND: %d\n", count);

			}

		}

	}
	return 0;

}
;