Linux网络编程的5种IO模型一多路复用Select

前言:我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO,对于其中的 阻塞/非阻塞IO 进行了说明。

这一讲我们来看 多路复用机制。

IO复用模型 ( I/O multiplexing )

所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要额外的功能来配合: select、poll、epoll。

select、poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

select时间复杂度O(n):它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

poll时间复杂度O(n):poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

epoll时间复杂度O(1):epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

在多路复用IO模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

小编推荐自己的linuxC/C++语言技术交流群:【1106675687】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!
在这里插入图片描述

select

使用select来监视文件描述符时,要向内核传递的信息包括:

  • 1、我们要监视的文件描述符个数
  • 2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。
  • 3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢?(可以永远等待,等待一段时间,或者不等待直接返回)
  • 4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。

fd_set 模型的原理(理解select模型的关键在于理解fd_set,假设取fd_set长度为1字节)

  • fd_set中的每一位可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  • 执行FD_ZERO(&set);则set用位表示是0000,0000。

  • 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
    若再加入fd=2,fd=1,则set变为0001,0011 、执行select(6,&set,0,0,0)阻塞等待…

  • 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)

select 实现原理

1、使用copy_from_user从用户空间拷贝fd_set到内核空间
2、注册回调函数__pollwait
3、遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_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从内核空间拷贝到用户空间。

select的几大缺点:
1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3)select支持的文件描述符数量太小了,默认是1024

Linux网络编程的5种IO模型一多路复用Select_第1张图片

/* According to POSIX.1-2001, POSIX.1-2008 */
#include 

/* According to earlier standards */
#include 
#include 
#include 

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);	//添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set);			//清空集合中的文件描述符,将每一位都设置为0;

#include 

int pselect(int nfds, fd_set *readfds, fd_set *writefds,
     fd_set *exceptfds, const struct timespec *timeout,
     const sigset_t *sigmask);

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

struct timespec{
     
    time_t tv_sec;//秒
    long tv_nsec;//纳秒
}

select和pselect有三个主要的区别:

1、select超时使用的是struct timeval,用秒和微秒计时,而pselect使用struct timespec ,用秒和纳秒。

2、select会更新超时参数timeout 以指示还剩下
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);

sigmask:这个参数保存了一组内核应该打开的信号(即:从调用线程的信号掩码中删除)

当pselect的sigmask 为 NULL时pselect和select一样;当sigmask!=NULL时,等效于以下原子操作:

sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);

接收信号的程序通常只使用信号处理程序来引发全局标志。全局标志将指示事件必须被处理。在程序的主循环中。一个信号将导致select和pselect返回-1 并将erron=EINTR。

我们经常要在主循环中处理信号,主循环的某个位置将会检查全局标志,那么我们会问:如果信号在条件之后,select之前到达怎么办。答案是select会无限期阻塞。

这种情况很少见,但是这就是为什么出现了pselect。因为他是类似原子操作的。

描述: 允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种 。

参数说明:

  • nfds: 一个整数值, 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1。

注意, 待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1
readfds/writefds/exceptfds:这些都是fd_set类型的,代表文件描述符集合; 可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。

  • readfds:监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。
  • writefds:监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。
  • exceptfds:用来监视发生错误异常文件
  • timeout: 表示select返回之前的时间上限。 设为0(NULL),代表无期限等待下去。这个等待可以被一个信号中断,只有当一个描述符准备好,或者捕获到一个信号时函数才会返回。如果是捕获到信号,select返回-1,并将变量errno设置成EINTR。

如果timeout ->tv_sec 为0 且 timeout->tv_sec 为0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。

返回值

  • 成功时:返回三种描述符集合中”准备好了“的文件描述符数量。
  • 超时:返回0
  • 错误:返回-1,并设置 errno
  • EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。
  • EINTR:捕获到一个信号。
  • EINVAL:nfds是负的或者timeout中包含的值无效。
  • ENOMEM:无法为内部表分配内存。
  • 例程:基于 select的 TCP 服务器

例程:基于 select的 TCP 服务器

server.c

/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  server.c
#    Created  :  Sat 25 Mar 2020 14:43:39 PM CST
*/

#include 
#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 

typedef struct _info {
     
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
     
    int my_socket;
    unsigned int len;
    int ret, i, j;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
    if(my_socket == -1) {
      perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 用于接收消息
    info buf ={
     0};

    // 指定地址
    struct sockaddr_in addr = {
     0};
    addr.sin_family = AF_INET;  // 地址协议族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定端口号

    int set = 1;
    int get = 0;
    int getlen = 0;

    // 服务器 绑定
    bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));

    ret = listen(my_socket, 10);
    if(-1 == ret) {
      perror("listen"); }
    printf("Listening\n");


    int connect_sockets[100] = {
     0}; // 我们规定,为 0 的成员为无效socket
    int connected_cnt = 0;

    //struct sockaddr_in new = {0};
    //int new_addr_size = {0};

    fd_set read_sets;

    int max_fd = my_socket; // 一开始时,只有 一个新的 文件描述:my_socket ,所以它是最大的

    while(1) // 在循环中等待连接请求
    {
     
        FD_ZERO(&read_sets); // 每次都需要初始化
        FD_SET(my_socket, &read_sets); // 添加 要监听的 socket

        // 添加 之后经过 connect 过来的 套接字数组(一般在第一次循环时是空的)
        for( i = 0; i < connected_cnt; i++)
        {
     
            if(connect_sockets[i])
            {
     
                FD_SET(connect_sockets[i], &read_sets); // 添加经过accept保存下来,需要进行读响应的套接字到集合中
            }
        }

        // 设置监听超时时间
        // timeout.tv_sec  = 2;
        // timeout.tv_usec = 0;

        ret = select(max_fd + 1, &read_sets, NULL, NULL, NULL);
        // 判断返回值
        switch (ret) {
     
            case 0 :
                printf("Time out.\n"); // 监听超时
                break;
            case -1 :
                printf("Err occurs.\n"); // 监听错误
                break;
            default :
                if(FD_ISSET(my_socket, &read_sets)) //这个是原的被动socket,如果是它,则 意味着有新的连接进来了
                {
     
                    connect_sockets[connected_cnt] = accept(my_socket, NULL, NULL);
                    max_fd = connect_sockets[connected_cnt];
                    printf("New socket is %d\n", connect_sockets[connected_cnt]);
                    connected_cnt ++;
                    printf("Now we has [%d] connecter\n", connected_cnt);
                }else{
      // 如果不是 被动socket,那么就意味着是 现有的连接 有消息发来(我们有数据可读)
                    printf("New message came in.\n");

                    // 求出是那个文件描述符可读
                    for(i = 0; i < connected_cnt; i++)
                    {
     
                        if(FD_ISSET(connect_sockets[i], &read_sets) == 1)      break;
                    }
                    if( i >= connected_cnt) {
      continue; }

                    printf("Socket [%d] send to server.\n", connect_sockets[i]);

                    // 接收消息
                    ret = recv(connect_sockets[i], &buf, sizeof(buf), 0);
                    if( ret <= 0 )
                    {
     
                        // 远程客户端断开处理(如果不处理,会导致服务器也断开)
                        printf("[%d]/[%d] Client [%d] disconnected.\n", i+1, connected_cnt, connect_sockets[i]);
                        close(connect_sockets[i]);
                        // 我们需要将对应的客户端从数组中移除  且 连接数 -1 (移除的方法: 数组成员前移覆盖)
                        for (j = i; j < connected_cnt - 1; ++j)
                        {
     
                            connect_sockets[j] = connect_sockets[ j + 1];
                        }
                        connected_cnt --;
                    }

                    // 打印消息
                    printf("[%s] : %s\n", buf.name, buf.text);
                    // 回复消息
                    sprintf(buf.name, "Server");
                    sprintf(buf.text, "Had recvied your[%d] message", connect_sockets[i]);
                    //sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
                    send(connect_sockets[i], &buf, sizeof(buf), 0);
                }
                break;
        }
        printf("while loop\n");
    }

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    for(i = 0; i < connected_cnt; i++)
    {
     
        close(connect_sockets[i]); perror("close");
    }
    return close(my_socket);
}

**client.c**

``/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  client.c
#    Created  :  Sat 25 Mar 2020 14:44:19 PM CST
*/

#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 

typedef struct _info {
     
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
     
    int my_socket;
    unsigned int len;
    int ret, i = 0;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
    if(my_socket == -1) {
      perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);


    // 用于接收消息
    info buf ={
     0};

    // 指定地址
    struct sockaddr_in addr = {
     0};
    addr.sin_family = AF_INET;  // 地址协议族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定端口号



    int new_socket;
    struct sockaddr_in new = {
     0};
    int new_addr_size;

    connect(my_socket, (struct sockaddr *)(&addr), sizeof(struct sockaddr_in));
    if(-1 == ret) {
      perror("connect"); }
    printf("connected\n");

    // 回复消息
    sprintf(buf.name, "Client");
    sprintf(buf.text, "Hello tcp text.");
    //sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    send(my_socket, &buf, sizeof(buf), 0);
    perror("sendto");

    // 接收并打印消息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    recv(my_socket, &buf, sizeof(buf), 0);
    perror("recvfrom");

    printf("[%s] : %s\n", buf.name, buf.text);

    for (i = 0; i < 5; ++i)
    {
     
        sleep(2);

        // 回复消息
        sprintf(buf.name, "Client");
        sprintf(buf.text, "Hello tcp text [%d].", i++);
        //sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
        send(my_socket, &buf, sizeof(buf), 0);
        perror("sendto");

        // 接收并打印消息
        //recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
        recv(my_socket, &buf, sizeof(buf), 0);
        printf("[%s] : %s\n", buf.name, buf.text);
        perror("recvfrom");
    }

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}


你可能感兴趣的:(内核,epoll,网络,linux,c++)