I/O多路复用——试图了解select、poll、epoll

前言

在前面一篇文章提到的文本回射服务器,每新来一个客户都需要新建子进程,每次套接字来数据都需要切换进程,而且,系统对进程描述符是有限的,而且需要处理僵尸进程,捕捉信号。如何让一个进程处理多个套接字描述符,这个时候就需要IO复用技术。
而对于客户端来说:当服务器进程被杀死时,服务器虽然给客户端发送了 FIN,但此时客户正阻塞于标准输入中,它将看不见这个EOF,而只有当它试图从套接字读取数据时才能知道服务器已终止。这时可能已经过了很多时间了。而如何解决这样的情况,就需要一种预先告知内核的能力,使得,内核一旦发现一个或多个条件已就绪(读入已准备好被读取,或者描述符已能承担更多的输出),它就通知进程,这个能力,称为I/O多路复用。

IO多路复用

多线程模型下的CS模型
在这里插入图片描述

io复用的CS模型
在这里插入图片描述

I/O复用模型:

使用IO复用,程序将不再阻塞在具体的I/O系统调用上,而是阻塞在select或者poll上。(新型的epoll还没学不敢乱说)。下面是I/O复用模型。
在这里插入图片描述

select

select函数允许进程指示内核等待多个事件的任何一个发生,或者过了指定一段时间后返回。事件如:

  1. 描述符集中的一个或多个准备好读
    2)描述符集中的一个或多个准备好写
    3)描述符集中的某些描述符存在异常条件待处理
    4)过了一段时间

函数原型

# include <sys/select.h>
# include <sys/time.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

返回值:若有就绪描述符则返回就绪描述符数目, 若出错返回 -1
参数介绍
fd_set :是一个数据结构,用来存储描述符集,原型是一个long类型的数组。其中每一个数组元素的每一位都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系。如:第一元素对应于描述符0-31
readset: 用来检查可读性的一组文件描述字。
writeset: 用来检查可写性的一组文件描述字。
exceptset: 用来检查是否有异常条件出现的文件描述字
maxfdpl:一个整型数据,为描述符集中的(fd)最大值+1,用来指定待测试的描述符个数。
timeout:是一个struct timeval用来设置对单个描述符的等待时间。

struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
}

有三种用法:
1)设为空指针永远等待下去,
2)等待一段固定时间,在有一个描述符准备好I/O时返回,但是不超过timeout指定的时间。
3)根本不等待,置为0					

相关宏

fd_set rset;
void FD_ZERO(fd_Set *rset);/*清空rset每一位,用来初始化*/
void FD_SET(int fd, fd_set *fdset);/*将fd添加到rset组*/
void FD_CLR(int fd, fd_set *fdset);/*将fd移除组,是FD_sET()的逆向操作*/
void FD_ISSET(int fd, fd_set *fdset);/*fd的是否就绪?*/
FD_SETSIZE 1024 /*现大多数系统对select的最大监听描述符量*/ 

描述符就绪条件

当一个描述符就绪,将计入select的返回值内。所以应该对描述符何时处于就绪有所了解。
在这里插入图片描述

使用流程

1)声明一个fd_set结构 rset,
2)FD_ZERO(&rset)清空rset组,初始化该结构
3)FD_SET(fd,&rset)将待检查的描述符添加到组
4)调用select();
5)FD_ISSET();检查相关描述符是否就绪。
需要注意的是: select函数的第一个参数是所有你关心的描述符fd中的最大值+1;
另外就是select中的三个参数:readset,writeset,exceptset都是值——结果参数,当select返回时,未就绪的描述符对应的位都会被清0。为此每次重新调用select都需要把关心的描述符重新置1,下面的例子中有个方便的方法就是建立两个描述符集结构allset和rset,其中allset保存所有关心的描述符,当select返回后,只需要将rset = allset即可。

使用了select的文本回射客户/服务器

客户端:

#include "mfs.h"
#include <iostream>
using namespace std;
void str_cli(FILE *fp, int sockfd){
    int     maxfdp1, stdineof;
    fd_set  rset;
    char    buf[MAXLINE];
    int     n;
    
    stdineof = 0;
    FD_ZERO(&rset);
    for(; ;){
        if (!stdineof)
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd,&rset);
        maxfdp1 = max(fileno(fp), sockfd)+1;
        select(maxfdp1, &rset, NULL, NULL, NULL);
        
        if(FD_ISSET(sockfd, &rset)){
            if( (n = read(sockfd, buf,MAXLINE)) == 0){
                if(stdineof == 1) 
                    return;   /*normal termination*/
                else {
                    perror("str_cli: server terminated prematurely");
                    exit(1);
                }
            }
            buf[n]='\0';
            cout<<"receive: "<<buf<<endl;
        }
        if(FD_ISSET(fileno(fp), &rset)){
            if( (n = read(fileno(fp), buf, MAXLINE)) == 0){
                stdineof = 1;
                shutdown(sockfd, SHUT_WR);/*send FIN*/
                FD_CLR(fileno(fp), &rset);/*turn off*/
                continue;
            }
            writen(sockfd, buf, n);
        }
    }
}
int main (int argc, char **argv){
    int sockfd;
    struct sockaddr_in servaddr;
    if(argc != 2){
        perror ("usage: tcpcli<IPaddress>");
        exit(1);
    }
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port =htons(2020);
    inet_pton(AF_INET,argv[1],&servaddr.sin_addr);  

    if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr) ))
   cout << "connect error"<< endl;
    str_cli(stdin, sockfd);

    exit(0);
}

服务器

#include "mfs.h"
#include <iostream>
using namespace std;
int main(){
    int     i,maxi, maxfd, listenfd, connfd, sockfd;
    int     nready, client[FD_SETSIZE];
    ssize_t n;
    fd_set  rset,allset;
    char    buf[MAXLINE];
    socklen_t clilen;
    struct sockaddr_in cliaddr,servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(2020);

    bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    
    listen(listenfd, LISTENQ);

    maxfd = listenfd;
    maxi = -1;
    for(i = 0; i < FD_SETSIZE; i++){
        client[i] = -1;       
    }
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for(; ;){
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if(FD_ISSET(listenfd , &rset)){/*新客户连接*/
            clilen = sizeof(cliaddr);
            connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
            
         
            for(i = 0; i < FD_SETSIZE; i++){
                if(client[i] < 0){
                    client[i] = connfd;/*保存描述符*/
                    break;
                }
            }
            if(i == FD_SETSIZE){
                cout<< "too many clients"<<endl;
                continue;
            }
            
               printf("connection from %s, port %d\n",
        inet_ntop(AF_INET, &cliaddr.sin_addr, buf, sizeof(buf) ),ntohs(cliaddr.sin_port));

            FD_SET(connfd, &allset);
            if(connfd > maxfd){
                maxfd = connfd;
            }
            if(i > maxi)
                maxi = i;
            if(--nready <= 0)
                continue;

        }
        for(i = 0; i <= maxi; i++){
            if((sockfd = client[i]) < 0)
                continue;
            if(FD_ISSET(sockfd, &rset)){
                bzero(buf,sizeof(buf));
                if( (n = read(sockfd, buf, MAXLINE)) == 0){
                    close(sockfd);
                    FD_CLR(sockfd,&allset);
                    client[i] = -1;
                }
                else {
                    buf[n]='\0';
                    cout<< buf << endl;
                    writen(sockfd, buf, n);
                }if(--nready <= 0)
                    break;
            }
        }
    }

}

上面代码中的mfs.h是自己常用的函数库

poll函数

函数原型

#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds,int timeout);
	/*返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1*/

第一个参数是指向一个结构数组的第一个元素的指针,每个元素都是一个pollfd结构,用来指定测试某个给定描述符fd的条件。
poll原理上与select差不多,只是选用了不同的数据结构struct pollfd。该数据结构完全存储了描述符的fd,所以不需要额外存储每个描述符的编号。另外这个数据结构不需要反复重置关心的描述符相关位。因为该结构声明了存储参数和结果的两个成员不需要。

struct pollfd{
int 	fd;		/*待检查的描述符*/
short 	events;	/*关心的事件,事件发生poll返回*/
short 	revents;/*发生的事件,事件在revents上按位存储所以后面检测要用位运算,*/
}

第二个参数nfds是pollfd数组中最大下标值加1。与select不同,select中的maxfp1是指最大描述符编号+1。
第三个参数是poll返回前等待多长时间,单位是毫秒。
可能的取值:

取值说明
负值(-1)永远等待
0立即返回
正值等待指定数目的毫秒值

相关事件

在这里插入图片描述
poll识别三类数据:普通、优先级带 和 高优先级。

POLLLIN可被定义为POLLRDNORM和POLLRDBAND的逻辑或,POLLOUT等同于POLLWRNORM,
之所以出现这么多常值,是因为历史遗留问题,为了向后兼容,某些古老的常值也留下来了,用的
时候挑合适的就好不用纠结。

普通数据
所有正规TCP数据和所有UDP数据都被认为是普通数据
当TCP连接读半部关闭(如接受了FIN),也是普通数据随后读操作返回 0
TCP连接存在错误,既可以认为是普通数据也可以是错误(POLLERR)。在监听套接字上有新连接既可以是普通数据也可以是优先级数据,大多数实现看成普通数据。

高优先级数据(High priority):带外数据,传输层协议使用带外数据(out-of-band,OOB)来发送一些重要的数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。为了发送这些数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。linux系统的套接字机制支持低层协议发送和接受带外数据。

优先级带数据(priority band)。
啥高优先级数据优先级带数据,实在找不到资料,如果有知道的朋友,麻烦告诉我一下。反正普通数据就挺常用的。

实例

把上面的select服务端改成poll服务

#include "mfs.h"
#include <iostream>
using namespace std;
int main()
{
    int i, maxi, listenfd, connfd, sockfd;
    socklen_t clilen;
    int nready;
    ssize_t n;
    char buff[MAXLINE];
    struct sockaddr_in servaddr, cliaddr;
    struct pollfd client[OPEN_MAX];

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(2020);

    bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

    listen(listenfd, LISTENQ);

    client[0].fd = listenfd;
    client[0].events = POLLRDNORM;

    for (i = 1; i < OPEN_MAX; ++i) /*初始化其他空位*/
        client[i].fd = -1;
    maxi = 0; /*client 里的最大下标*/

    for (;;)
    {
        nready = poll(client, maxi + 1, -1);

        if (client[0].revents & POLLRDNORM)
        {

            clilen = sizeof(cliaddr);
            connfd = accept(listenfd, (SA *)&cliaddr, &clilen);

            for (i = 1; i < OPEN_MAX; ++i)
            {

                if (client[i].fd < 0)
                {
                    client[i].fd = connfd;
                    client[i].events = POLLRDNORM;
                    printf("connection from %s, port %d\n",
                           inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));

                    break;
                }
            }
            if (i == OPEN_MAX)
            {
                cout << "too many client" << endl;
                continue;
            }
            if (i > maxi)
                maxi = i;
            if (--nready <= 0)
                continue;
        }

        for (i = 1; i <= maxi; i++)
        { /*检查所有描述符是否有数据*/
            if ((sockfd = client[i].fd) < 0)
                continue;
            if (client[i].revents & (POLLRDNORM | POLLERR))
            {
                cout << "new data" << endl;
                if ((n = read(sockfd, buff, MAXLINE)) < 0)
                {
                    if (errno == ECONNRESET)
                    {
                        /*connection reset by client*/
                        close(sockfd);
                        client[i].fd = -1;
                    }
                    else
                    {
                        perror("read error");
                        exit(1);
                    }
                }
                else if (n == 0)
                {
                    close(sockfd);
                }
                else
                {

                    buff[n] = '\0';
                    cout << buff << endl;
                    writen(sockfd, buff, n);
                }
                if (--nready <= 0)
                    break;
            }
        }
    }
}

运行结果:

在这里插入图片描述
在这里插入图片描述

总结

与select对比:
1、poll的描述符上限没有特别上限,主要取决你对pollfd数组的定义以及系统对该进程的描述符打开上限,select上限是 1024。
2、select与poll用的数据结构不同,select采用一个长整型数组fd_set,每一个描述符,对应其中一位(bit)而poll使用pollfd储存整个描述符编号。
3、select的fd_set每次都会重置没有事件发生的描述符,而pollfd不会。
4、select事件限制为微秒单位,poll为毫秒。

select和poll的优缺点

优点:避免了多进程或者多线程需要的进/线程切换开销。以及避免阻塞在单一I/O上,而错过其他描述符信息。
缺点:
1、每次都需要从用户空间复制描述符集到内核。
2、过多描述符时,每次遍历描述符集,服务速度会明显下降。

据说在epoll上解决了上面的问题。下次再学学看。。。。。

epoll

epoll可以参考select、poll、epoll之间的区别(搜狗面试)写得很好,也没必要再抄一遍吧。
补充水平触发与边缘触发:.
水平触发level trigger LT(状态达到)

当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,如果用户一次读写没取完数据,他会一直通知用户,如果这个描述符是用户不关心的,它每次都返回通知用户,则会导致用户对于关心的描述符的处理效率降低。

复用型IO中的select和poll都是使用的水平触发方式。

2.边缘触发edge trigger ET(状态改变)

当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,它只会通知用户进程一次,这需要用户一次把内容读取玩,相对于水平触发,效率更高。如果用户一次没有读完数据,再次请求时,不会立即返回,需要等待下一次的新的数据到来时才会返回,这次返回的内容包括上次未取完的数据。

信号驱动型IO使用的是边缘触发方式。

epoll既支持水平触发也支持边缘触发,默认是水平触发。

3.比较

水平触发是状态达到后,可以多次取数据。这种模式下要注意多次读写的情况下,效率和资源利用率情况。

   边缘触发是状态改变一次,取一次数据。这种模式下读写数据要注意一次是否能读写完成。

epoll与select对比

假设现在有1024个fd ; select 和epoll 都同时维护他, 假设这些fd 都是活跃的, 这种情况下,select一次扫描 可以扫描1024个fd,空闲的fd很少,
但是epoll 就有可能不一样了, epoll 是先注册等待回调, 有可能出现1024次回调;
  这样的情况下, 要是说epoll 效率比select 高-----这就不好说了!!!!!!!!
  如果select 和epoll 同时维护1024个fd ,但是每次只有一个fd有事件,这种情况下 select 每次都会扫描所有的fd, 对比于epoll 每次只有一个fd 回调。 select 做了很多无用功, 此时应该epoll的效率高吧!!
  或者在短连接多的时候, 一个连接使用epoll 会触发epoll_ctrl_add/del 两次系统调用,但是select 只有一次扫描 ,此时 也许select 效率性能更高。
高并发,且任一时间只有少数socket是活跃的。如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了

参考:
《unix网络编程卷1》
为什么人们总是认为epoll 效率比select高!!!!!!l)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值