IO多路复用的三个函数select、poll和epoll
1.引出IO多路复用
以下内容参考了https://blog.csdn.net/SkydivingWang/article/details/74917897
我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output),往流中读出数据,系统调用read,写入数据,系统调用write。不过话说回来了 ,计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。
当我们处理高并发时,即有好多个fd需要我们处理时,我们该怎么做呢?我们想到的是开辟多个进程或者线程对许多fd进行处理,但是多进程并发与多线程并发又存在着下面的一些问题:
多进程并发:
- 进程数量有限制
- 代价太高——进程的销毁/切换
- 受限于cpu——一个cpu同一个时间只有一个进程在跑
- 进程间内存隔离
- 进程间通信复杂
多线程高并发:
-
受限于cpu——影响响应能力
-
阻塞——某个线程啥也不干,影响响应能力 非阻塞——死循环一直读数据,影响响应能力
(双十一几十万人同时访问淘宝,服务器也不可能开几十万个线程,引入IO多路复用)
于是,我们引入IO多路复用,其有select、poll和epoll三个函数,这三个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这三个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了(比如可读),select就会返回套接字可读,我们就可以调用recv来接受数据。我们平时用到的scanf、read、recv等函数都是阻塞IO,但是他们一次只能监测一个fd,对一个fd进行操作。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。
2.select函数——同步IO多路复用
select()函数允许一个进程监控多个文件描述符fd,当有一个或多个文件描述符就绪时(即可读可写等),程序对就绪的fd执行相应的操作(读/写等)。select()函数将分别监听三种独立的fd,可读,可写和异常情况的fd。在此函数处会发生阻塞
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
nfds:三种监听的fd的最大值+1
-
readfds:监听的可读的文件fd集合
-
writefds:监听的可写的文件fd集合
-
exceptfds:监听的可读的文件fd集合
-
timeout:延时
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
通过下面这四个函数可以创建、操作集合
void FD_CLR(int fd, fd_set *set);//清除监听集合的某个fd
int FD_ISSET(int fd, fd_set *set);//判断是否在监听集合中
void FD_SET(int fd, fd_set *set);//创建监听集合
void FD_ZERO(fd_set *set);//对监听集合清0
select()函数会一直发生阻塞,阻塞结束的条件:
- 一个文件描述符fd就绪
- 调用被信号中断
- 延时结束
返回值:
- retval>0:有retval个fd就绪
- retval=0:超出延时
- retval<0:发生错误
//eg
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void){
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
//设置监听集合
FD_ZERO(&rfds);
FD_SET(0, &rfds);//0为stdout
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);//std放在readfds处,监听是否可读
/* Don't rely on the value of tv now! */
if (retval == -1)//错误
perror("select()");
else if (retval){//有fd就绪
//此时数据是ready的 还在标准IO的缓冲中,并没有读
//如果不读,程序结束后,缓冲区的东西到达内核的stdin
printf("Data is available now.\n");
//修改部分
char buff[512] = {0};
scanf("%s", buff);
//修改部分
}
/* FD_ISSET(0, &rfds) will be true. */
else//超时
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
3.poll函数——等待一个fd上的events
poll()函数执行与select类似的任务,等待一个fd集合中的一个fd变成就绪状态
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
fds:在fds参数中指定要监控的文件描述符
struct pollfd { int fd; /* file descriptor */ //文件描述符 short events; /* requested events */ //要监听的时间 可读/写? short revents; /* returned events */ //反馈 当可读/写时会通过这个参数反馈 };
-
nfds: 要监听的fd的个数,一般就是struct pollfd结构体集合的大小
-
timeout:延时ms
常用的events:
- POLLIN:可读
- POLLOUT:可写
- POLLERR:错误
- POLLHUP:挂起
阻塞结束条件和返回值与select()类似
//eg:用poll函数写一个服务器,用来接受多个客户端发来的消息并给客户端发送对应信息
#include"../1.select将connect设置为非阻塞/common/head.h"
#include"../1.select将connect设置为非阻塞/common/common.h"
#include"../1.select将connect设置为非阻塞/common/tcp_server.h"
#include<poll.h>
#define POLLSIZE 100
#define BUFSIZE 1024
//小写->大写
char ch_char(char c){
if(c >= 'a' && c <= 'z') return c - 32;
return c;
}
int main(int argc, char **argv){
if(argc != 2){
fprintf(stderr, "Usage: %s port!\n", argv[0]);
exit(1);
}
int server_listen, fd;
if((server_listen = socket_create(atoi(argv[1]))) < 0){//将socket bind listen封装
perror("socket_create");
exit(1);
}
struct pollfd event_set[POLLSIZE];
for(int i = 0; i < POLLSIZE; i++){
event_set[i].fd = -1;
}
//用0号event_set监听server_listen
event_set[0].fd = server_listen;
event_set[0].events = POLLIN;
while(1){
//一次while循环只会加上一个client
int retval;
if((retval = poll(event_set, POLLSIZE, -1)) < 0){
perror("poll");
return 1;
}
//如果server_listen就绪,需要向结构体数组里加入客户端fd,以便进行监听
if(event_set[0].revents & POLLIN){
if((fd = accept(server_listen, NULL, NULL)) < 0){
perror("accept");
continue;
}
retval--;
//找到最小的下标存放fd
int i;
for(i = 1; i < POLLSIZE; i++){
if(event_set[i].fd < 0){
event_set[i].fd = fd;
event_set[i].events = POLLIN;//对于服务端,要对fd进行读操作
break;
}
}
if(i == POLLSIZE){
printf("Too many client!\n");
close(fd);
}
}
//当有fd就绪时,要对就绪的fd进行读操作并且给客户端发送对应的大写信息
for(int i = 1; i < POLLSIZE; i++){
if(event_set[i].fd < 0) continue;
if(event_set[i].revents & (POLLIN | POLLHUP | POLLERR)){
retval--;
char buff[BUFSIZE] = {0};
if(recv(event_set[i].fd, buff, BUFSIZE, 0) > 0){
printf("Recv: %s\n", buff);
for(int i = 0; i < strlen(buff); i++){
buff[i] = ch_char(buff[i]);
}
send(event_set[i].fd, buff, strlen(buff), 0);
}else{//当recv失败时,说明这个客户端已经下线,清楚对应的event_set
close(event_set[i].fd);
event_set[i].fd = -1;
}
}
if(retval < 0) break;
}
}
return 0;
}
4.epoll函数——IO事件通知
epoll()函数执行与poll函数相同的任务:监听多个文件描述符。其有两种模式:
- 边缘触发:循环扫描,只有扫描到它才对它监控
- 水平触发:当扫描到它时,往后都会对其监控
我们要用到以下3个函数:
-
int epoll_create(int size);//创建一个新的epoll实例 //程序是磁盘中的文件在内存的镜像(实例)
-
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //该系统调用对文件描述符epfd引用的epoll(7)实例执行控制操作。 它要求对目标文件描述符fd执行操作op。
-
epfd:通过epoll_create()创建的实例
-
op:对epoll的操作
- EPOLL_CTL_ADD:注册(添加)一个新的fd在epoll实例中
- EPOLL_CTL_MOD:改变对应fd的event参数(比如由原来监听可读->可写)
- EPOLL_CTL_DEL:从epoll实例中将对应的fd删除,不再监听
-
fd:要对epoll实例中的哪个实例进行操作
-
event:描述一个fd的状态
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
-
-
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); //等待epoll实例中fd集合的事件发生 //执行后epoll实例中所有要监听的fd的状态都放在传出参数events中 //其中events.data.fd = fd, events.events=可读/写/挂起/错误等
- epfd:创建的epfd实例
- events:传出参数
- maxevents:要监听的fd的数量
- timeout:延时
阻塞结束条件和返回值跟select类似
//eg:用epoll写一个服务器
#include <sys/epoll.h>
#define MAX_EVENTS 10
#include "../1.select将connect设置为非阻塞/common/head.h"
#include "../1.select将connect设置为非阻塞//common/tcp_server.h"
#include "../1.select将connect设置为非阻塞//common/common.h"
#define BUFFSIZE 512
int main(int argc, char **argv) {
//ev负责向epollfd里面注册
//events数组用来接受监控的结果
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
char buff[BUFFSIZE] = {0};
if (argc != 2) exit(1);
listen_sock = socket_create(atoi(argv[1]));
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);//创建epoll实例,一开始size=0
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
//将listen_sock注册进epollfd实例
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
//先找是否有新加入的client
for (int n = 0; n < nfds; ++n) {
//来一个client往epollfd注册一个
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock, NULL, NULL);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
make_nonblock(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
//当epollfd监测的sockfd有数据就绪时,接受转化为大写发送
} else {
//do_use_fd(events[n].data.fd);
if (events[n].events & (EPOLLIN | EPOLLHUP | EPOLLERR)) {
memset(buff, 0, sizeof(buff));
if (recv(events[n].data.fd, buff, BUFFSIZE, 0) > 0) {
printf("recv: %s", buff);
for (int i = 0; i < strlen(buff); i++) {
if (buff[i] >= 'a' && buff[i] <= 'z') buff[i] -= 32;
}
send(events[n].data.fd, buff, strlen(buff), 0);
//当没有数据接受时,说明这个sockfd已经关闭,将这个sockfd从epollfd删除
} else {
if (epoll_ctl(epollfd, EPOLL_CTL_DEL, events[n].data.fd, NULL) < 0) {
perror("epoll_ctrl");
}
close(events[n].data.fd);
printf("Logout!\n");
}
}
}
}
}
return 0;
}
5.三个函数的比较
以下内容参考了https://blog.csdn.net/sinat_36629696/article/details/81186774
-
select 的参数类型 fd_set 没有将文件描述符和事件绑定 , 只是一个文件描述符集合 , 因此 select 需要提供三个此类型的参数来分别传入和输出可读 , 可写 , 异常等事件 , 这使得 select 不能处理更多类型的事件 , 又因为内核对 fd_set 集合的在线修改 , 下次调用 select 之前必须重置 这 3 个 fd_set 集合
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
-
poll 的参数类型 pollfd 将文件描述符和事件绑定 , 任何事件都被统一处理 , 并且内核每次修改的是 pollfd 结构体的 revents 成员 , 而 events 成员保持不变 , 因此下次调用 poll 时无需重置 pollfd 类型的事件集参数
-
epoll则采用与select和poll完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到o(1)
epoll 适用于连接数量多 , 但活动连接较少的情况 , 因为当活动连接较多时 , 回调函数的触发过于频繁 , 其效率未必高于 select 和 poll