I/O复用——select

一、I/O模型


1、I/O 复用是程序能够同时监听多个文件描述符。内核一旦发现进程指定的一个或者多个I/O条件准备读,它就通知该进程。

2、I/O 复用典型用于以下网络场合:

1)当客户端同时处理多个套接字,这种情况很少出现。

2)当客户端同时处理多个文件描述符(交互式输入和网络套接字)时,必须使用I/O复用。

3)如果一个TCP服务器既要处理监听套接口,又要处理连接套接口,一般也用到I/O复用。

4)如果一个服务器要处理多个服务或多个协议,一般也用到I/O复用。

3、与多进程和多线程技术相比,I/O复用最大的优势在于系统开销小,系统不必创建进程或线程,同时也不必维护进程或者线程,从而大大减小了系统的开销。


4、I/O 复用的本质是内核级别对fd文件描述符进行轮询,那个准备好了就通知用户代码,这样最优。如果没有I/O复用的话,你需要自己轮询哪个fd准备好了,或者有个线程阻塞等待一个fd

I/O 复用只是在fd是否就绪这个问题上帮助用户代码,所谓就绪包括下载请求的到来,但是真正处理下载,是由于用户自己的工作线程去处理的,如果同时请求过大,超过了单机的处理能力,那么用户需要自己设计排队或者分流,这根I/O复用无关。


二、select函数实现


1、select() 函数允许进程指示内核等待多个事件中的任意一个发生,并仅在一个或者多个事件发生或经过指定的时间时才唤醒进程。

#include<sys/select.h>  
#include<sys/time.h>  
int select(int maxfdp1,fd_set* readset,  fd_set* writeset, fd_set* execepset,  const struct timeval* timeout);  
//返回:返回值表示所有描述字集中已准备好的描述字个数。如定时到,则返回0;若出错,则返回-1。 

在上面的参数中可以看到一个timeval结构,这个结构可以提供秒数和毫秒数成员,形式如下:


struct timeval  
{  
long tv_sec; /second*/  
long tv_usec; /*microsecond*/  
}   

1)设置为空指针NULL,永远等待下去,仅在有一个描述字准备好I/O时才返回。

2)等待固定时间,再有一个描述字准备好I/O时返回。

3)成员设置为0,根本不需要等待,检查描述字后立即返回,轮询。


2、
 (1)FD_CLR : 用来清除描述词组set中相关fd的位
 (2)FD_ISSET:用来测试描述词组中set相关fd的位是否为真
 (3)FD_SET:用来设置词组中的set中相关的位
 (4)FD_ZERO:用来清除描述词组set的全部位

3、满足下列四个条件之一的任何一个时,一个套接字准备好读:

1)该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对于这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们使用SO_RECVLOWAT套接字选项设置套接字的低水位标记。对于TCPUDP套接字而言,其默认值为1

2)该连接的读半部关闭(也就是接收了FINTCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)

3)该套接字时一个监听套接字且已完成的连接数不为0

4)其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置为确切的错误条件。这些待处理错误也可以通过SO_ERROR套接字选项调用getsockopt获取并清除。

4 、下列四个条件的任何一个满足时,一个套接字准备好写:

1)该套接字发送缓冲区中的可用字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或该套接字已连接,或者该套接字不需要连接(UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞的,写操作将不阻塞并返回一个正值(如由传输层接收的字节数)。我们使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCPUDP而言,默认值为2048

2)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号

3)使用非阻塞式connect套接字已建立连接,或者connect已经已失败告终

4)其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置为确切的错误条件。这些待处理错误也可以通过SO_ERROR套接字选项调用getsockopt获取并清除。

5、 采用select()函数实现I/O多路复用的基本步骤如


1)清空描述符集合;

2)建立需要监视的描述符与描述符集合的联系;

3)调用select()函数;

4)检查所有需要监视的描述符,利用FD_ISSET宏判断是否已经准备好;

5)对已经准备好的描述符进行I/O操作。

6、

select(服务器代码)//同步非阻塞io(模型) 阻塞仅仅在select上 缺点每次使用都需要吧外部的fds拷贝到内核中浪费时间并且不知道具体是哪个fd有数据只知道有几个查找浪费效率(服务器代码)

具体实现select底层是通过pollfds中的fd所对应得应用驱动程序进行检查是否有数据可读并且返回可以读的个数;

7、编程代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>

#define MAXSIZE 10

int create_socket();

void fds_init(int fds[])//初始化fds数组
{
    int i = 0;
    for( ; i < MAXSIZE; i++ )
    {
        fds[i] = -1;
    }
}

void fds_add(int fds[], int fd)//给数组中添加一个新的描述符
{
    int i = 0;
    for( ;i < MAXSIZE; i++ )
    {
        if ( fds[i] == -1 )
        {
            fds[i] = fd;
            break;
        }
    }
}

void fds_del(int fds[], int fd)
{
    int i = 0;
    for( ; i < MAXSIZE; i++ )
    {
        if ( fds[i] == fd )
        {
            fds[i] = -1;
            break;
        }
    }
}
int main()
{
    int sockfd = create_socket();
    assert( sockfd != -1 );

    int fds[MAXSIZE];//设置数组最大可放MAXSIZE个
    
    fds_init(fds); //将数组全部置为-1

    fds_add(fds,sockfd);//将监听描述符放到数组中

    fd_set fdset;//创建多个文件描述符构成的集合用于存储文件描述符
    while( 1 )
    {
        FD_ZERO(&fdset);//清除所有位
        
        int maxfd = -1;//巧妙地方用于存储当前最大的描述符的值
        int i = 0;
        for( ; i < MAXSIZE; i++ )
        {
            if ( fds[i] != -1 )
            {
                FD_SET(fds[i],&fdset);//将数组中的文件描述符放到集合中
                if ( maxfd < fds[i] )
                {
                    maxfd = fds[i];//最大的描述符用于存储
                }
            }
        }

        struct timeval tv = {5,0};//设置等待时间

        int n = select( maxfd + 1, &fdset,NULL,NULL,&tv);//获取第一个参数作为fd的最大描述符
        if ( n == -1 )
        {
            perror("select error");
            continue;
        }
        else if ( n == 0 )
        {
            printf("time out\n");
            continue;
        }
        else
        {
            int i = 0;
            for( ; i < MAXSIZE; i++ )
            {
                if ( fds[i] == -1 )
                {
                    continue;
                }

                if ( FD_ISSET(fds[i],&fdset) )//一个一个fd去判断是不是该fd有数据
                {//用来测试位置是否被设置
                    if ( fds[i] == sockfd )
                    {
                        struct sockaddr_in caddr;
                        int len = sizeof(caddr);
                        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
                        if ( c < 0 )
                        {
                            perror("accept error");
                            continue;
                        }

                        printf("accept c = %d\n",c);//创建一个新的描述符让下一步去判断
                        fds_add(fds,c);//把当前描述符放进数组中
                    }
                    else
                    {
                        char buff[128] = {0};
                        int num = recv(fds[i],buff,1,0);
                        if ( num <= 0 )
                        {
                            close(fds[i]);
                            fds_del(fds,fds[i]);
                            printf("one client over\n");
                            continue;
                        }

                        printf("buff(%d)=%s\n",fds[i],buff);
                        send(fds[i],"ok",2,0);
                    }
                }
            }
        }
    }
    
}
int create_socket()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if ( sockfd == -1 )
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if ( res == -1 )
    {
        return -1;
    }

    listen(sockfd,5);

    return sockfd;
}

8、 Select 模型特点


1)可监控描述符的个数取决于sizeof(fd_set)的值。

2)将fd加入select监控集的同时,还要用一个数据结构arr保存存放在select监控集中的fd

3select需要在select前面循环arr(加fd,取maxfd)返回后循环arrFD_ISSET是否有事件发生)


9、 Select 的实现:


(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_pollsock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd

(8)把fd_set从内核空间拷贝到用户空间。

10、 Select 的缺点:


1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

3select支持的文件描述符数量太小了,默认是1024




猜你喜欢

转载自blog.csdn.net/swty3356667/article/details/80633015