IO多路复用机制(select|poll|epoll) -- 笔记整理

select机制

  • 单线程内处理多个IO请求,用户线程可根据自身需求,注册自己需要的socket或IO请求,等有数据上来再进行处理,以提高CPU利用率
  • 监听上限受文件描述符限制【FD_SETSIZE】,最大 1024
  • 解决1024以下客户端时使用select还可,但若链接客户端过多,采用轮询模型的select,会大大降低服务器响应效率
  • 理解图示
    IO多路复用机制(select|poll|epoll) -- 笔记整理_第1张图片

select函数

头文件
select		     #include <sys/select.h>
struct timeval   #include <sys/time.h>

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

1. nfds      【监听】所有文件描述符中,最大文件描述符 +1
2. readfds   【读】文件描述符监听集合,传入传出参数
3. writefds  【写】文件描述符监听集合,传入传出参数	 NULL
4. exceptfds 【异常】文件描述监听集合,传入传出参数	 NULL
5. timeout   
			  > 0 	    设置监听超时时长
		      = NULL	阻塞监听
              = 0 	    非阻塞监听,轮询           
6. 返回值
			  > 0	    所有监听集合[||异常]中, 满足对应事件的总数
			  = 0	    没有满足监听条件的文件描述符
	 		  =-1 	    errno
	 		
7. 请注意     |r|w|e|   传入传出参数
8. 传入:各自要求监听的集合
9. 传出:各自实际发生的监听集合
			 

相关函数

1. 清空一个文件描述符集合
void FD_ZERO(fd_set *set);	

2. 将待监听的文件描述符,添加到监听集合中
void FD_SET(int fd, fd_set *set);	

3. 将一个文件描述符从监听集合中移除
void FD_CLR(int fd, fd_set *set);	

4. 判断一个文件描述符是否在监听集合
/* 返回值: 存在:1 | 不存在:0 */
int  FD_ISSET(int fd, fd_set *set);

代码部分示例


// main函数
int main(int argc, char *argv[])
{
    int i, j, n, maxi;
	
	/* 自定义数组client, 存要监听的文件描述符 */
    int nready, client[FD_SETSIZE];               
    int maxfd, listenfd, connfd, sockfd;
    
    /* #define INET_ADDRSTRLEN 16 */
    char buf[BUFSIZ], str[INET_ADDRSTRLEN];       

    struct sockaddr_in clie_addr, serv_addr;
    socklen_t clie_addr_len;
    
    /* rset 读事件文件描述符集合 allset暂存 */
    fd_set rset, allset;                         

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	/* 端口复用 */
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	
	/* 填充addrInfo */
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family= AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port= htons(SERV_PORT);

    Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    Listen(listenfd, 128);
	
	/* 起初 listenfd 即为最大文件描述符 */
    maxfd = listenfd; 
                                             
    /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
    maxi = -1;  
       
    /* 用-1初始化client[] */                                            
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;                                        

    FD_ZERO(&allset);
    
    /* 构造select监控文件描述符集 */
    FD_SET(listenfd, &allset);                                  

    while (1) {   
    
    	/* 每次循环时,更新设置select监控集合 */
        rset = allset;                                          
		/* [maxfd+1]: lfd  [rset]: cfd*/
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);  
        if (nready < 0)
            perr_exit("select error");
            
		/* 有新的客户端连接请求存在 */
        if (FD_ISSET(listenfd, &rset)) {                    

            clie_addr_len = sizeof(clie_addr);
            connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       
           
            printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
                    ntohs(clie_addr.sin_port));

			/* 找client[]中没有使用的位置 */
			
            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {                            
                    client[i] = connfd;                         
                    break;
                }

			/* 达上限1024,进行错误处理 */
            if (i == FD_SETSIZE) {                              
                fputs("too many clients\n", stderr);
                exit(1);
            }
            
			/* 向监控文件描述符集合allset添加新的文件描述符connfd */
            FD_SET(connfd, &allset); 
                                       
			/* 更新maxfd */
            if (connfd > maxfd)
                maxfd = connfd;       
                                         
			/* 保证maxi存的总是client[]最后一个元素下标 */
            if (i > maxi)
                maxi = i;     
                                                 
			/* 说明此时只有listenfd,不存在监听的 读事件,没必要继续执行后续,直接continue*/
            if (nready == 1)
                continue;
        } 

		/* 筛选并查找 【有数据传入的客户端】,进行read读取/write回写处理 */        
		for (i = 0; i <= maxi; i++) {                               
            if ((sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
            
				 /* 当client关闭链接时,服务器端也关闭对应链接 */
                if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {   
                    Close(sockfd);
                    
                    /* 移除select对此文件描述符的监控 */
                    FD_CLR(sockfd, &allset);                        
                    client[i] = -1;
                } else if (n > 0) {
                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]);
                    Write(sockfd, buf, n);
                    Write(STDOUT_FILENO, buf, n);
                }
                if (--nready == 0)
                    break;                                        
            }
        }
    }
    Close(listenfd);
    return 0;
}

poll机制(了解)

相关知识

  • poll,半成品,是对select的改进,但相对提升不大
  • 最终版本epoll,需重点掌握
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

1. fds:监听的文件描述符【数组】
2. struct pollfd {
		int fd;	待监听的文件描述符
		
		short events;	待监听的文件描述符对应的监听事件
						取值:POLLIN、POLLOUT、POLLERR
		short revnets;	传入时,赋为0
						如果满足对应事件的话, 返回非0 => POLLIN、POLLOUT、POLLERR
}

3. nfds: 监听数组的,实际有效监听个数
4. timeout  
		>0:  超时时长[单位:毫秒]
		-1:	 阻塞等待
		=0:  不阻塞
		
5. 返回值: 返回满足对应监听事件的文件描述符[总个数]

6. 优点
自带数组结构, 可以将[监听事件集合][返回事件集合]分离
拓展监听上限, 超出1024限制

7. 缺点:
不能跨平台, Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大

epoll机制(重点)

epoll相关函数

#include 

1. 创建一棵监听红黑树

int epoll_create(int size)	
	
	-size    创建的红黑树的监听节点数量,跟内存大小有关[估摸的预计值,仅供内核参考]
	
	-返回值	 成功:指向新创建的红黑树的根节点[epfd]
			
			 失败: 返回-1, 置errno

2. 操作监听红黑树,往树上摘结点 ,挂结点

函数及结构体声明:
//返回值:成功 0 | 失败-1 [errno]
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

参数详解:
		-epfd	为epoll_create的句柄
		-op		表示动作,用3个宏来表示:
				EPOLL_CTL_ADD 添加fd到 监听红黑树
				EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
				EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
				
		-fd		待监听的fd
		-event	告之内核【需要监听的事件】
		
				struct epoll_event {
						__uint32_t events; /* Epoll events */
						epoll_data_t data; /* User data variable */
				};
				
				1. 本质 struct epoll_event 结构体 地址
				2. 成员 events:EPOLLIN() / EPOLLOUT() / EPOLLERR(异常)

				3. 成员 data: 联合体[共用体]

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

				1.  int  fd	   对应监听事件的 fd
				2. void *ptr 


3. 等待所监听的cfd上有事件发生(阻塞监听),类似于select()调用

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

参数值:
		-epfd	  	epoll_create()的返回值
		
		-events		传出参数,【数组】,满足监听条件的那些【fd结构体】

		-maxevents	数组元素的[总个数]
					struct epoll_event events[1024]
					告之内核events大小,【maxevents】不能大于epoll_create(size)
					
		-timeout	超时时间:【-1:阻塞 | =0:不阻塞 | >0:超时时间(毫秒)】
		
		
返回值:		> 0 	满足监听的 总个数,可用作循环上限
			= 0 	没有fd满足监听事件
		    =-1		失败,置为errno
	

实现思路

大致步骤:

// 监听 lfd
lfd = socket();			      
bind();
listen();
	
// epfd, 监听红黑树的[根节点]
int epfd = epoll_create(1024);	

// tep: 用来设置单个fd属性
// ep : epoll_wait() 传出的满足监听事件的数组	
struct epoll_event tep, ep[1024];	
	
// 初始化lfd的监听属性
tep.events = EPOLLIN;			
tep.data.fd = lfd   

// 将lfd 挂到监听红黑树上
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep);		

while(1) {
	// 实施监听
	ret = epoll_wait(epfd, ep,1024-1);			
	
	for (i = 0; i < ret; i++) {
			// lfd 满足读事件, 有新的客户端发起连接请求
		if (ep[i].data.fd == lfd) {			
				// 创建新的套接字cfd与客户端进行连接
				cfd = Accept();
				// 	初始化 cfd的监听属性
				tep.events = EPOLLIN;			
				tep.data.fd = cfd;
				// 将cfd 挂到监听红黑树上
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
		} 
		else {	// cfd满足读事件, 有客户端写数据来。
				n = read(ep[i].data.fd, buf, sizeof(buf));
				// 对端关闭处理
				if ( n == 0) { 
					close(ep[i].data.fd);
					// 将关闭的cfd,从监听树上摘下
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL);	
				} 
				else if(n > 0) {
					// 处理操作,然后回写
					write(ep[i].data.fd, buf, n);
			}
		}
	}
}


总结select && epoll

用户态与内核态

  • 例如:服务端读取文件内容发送至客户端过程:
  • 读:调用【系统函数】访问磁盘IO读取【数据】 => 保存数据到内核buf中 => 拷贝数据到用户buf中
  • 写:调用【系统函数】拷贝用户数据到 【内核】 => 将数据写入IO设备中

阻塞IO与非阻塞IO

  • 阻塞I/O 分为两部分阻塞
  • 第一个阶段:等待数据准备完成
  • 第二个阶段:等数据从内核buf拷贝到用户内存,内核返回结果,用户进程接触阻塞
  • 非阻塞I/O 主动询问内核数据是否准备完毕
  • 用户进程系统调用 => 内核buf数据没准备好 =>直接返回error
  • 不断询问发送R|W操作,直到直接拷贝用户内存(此时内核buf已备)

多路IO复用(同步I/O)

-例: 用户进程调用select(阻塞等待) => 内核监听fds(需拷贝) => 当有新连接,则返回 =>再调用读写操作 =>数据(内核-用户)

select函数( )存在的问题

  • 监听集合lfd限制【1024】| 集合需拷贝(用户 =>内核)
  • 当有新连接时,会遍历整个socket集合来收集fds可读列表

epoll的引入解决问题

  • (1)epoll_wait()读集合fds拷贝问题
  • epoll利用内存映射mmap(用户-内核),减少两者间的数据交换
  • epoll使用红黑树来组织监控的fds集合,epoll_ctl()来对监控的fds集合进行增删改操作
  • (2)遍历就绪态的集合fds
  • epoll中间层(ready_list双向链表+sleep_queue睡眠队列)
  • sockfd睡眠被唤醒调用回调函数将当前fd插入ready_list,并执行回调遍历函数收集读事件,通过epoll_wait()传入的事件数组唤醒相应process

你可能感兴趣的:(网络编程,epoll)