多路复用IO
第一章 高性能服务器技术栈 (select)
第二章 高性能服务器技术栈 (epool/poll)
前言
现在网络技术越来月普及,网络充斥了我们的生活当中,网络场景越来越复杂。在网络开发中,高并发的情景是在后端开发中司空见惯的场景。掌握并发技术是后端开发中的基本能力。目前实现并发的技术,主要有借助于协程,进程实现;多路复用I/O(select, poll, epool)等。
多路复用(IO multiplexing) 在网络层通常是指select/epoll。 但是有些地方
也称这种IO 方式为事件驱动IO(event driven IO)。
一、并发基础
实现并发的技术主要有select,poll ,epoll 等技术,它们各有优缺点(它们在内核中的实现其它章节讲解)。select 技术是通过轮询遍历的方式处理网路就绪IO。epool 是通过网络事件的方式处理网络就绪IO 。具体比较其它章节讲解
1.1 select 技术
select 引入了集合(fd_set)概念Select采用一个bit表,每个fd对应表中的一个bit位,采用此方式存储放入的fd.通过轮询检测集合中的句柄。等待检测的fd 准备就绪了(fd 已经是非阻塞状态)。然后返回就绪的文件描述符的个数。在进行进一步处理。IO有三种状态,可读,可写,异常。
接口函数
/* According to POSIX.1-2001 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> /**常用的select 函数*/ int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);
函数接口分别分析*
int select(int maxfd,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数解析
maxfd:文件描述符的范围,比监听的最大文件描述符加1。select最多同时监听描述符 数量有个上限,FD_SETSIZE(1024); readfds:可读:它是指向fd_set结构的指针,fd_set是一个描述符集合。这个集合中是要监控的读类型的文件的描述符。 writefds:可写:它也是指向fd_set结构的指针,这个集合是要监控的写类型的文件的描述符。 errorfds:异常:它是用来监视文件错误异常的文件描述符的集合。 timeout:它是select函数的超时时间,这个参数至关重要,它可以使select处于三种状态。 1,timeout=NULL 阻塞: 若将NULL以形参传入,即不传入时间结构,则select一直置于阻塞状态,直到监控到文件描述符集合中某个文件描述符发生变化为止; 2,timeout所指向的结构,时间设为0 非阻塞 :若将时间值设为0秒0毫秒,就变成一个纯粹的 非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值; 3,timeout所指向的结构设为非零时间: timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后返回0。
函数的返回值:
1.在正常情况下返回就绪的文件描述符个数;
2.时间超时返回0;
3.出错或者select被某个信号中断返回-1;
NODE
- 每次调用select后,都需要重新清空描述符集并重新添加感兴趣的文件描述符。
2.select返回时会将剩余时间填充到timeout参数中,因此重新调用select的时候也要重新初始化该时间参数;
- 添加到fd_set中的fd的数目必须小于FD_SETSIZE,否则就会越界。
检测越界工具:
使用valgrind和purify等内存检测工具。
API 接口
void FD_CLR(inr fd, fd_set *fdset);用来清除描述符集合fdset中的描述符fd
int FD_ISSET(int fd,f d_set *fdset);用来检测描述符集合fdset中的描述符fd是否存在, 返回值: 存在返回1,不存在返回0;
void FD_SET(int fd, fd_set *fdset);用来将描述符fd添加到描述符集合fdset中
void FD_ZERO(fd_set *fdset);用来清除描述符集合fdset,把集合置0;
详解:
fd_set 定义的位表中,有两类fd, listenfd 和 rwfd;
1.2 select 结构体分析
fdset 是一个 fd 的位图,采用数组的形式来存储。每个 bit 位代表一个 fd,0表示该 fd 的事件未就绪,1表示该 fd 事件的已就绪。
// fd_set 里面文件描述符的数量,可以使用 ulimit -n 进行查看,默认1024 #define FD_SETSIZE __FD_SETSIZE // fd_set 的成员是一个长整型的结构体 typedef long int __fd_mask; // fd_set 记录要监听的 fd 集合,及其对应状态 typedef struct { // fds_bits是long类型数组,用来实现位图,长度: 1024/32=32 // 共1024个bit位,每个bit位代表一个fd,0表示未就绪,1表示就绪 __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; } fd_set;
1.3 select 使用流程
创建集合(集合的元素:文件描述符),把要等待的fd放入集合(监听)
- 创建fd_set集合
- 用FD_ZERO初始化集合(将位图所有位置0)
- 用FD_SET把要监听文件描述符fd加入集合(将位图所对应的位置为1)
- 把listenfd加入集合,监听到listenfd 获取connfd ,然后加入集合;
用 select 系统调用,进程阻塞,当集合中任意一个FD准备就绪,解除阻塞
用 FD_ISSET 检查 fd 是否就绪,就绪就解除阻塞
1.4 select 缺点
- 监听的 fd 的数量有限制,默认是1024, 此值可以修改
- 每次 select 都需要把监听的 fd 从用户态拷贝到内核态,返回后从内核态拷贝数据到用户态
- 轮询遍历所有的 fd 来判断就绪状态,效率低
二、实例
1.多线程多进程实例
代码如下(示例):借助于多线程实现,react模型。
void * routine(void *arg) { pthread_detach(pthread_self()); int clientfd = *(int *)arg; unsigned char buff[BUFF_LENGTH]; while (1) { int ret = recv(clientfd, buff, BUFF_LENGTH,0); printf("buffer:%s ret:%dn",buff, ret); if (0 == ret) { close(clientfd); break; } send(clientfd, buff, ret,0); printf("clientfd : %d",clientfd); } } printf("threads method\n"); while(1) { struct sockaddr_in client; socklen_t len = sizeof(client); int clientfd = accept(listen_fd, (struct sockaddr *)&client, &len); pthread_t threadid; pthread_create(&threadid,NULL,routine,&clientfd); }
2.select 实例
代码如下(示例): 实现多个客户端连接服务端,通过requese-ack 模式实现。服务端借助于select 实现。
#if defined(PSELECT) printf("Slect method\n"); fd_set rfds,wfds, rset, wset; /** < fd_set 就是bit位,就是一个位表; rfds 是rset 的备份。 定义两个变量主要是一个变量用来执行查询操作,一个用来置位操作 */ int max_fd = listen_fd; int nready = 0; int conn_fd = 0; int nbytes = 0; int nsends = 0; char buff[BUFF_LENGTH] = {0}; int n = 0; int i = 0; FD_ZERO(&rfds); FD_ZERO(&wfds); FD_SET(listen_fd, &rfds); while(1) { rset = rfds; wset = wfds; nready = select(max_fd+1, &rset, &wset, NULL, NULL); if (0 > nready ) { printf("select error\n"); break; }else if(0 == nready) { continue; } if(FD_ISSET(listen_fd, &rset)) { struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); conn_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &length); if (0 > conn_fd ) { printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno); return 0; } FD_SET(conn_fd, &rfds); //max_fd = max_fd > conn_fd ? max_fd : conn_fd; if (conn_fd > max_fd) max_fd = conn_fd; if (--nready == 0) { continue; } } for(i = listen_fd+1; i < max_fd +1; i++) { if (FD_ISSET(i, &rset)){ nbytes = recv(i, buff, BUFF_LENGTH, 0); if (nbytes > 0) { buff[nbytes] = '\0'; printf("recv:%s \n",buff); FD_SET(i, &wfds); }else if (0 == nbytes) { FD_CLR(i,&rfds); close(i); } if (--nready == 0) { break; } }else if(FD_ISSET(i, &wset)) { FD_SET(i, &rfds); n = send(i,buff,nbytes,0); if (n == nbytes) { FD_CLR(i,&wfds); } if (--nready == 0) { break; } printf("i: %d maxid:%d listen_fd:%d\n",i,max_fd, listen_fd); //FD_CLR(i,&wfds); } } }
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。
本文主要讲解的是可以实现服务器并发方法,借助多线程实现并发,此种方法消耗资源比较大,相当于每一条会话都要借助于一个线程。linxu 系统的线程资源是有限的,此方法不太适合大量的并发。第二种方法借助于select 实现服务器并发,此种方法消耗资源较少。
推荐
[荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,DockerTCP/IP,协程,DPDK等技术内容,点击立即学习: