【Linux高性能服务器编程】:9、select/poll/epoll系列 系统调用

1.1、select API

在某段时间内,监听用户关注的fd的可读、可写和异常等事件

// nfds 被监听的文件描述符的总数
// readfds、writefds、exceptfds  可读、可写和异常等事件对应的fd集合
// timeout 设置select函数的超时时间, NULL将一直阻塞
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

fd_set 结构体:

#include <typesizes.h>
#define __FD_SETSIZE 1024

#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
	__fd_mask fds_bits[__FD_SETSIZE/__NFDBITS];		//限制了同时处理的fd数量
	#define __FDS_BITS(set)((set)->fds_bits)
#else
	__fd_mask __fds_bits[__FD_SETSIZE/__NFDBITS];
	#define __FDS_BITS(set)((set)->__fds_bits)
#endif
} fd_set;

访问 fs_set 位:

#include <sys/select.h>
FD_ZERO(fd_set* fdset);			/*清除fdset的所有位*/
FD_SET(int fd, fd_set* fdset);	/*设置fdset的位fd*/
FD_CLR(int fd, fd_set* fdset);	/*清除fdset的位fd*/
int FD_ISSET(int fd, fd_set* fdset);	/*测试fdset的位fd是否被设置*/

timeval 结构体:

// tv_sec、tv_usec均为0, 则立即返回
struct timeval
{
	long tv_sec;	/*秒数*/
	long tv_usec;	/*微秒数*/
};
1.2、文件描述符就绪事件

socket可读:
1、socket 内核接收缓存区,字节数 >= 其低水位标记SO_RCVLOWAT【此时可无阻塞读该socket,且读返回字节数 > 0】
2、socket 通信的 对方关闭连接【此时读该socket 将返回0】
3、监听socket 有新连接请求
4、socket 有未处理错误【此时可用 getsockopt 读和清除该错误】


socket可写:
1、socket内核发送缓存区,可用字节数 >= 其低水位标记SO_SNDLOWAT【此时可无阻塞写该socket,且写返回字节数 > 0】
2、socket 写被关闭【对写被关闭的 socket 执行写,将触发一SIGPIPE信号】
3、socket 用 非阻塞connect 连接成功或失败【超时】之后
4、socket 有未处理的错误【此时可用 getsockopt 来读和清除该错误】


socket异常:
1、收到带外数据

1.3、处理带外数据

socket 读普通数据返回,处可读状态;读带外数据返回,处异常状态

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
	if (argc <= 2)
	{
		printf("usage:%s ip_address port_number\n", basename(argv[0]));
		return 1;
	}
	const char* ip = argv[1];
	int port = atoi(argv[2]);

	int ret = 0;
	struct sockaddr_in address;
	bzero(&address, sizeof(address));
	address.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &address.sin_addr);
	address.sin_port = htons(port);
	int listenfd = socket(PF_INET, SOCK_STREAM, 0);
	assert(listenfd >= 0);

	ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
	assert(ret != -1);
	ret = listen(listenfd, 5);
	assert(ret != -1);

	struct sockaddr_in client_address;
	socklen_t client_addrlength = sizeof(client_address);
	int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
	if (connfd < 0)
	{
		printf("errno is:%d\n", errno);
		close(listenfd);
	}

	char buf[1024];
	fd_set read_fds;
	fd_set exception_fds;
	FD_ZERO(&read_fds);
	FD_ZERO(&exception_fds);
	while (1)
	{
		memset(buf, '\0', sizeof(buf));
		/*每次调用select前都要重新在read_fds和exception_fds中设置文件描述符connfd,
		因为事件发生之后,文件描述符集合将被内核修改*/
		FD_SET(connfd, &read_fds);
		FD_SET(connfd, &exception_fds);
		ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
		if (ret < 0)
		{
			printf("selection failure\n");
			break;
		}
		/*对于可读事件,采用普通的recv函数读取数据*/
		if (FD_ISSET(connfd, &read_fds))
		{
			ret = recv(connfd, buf, sizeof(buf) - 1, 0);
			if (ret <= 0)
			{
				break;
			}
			printf("get%d bytes of normal data:%s\n", ret, buf);
		}
		/*对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据*/
		else if (FD_ISSET(connfd, &exception_fds))
		{
			ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB);
			if (ret <= 0)
			{
				break;
			}
			printf("get%d bytes of oob data:%s\n", ret, buf);
		}
	}
	close(connfd);
	close(listenfd);
	return 0;
}
2、poll 系统调用

在某段时间内,轮询用户关注的fd的可读、可写和异常等事件

// fds: pollfd 结构类型的数组,指定用户关注的fd发生的可读、可写和异常等事件
// nfds: 被监听事件集合fds的大小
// timeout: poll的超时值; -1 永久阻塞, 0 立即返回
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

pollfd 结构体:

struct pollfd
{
	int fd;			/*文件描述符*/
	short events;	/*注册的事件*/
	short revents;	/*实际发生的事件,由内核填充*/
};
事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLPRI高级优先数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先数据可写
POLLRDHUPtcp连接被对方关闭,或者对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起;比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNVAL文件描述符没有打开

Linux内核2.6.17之后,poll 监听POLLRDHUP事件【用前需定义_GNU_SOURCE】,以确认对方关闭连接

3.1、epoll 内核事件表

epoll【Linux特有】用 一组函数 来完成任务,其将关注的文件描述符事件,放入内核一事件表
故较select/poll优势:无须每次调用,均重复传文件描述符集或事件集

1、创建 额外文件描述符 标识内核事件表

#include <sys/epoll.h>
int epoll_create(int size)	//size不起作用, 只是提示内核事件表要多大

2、操作 内核事件表

// 成功返0, 失败返-1并设置errno
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
opt 类型说明
EPOLL_CTL_ADD往事件表中注册fd上的事件
EPOLL_CTL_MOD修改fd上的注册事件
EPOLL_CTL_DEL删除fd上的注册事件
struct epoll_event
{
	__uint32_t events;	/*epoll事件*/
	epoll_data_t data;	/*用户数据*/
};

typedef union epoll_data	// 因联合体故不可同时用
{
	void* ptr;		// 指定与 fd 相关数据
	int fd;			// 事件从属的目标文件描述符
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

注意:因epoll_data是联合体,故不可同时用 ptr 和 fd
若想关联以快速访问,可让data放弃fd,移到ptr指向数据中

3.2、epoll_wait 函数

1、等待一组文件描述符上的事件:

// 成功返 描述符个数, 失败返-1并设置errno
// maxevents 指定最多监听多少个事件, 必>0
// 检测到事件就将其从内核事件表 复制到 events中
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

注意:events 只输出就绪事件,不同于 select / poll 既传入注册事件,又输出就绪事件


poll / epoll 差异:

// 如何索引poll返回的就绪文件描述符
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
// 必须遍历所有已注册文件描述符并找到其中的就绪者 [当然,可以利用ret来稍做优化]
for(int i = 0; i < MAX_EVENT_NUMBER; ++i)
{
	if(fds[i].revents & POLLIN)	// 判断第i个文件描述符是否就绪
	{
		int sockfd = fds[i].fd;
		// 处理sockfd
	}
}

// 如何索引epoll返回的就绪文件描述符
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
// 仅遍历就绪的ret个文件描述符
for(int i = 0; i < ret; i++)
{
	int sockfd = events[i].data.fd;
	// sockfd肯定就绪,直接处理
}
3.3、LT/ET 模式

epoll 操作fd 模式:
1、LT模式【Level Trigger,电平触发】:
当epoll_wait检测到事件并通知应用后,应用 可不立即处理,因后续调epoll_wait仍通知此事件,直至处理

2、ET模式【Edge Trigger,边沿触发,高效】:
当epoll_wait检测到事件并通知应用后,应用 必须立即处理,因后续调epoll_wait将不再通知此事件


epoll 默认是 LT模式,当往epoll内核事件表注册一fd的 EPOLLET事件时,其将以ET模式来操作

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

/*将文件描述符设置成非阻塞的*/
int setnonblocking(int fd)
{
	int old_option = fcntl(fd, F_GETFL);
	int new_option = old_option | O_NONBLOCK;
	fcntl(fd, F_SETFL, new_option);
	return old_option;
}

/*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数enable_et指定是否对fd启用ET模式*/
void addfd(int epollfd, int fd, bool enable_et)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN;
	if (enable_et)
	{
		event.events |= EPOLLET;
	}
	epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}

/*LT模式的工作流程*/
void lt(epoll_event* events, int number, int epollfd, int listenfd)
{
	char buf[BUFFER_SIZE];
	for (int i = 0; i < number; i++)
	{
		int sockfd = events[i].data.fd;
		if (sockfd == listenfd)
		{
			struct sockaddr_in client_address;
			socklen_t client_addrlength = sizeof(client_address);
			int connfd = accept(listenfd, (struct sockaddr*)&client_address,
				&client_addrlength);
			addfd(epollfd, connfd, false);	/*对connfd禁用ET模式*/
		}
		else if (events[i].events & EPOLLIN)
		{
			/*只要socket读缓存中还有未读出的数据,这段代码就被触发*/
			printf("event trigger once\n");
			memset(buf, '\0', BUFFER_SIZE);
			int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
			if (ret <= 0)
			{
				close(sockfd);
				continue;
			}
			printf("get%d bytes of content:%s\n", ret, buf);
		}
		else 
		{
			printf("something else happened\n");
		}
	}
}

/*ET模式的工作流程*/
void et(epoll_event* events, int number, int epollfd, int listenfd)
{
	char buf[BUFFER_SIZE];
	for (int i = 0; i < number; i++)
	{
		int sockfd = events[i].data.fd;
		if (sockfd == listenfd)
		{
			struct sockaddr_in client_address;
			socklen_t client_addrlength = sizeof(client_address);
			int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
			addfd(epollfd, connfd, true);	/*对connfd开启ET模式*/
		}
		else if (events[i].events & EPOLLIN)
		{
			/*这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出*/
			printf("event trigger once\n");
			while (1)
			{
				memset(buf, '\0', BUFFER_SIZE);
				int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
				if (ret<0)
				{
					/*对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。
					此后,epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作*/
					if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
					{
						printf("read later\n");
						break;
					}
					close(sockfd);
					break;
				}
				else if (ret == 0)
				{
					close(sockfd);
				}
				else
				{
					printf("get%d bytes of content:%s\n", ret, buf);
				}
			}
		}
		else
		{
			printf("something else happened\n");
		}
	}
}

int main(int argc, char* argv[])
{
	if (argc <= 2)
	{
		printf("usage:%s ip_address port_number\n", basename(argv[0]));
		return 1;
	}

	const char* ip = argv[1];
	int port = atoi(argv[2]);

	int ret = 0;
	struct sockaddr_in address;
	bzero(&address, sizeof(address));
	address.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &address.sin_addr);
	address.sin_port = htons(port);
	int listenfd = socket(PF_INET, SOCK_STREAM, 0);
	assert(listenfd >= 0);

	ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
	assert(ret != -1);
	ret = listen(listenfd, 5);
	assert(ret != -1);

	epoll_event events[MAX_EVENT_NUMBER];
	int epollfd = epoll_create(5);
	assert(epollfd != -1);
	addfd(epollfd, listenfd, true);

	while (1)
	{
		int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
		if (ret<0)
		{
			printf("epoll failure\n");
			break;
		}
		lt(events, ret, epollfd, listenfd);	/*使用LT模式*/
		//et(events,ret,epollfd,listenfd);	/*使用ET模式*/
	}
	close(listenfd);
	return 0;
}

注意:每个ET模式的fd 都应该是 非阻塞的;若阻塞,则读写将会因无后续事件而一直阻塞【饥渴状态】

3.4、EPOLLONESHOT 事件

ET模式下,一socket某事件可能被触发多次,如多线程读此socket
为在任意时刻其 只被一线程处理,故对其注册EPOLLONESHOT,同时处理后再重置

EPOLLONESHOT:最多触发fd上注册的一个可读、可写或者异常事件,且只触发一次

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024

struct fds
{
	int epollfd;
	int sockfd;
};

int setnonblocking(int fd)
{
	int old_option = fcntl(fd, F_GETFL);
	int new_option = old_option | O_NONBLOCK;
	fcntl(fd, F_SETFL, new_option);
	return old_option;
}

/*将fd上的EPOLLIN和EPOLLET事件注册到epollfd指示的epoll内核事件表中,参数oneshot指定是否注册fd上的EPOLLONESHOT事件*/
void addfd(int epollfd, int fd, bool oneshot)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET;
	if (oneshot)
	{
		event.events |= EPOLLONESHOT;
	}
	epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}

/*重置fd上的事件。这样操作之后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次*/
void reset_oneshot(int epollfd, int fd)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
	epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

/*工作线程*/
void* worker(void* arg)
{
	int sockfd = ((fds*)arg)->sockfd;
	int epollfd = ((fds*)arg)->epollfd;
	printf("start new thread to receive data on fd:%d\n", sockfd);
	char buf[BUFFER_SIZE];
	memset(buf, '\0', BUFFER_SIZE);
	/*循环读取sockfd上的数据,直到遇到EAGAIN错误*/
	while (1)
	{
		int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
		if (ret == 0)
		{
			close(sockfd);
			printf("foreiner closed the connection\n");
			break;
		}
		else if (ret<0)
		{
			if (errno == EAGAIN)
			{
				reset_oneshot(epollfd, sockfd);
				printf("read later\n");
				break;
			}
		}
		else
		{
			printf("get content:%s\n", buf);
			/*休眠5s,模拟数据处理过程*/
			sleep(5);
		}
	}
	printf("end thread receiving data on fd:%d\n", sockfd);
}

int main(int argc, char* argv[])
{
	if (argc <= 2)
	{
		printf("usage:%s ip_address port_number\n", basename(argv[0]));
		return 1;
	}

	const char* ip = argv[1];
	int port = atoi(argv[2]);

	int ret = 0;
	struct sockaddr_in address;
	bzero(&address, sizeof(address));
	address.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &address.sin_addr);
	address.sin_port = htons(port);
	int listenfd = socket(PF_INET, SOCK_STREAM, 0);
	assert(listenfd >= 0);

	ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
	assert(ret != -1);
	ret = listen(listenfd, 5);
	assert(ret != -1);

	epoll_event events[MAX_EVENT_NUMBER];
	int epollfd = epoll_create(5);
	assert(epollfd != -1);
	/*注意,监听socket listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序只能处理一个客户连接!
	因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件*/
	addfd(epollfd, listenfd, false);
	while (1)
	{
		int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
		if (ret < 0)
		{
			printf("epoll failure\n");
			break;
		}

		for (int i = 0; i<ret; i++)
		{
			int sockfd = events[i].data.fd;
			if (sockfd == listenfd)
			{
				struct sockaddr_in client_address;
				socklen_t client_addrlength = sizeof(client_address);
				int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
				/*对每个非监听文件描述符都注册EPOLLONESHOT事件*/
				addfd(epollfd, connfd, true);
			}
			else if (events[i].events & EPOLLIN)
			{
				pthread_t thread;
				fds fds_for_new_worker;
				fds_for_new_worker.epollfd = epollfd;
				fds_for_new_worker.sockfd = sockfd;
				/*新启动一个工作线程为sockfd服务*/
				pthread_create(&thread, NULL, worker, (void*)&fds_for_new_worker);
			}
			else
			{
				printf("something else happened\n");
			}
		}
	}
	close(listenfd);
	return 0;
}

因socket注册EPOLLONESHOT,故保证连接完整性的同时,避免了其他线程竞争

4、三组 I/O 复用函数比较

select/poll/epoll 相同:
同时监听多个文件描述符,将等待超时时间,直至fd事件发生,返回就绪fd数量


事件集差异:
select 中fd_set 未将fd 和事件绑定,故需分别传可读、可写及异常等事件
缺陷:1、不能处理更多类型事件
   2、内核对fd_set在线修改,应用下次调select必须重置fd_set

poll 把 fd 和事件都定义其中
优势:1、任何事件都被统一处理
   2、内核修改pollfd 的 revents,而events不变,应用下次调 poll 无须重置

epoll 在内核维护一事件表,并提供 epoll_ctl 对其增删改
优势:1、调 epoll_wait 直接从内核事件表 取事件,无需反复传事件

索引就绪fd差异:
poll、select 均返事件集合【包括就绪和未就绪】,应用索引就绪fd 时间复杂度均为 O(n)
epoll 仅返就绪事件,应用索引就绪fd 时间复杂度为 O(1)


最大支持fd数差异:
poll、epoll_wait 均可 监听系统允许打开的最大fd数 65 535 【cat/proc/sys/fs/file-max】
select 监听 fd 数量通常受限制,虽可改此限制,但可能后果不可预期


工作模式差异:
select、poll 均只能以 相对低效的LT模式工作
epoll 可以 LT 或 相对高效的ET模式工作【还支持EPOLLONESHOT,进一步减少触发】


具体实现差异:
select、poll 采用轮询,检测就绪事件 时间复杂度是O(n)
epoll_wait 采用回调,回调将事件插入内核就绪事件队列,内核适时将内容拷到用户空间,时间复杂度是O(1)

epoll_wait 特殊情况:
活动连接较多时,因回调过于频繁,效率未必比select、poll高,故适合连接数多,但活动连接较少的情况

系统调用selectpollepoll
事件集合用户通过readset、writeset、exceptset分别传入可读、可写及异常等事件,内核通过改参数来反馈就绪事件;
每次调select都要重置三参数
统一处理所有事件,故只需传事件集;
用户通过 fd.events 传入关注事件,内核通过改 pollfd.revents 反馈就绪事件
内核通过一事件表管理所有事件;
每次调epoll_wait,无需反复传事件;
epoll_wait 参数events仅反馈就绪事件
应用程序索引就绪文件描述符的时间复杂度O(n)O(n)O(1)
最大支持的文件描述符数一般有最大值限制:1024系统允许打开的最大文件描述符数目:65535【cat/proc/sys/fs/file-max】系统允许打开的最大文件描述符数目:65535【cat/proc/sys/fs/file-max】
工作模式LTLTLT / ET
内核实现和工作效率轮询检测就绪事件,时间复杂度O(n)轮询检测就绪事件,时间复杂度O(n)回调检验就绪事件,时间复杂度O(1)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值