网络编程:select

原理:参考:https://my.oschina.net/fileoptions/blog/911091

select中内核函数有哪些


源码实现:

#undef __NFDBITS
#define __NFDBITS    (8 * sizeof(unsigned long))

#undef __FD_SETSIZE
#define __FD_SETSIZE    1024

#undef __FDSET_LONGS
#define __FDSET_LONGS    (__FD_SETSIZE/__NFDBITS)

 
typedef struct {
    unsigned longfds_bits [__FDSET_LONGS];   //1024个bit。可以看到可以支持1024个描述符
} __kernel_fd_set;

//系统调用(内核态)
//参数为 maxfd, r_fds, w_fds, e_fds, timeout。
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)
{
    s64 timeout = -1;
    struct timeval tv;
    int ret;

    //将超时时间换成jiffies
    if (tvp) {
        if (copy_from_user(&tv, tvp, sizeof(tv))) //将用户态参数拷贝到内核态
            return -EFAULT;
         if (tv.tv_sec < 0 || tv.tv_usec < 0)
            return -EINVAL;
         /* Cast to u64 to make GCC stop complaining */
        if ((u64)tv.tv_sec >= (u64)MAX_INT64_SECONDS)
            timeout = -1;    /* infinite */
        else {
            timeout = ROUND_UP(tv.tv_usec, USEC_PER_SEC/HZ);
            timeout += tv.tv_sec * HZ;
        }
    }
    // (***) 调用 core_sys_select
    ret = core_sys_select(n, inp, outp, exp, &timeout);

    //将剩余时间拷贝回用户空间进程
    if (tvp) {
        struct timeval rtv;
        if (current->personality & STICKY_TIMEOUTS) //判断当前环境是否支持修改超时时间(不确定)
            goto sticky;
        rtv.tv_usec = jiffies_to_usecs(do_div((*(u64*)&timeout), HZ));
        rtv.tv_sec = timeout;
        if (timeval_compare(&rtv, &tv) >= 0)
            rtv = tv;
        if (copy_to_user(tvp, &rtv, sizeof(rtv))) {
sticky:
            /*
             * 如果应用程序将timeval值放在只读存储中,
             * 我们不希望在成功完成select后引发错误(修改timeval)
             * 但是,因为没修改timeval,所以我们不能重启这个系统调用。
             */
            if (ret == -ERESTARTNOHAND)
                ret = -EINTR;
        }
    }
    return ret;
}
//主要的工作在这个函数中完成
staticint core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, s64 *timeout)
{
    fd_set_bits fds;
    /*  fd_set_bits 结构如下:
     typedef struct {
         unsigned long *in, *out, *ex;
         unsigned long *res_in, *res_out, *res_ex;
    } fd_set_bits;

    这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。

     */

    void *bits;
    int ret, max_fds;
    unsigned int size;
    struct fdtable *fdt;
    /* Allocate small arguments on the stack to save memory and be faster 先尝试使用栈(因为栈省内存且快速)*/

    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];  // SELECT_STACK_ALLOC=256

    ret = -EINVAL;
    if (n < 0)
        goto out_nofds;

    /* max_fds can increase, so grab it once to avoid race */
    rcu_read_lock(); //rcu锁

    fdt = files_fdtable(current->files); //读取文件描述符表
    /*  struct fdtable 结构如下:
    struct fdtable {
       unsigned int max_fds;
       struct file **fd;
       ...
    };
     */

    max_fds = fdt->max_fds; //从files结构中获取最大值(当前进程能够处理的最大文件数目)
    rcu_read_unlock();
    if (n > max_fds)// 如果传入的n大于当前进程最大的文件描述符,给予修正
        n = max_fds;

    /* 我们需要使用6倍于最大描述符的描述符个数,
     * 分别是in/out/exception(参见fd_set_bits结构体),
     * 并且每份有一个输入和一个输出(用于结果返回) */
    size = FDS_BYTES(n);// 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
    bits = stack_fds;
    if (size > sizeof(stack_fds) / 6) { // 除以6,因为每个文件描述符需要6个bitmaps上的位。
        //栈不能满足,先前的尝试失败,只能使用kmalloc方式
        /* Not enough space in on-stack array; must use kmalloc */
        ret = -ENOMEM;
        bits = kmalloc(6 * size, GFP_KERNEL);
        if (!bits)
            goto out_nofds;
    }
    //设置fds
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;

    // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_se
    if ((ret = get_fd_set(n, inp, fds.in)) ||
        (ret = get_fd_set(n, outp, fds.out)) ||
        (ret = get_fd_set(n, exp, fds.ex)))
        goto out;

    // 对这些存放返回状态的字段清0
    zero_fd_set(n, fds.res_in);
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    // 执行do_select,完成监控功能
    ret = do_select(n, &fds, timeout);
    if (ret < 0) // 有错误
        goto out;
    if (!ret) {  // 超时返回,无设备就绪
        ret = -ERESTARTNOHAND;
        if (signal_pending(current))
            goto out;
        ret = 0;
    }

    if (set_fd_set(n, inp, fds.res_in) ||
        set_fd_set(n, outp, fds.res_out) ||
        set_fd_set(n, exp, fds.res_ex))
        ret = -EFAULT;

out:
    if (bits != stack_fds)
        kfree(bits);

out_nofds:
    return ret;
}
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
    struct poll_wqueues table;

    /*
     struct poll_wqueues {
          poll_table pt;
          struct poll_table_page *table;
          struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
          int triggered;         // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
          int error;             // 错误码
          int inline_index;      // 数组inline_entries的引用下标
          struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
    };

     */

    poll_table *wait;
    int retval, i;
    rcu_read_lock();
    //根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd。
    retval = max_select_fd(n, fds);
    rcu_read_unlock();
    if (retval < 0)
        return retval;

    n = retval;
    /* 一些重要的初始化:
       poll_wqueues.poll_table.qproc函数指针初始化,
       该函数是驱动程序中poll函数(fop->poll)实现中必须要调用的poll_wait()中使用的函数。  */
    poll_initwait(&table);
    wait = &table.pt;
    if (!*timeout)
        wait = NULL;        // 用户设置了超时时间为0
    retval = 0;

    for (;;) {
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
        long __timeout;
        set_current_state(TASK_INTERRUPTIBLE);
        inp = fds->in; outp = fds->out; exp = fds->ex;
        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
        // 所有n个fd的循环
        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
            unsigned long in, out, ex, all_bits, bit = 1, mask, j;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;
            const struct file_operations *f_op = NULL;
            struct file *file = NULL;
             // 先取出当前循环周期中的32(设long占32位)个文件描述符对应的bitmaps
            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;// 组合一下,有的fd可能只监测读,或者写,或者err,或者同时都监测
            if (all_bits == 0) {
                i += __NFDBITS; //如果这个字没有待查找的描述符, 跳过这个长字(32位,__NFDBITS=32),取下一个32个fd的循环中
                continue;

            }
            // 本次32个fd的循环中有需要监测的状态存在
            for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
                int fput_needed;
                if (i >= n)
                   break;
                if (!(bit & all_bits)) // bit每次循环后左移一位的作用在这里,用来跳过没有状态监测的fd
                   continue;

                file = fget_light(i, &fput_needed);//得到file结构指针,并增加引用计数字段f_count
                if (file) {// 如果file存在(这个文件描述符对应的文件确实打开了)
                    f_op = file->f_op;
                    mask = DEFAULT_POLLMASK;
                    if (f_op && f_op->poll) //这个文件对应的驱动程序提供了poll函数(fop->poll)。
                        mask = (*f_op->poll)(file, retval ? NULL : wait);//调用驱动程序中的poll函数。
                    /*  调用驱动程序中的poll函数,以evdev驱动中的evdev_poll()为例
                     *  该函数会调用函数poll_wait(file, &evdev->wait, wait),
                     *  继续调用__pollwait()回调来分配一个poll_table_entry结构体,
                     *  该结构体有一个内嵌的等待队列项,
                     *  设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。  */

                    fput_light(file, fput_needed);  // 释放file结构指针,实际就是减小他的一个引用计数字段f_count。
                    //记录结果。poll函数返回的mask是设备的状态掩码。
                    if ((mask & POLLIN_SET) && (in & bit)) {
                        res_in |= bit; //如果是这个描述符可读, 将这个位置位
                        retval++;   //返回描述符个数加1
                    }

                    if ((mask & POLLOUT_SET) && (out & bit)) {
                        res_out |= bit;
                        retval++;
                    }

                    if ((mask & POLLEX_SET) && (ex & bit)) {
                        res_ex |= bit;
                        retval++;
                    }
                }
                /*
                 *  cond_resched()将判断是否有进程需要抢占当前进程,
                 *  如果是将立即发生调度,这只是为了增加强占点。
                 *  (给其他紧急进程一个机会去执行,增加了实时性)
                 *  在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
                 *  cond_resched是空操作。
                 */
                cond_resched();
            }

            //返回结果
            if (res_in)
                *rinp = res_in;
            if (res_out)
                *routp = res_out;
            if (res_ex)
               *rexp = res_ex;
        }
        wait = NULL;
        if (retval || !*timeout || signal_pending(current)) // signal_pending(current)检查当前进程是否有信号要处理
            break;
        if(table.error) {
            retval = table.error;
            break;
        }
 
        if (*timeout < 0) {
            /* Wait indefinitely 无限期等待*/
            __timeout = MAX_SCHEDULE_TIMEOUT;
        } elseif (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
            /* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
            __timeout = MAX_SCHEDULE_TIMEOUT - 1;
            *timeout -= __timeout;
        } else {
            __timeout = *timeout;
            *timeout = 0;
       }

         /* schedule_timeout 用来让出CPU;
          * 在指定的时间用完以后或者其它事件到达并唤醒进程(比如接收了一个信号量)时,
          * 该进程才可以继续运行  */
        __timeout = schedule_timeout(__timeout);
        if (*timeout >= 0)
            *timeout += __timeout;
    }
    __set_current_state(TASK_RUNNING);

    poll_freewait(&table);
    return retval;
}

源码中比较重要的结构体有四个:
struct poll_wqueuesstruct poll_table_pagestruct poll_table_entrystruct poll_table_struct
每一个调用select()系统调用的应用进程都会存在一个struct poll_wqueues结构体,用来统一辅佐实现这个进程中所有待监测的fd的轮询工作,后面所有的工作和都这个结构体有关,所以它非常重要。

struct poll_wqueues {
       poll_table pt;
       struct poll_table_page *table;
       struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
       int triggered;            // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
       int error;                 // 错误码
       int inline_index;        // 数组inline_entries的引用下标
       struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};

实际上结构体poll_wqueues内嵌的poll_table_entry数组inline_entries[] 的大小是有限的,如果空间不够用,后续会动态申请物理内存页以链表的形式挂载poll_wqueues.table上统一管理。接下来的两个结构体就和这项内容密切相关:

struct poll_table_page { // 申请的物理页都会将起始地址强制转换成该结构体指针
       struct poll_table_page   *next;      // 指向下一个申请的物理页
       struct poll_table_entry  *entry;     // 指向entries[]中首个待分配(空的) poll_table_entry地址
       struct poll_table_entry  entries[0]; // 该page页后面剩余的空间都是待分配的poll_table_entry结构体
};

对每一个fd调用fop->poll() => poll_wait() => __pollwait()都会先从poll_wqueues.inline_entries[]中分配一个poll_table_entry结构体,直到该数组用完才会分配物理页挂在链表指针poll_wqueues.table上然后才会分配一个poll_table_entry结构体(poll_get_entry函数)。
poll_table_entry具体用处:函数__pollwait声明如下

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

该函数调用时需要3个参数,第一个是特定fd对应的file结构体指针,第二个就是特定fd对应的硬件驱动程序中的等待队列头指针,第3个是调用select()的应用进程中poll_wqueues结构体的poll_table项(该进程监测的所有fd调用fop->poll函数都用这一个poll_table结构体)。

struct poll_table_entry {
       struct file     *filp;                 // 指向特定fd对应的file结构体;
       unsigned long   key;                   // 等待特定fd对应硬件设备的事件掩码,如POLLIN、 POLLOUT、POLLERR;
       wait_queue_t    wait;                  // 代表调用select()的应用进程,等待在fd对应设备的特定事件 (读或者写)的等待队列头上,的等待队列项;
       wait_queue_head_t   *wait_address;     // 设备驱动程序中特定事件的等待队列头(该fd执行fop->poll,需要等待时在哪等,所以叫等待地址);
};


API

Select函数的声明:

int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
  • maxfd 表示的是待测试的描述符基数,它的值是待测试的最大描述符加 1。比如:描述字集合{0,1,4},对应的 maxfd 是 5,而不是 4

很多系统是用一个整型数组来表示一个描述字集合的,一个 32 位的整型数可以表示 32 个描述字,例如第一个整型数表示 0-31 描述字,第二个整型数可以表示 32-63 描述字,以此类推。

  • 紧接着的是三个描述符集合,分别是读描述符集合 readset、写描述符集合 writeset 和异常描述符集合 exceptset,这三个分别通知内核,在哪些描述符上检测数据可以读,可以写和有异常发生。
void FD_ZERO(fd_set *fdset);      
void FD_SET(int fd, fd_set *fdset);  
void FD_CLR(int fd, fd_set *fdset);   
int  FD_ISSET(int fd, fd_set *fdset);
  • FD_ZERO 用来将这个向量的所有元素都设置成 0;
  • FD_SET 用来把对应套接字 fd 的元素,a[fd]设置成 1;
  • FD_CLR 用来把对应套接字 fd 的元素,a[fd]设置成 0;
  • FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd]是 0 还是 1。

其中 0 代表不需要处理,1 代表需要处理。

  • 最后一个参数是 timeval 结构体时间:
struct timeval {
  long   tv_sec; /* seconds */
  long   tv_usec; /* microseconds */
};

这个参数设置不同的值,会有不同的可能:

  • 第一个可能是设置成空 (NULL),表示如果没有 I/O 事件发生,则 select 一直等待下去。
  • 第二个可能是设置一个非零的值,这个表示等待固定的一段时间后从 select 阻塞调用中返回
  • 第三个可能是将 tv_sec 和 tv_usec 都设置成 0,表示根本不等待,检测完毕立即返回。这种情况使用得比较少。

实践

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 1024
#define SERV_PORT 43211

tcp_client(char *address, int port)
{
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, address, server_addr.sin_addr);

    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int connfd_rt  = connect(socket_fd, (struct sockaddr *)&client_addr, client_len);
    if(connfd_rt < 0)
    {
        perror("connect failed");
        return -1;
    }
    return socket_fd;
}

int main(int argc, char *argv[])
{   
    if(argc != 2)
    {
        perror("usage:select <IPaddress>");
        return -1;
    }

    int socket_fd;
    socket_fd = tcp_client(argv[1], SERV_PORT);

    char recv_line[MAXLINE], send_line[MAXLINE];
    int n;

    fd_set readmask;
    fd_set allreads;
    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);

    for(;;)
    {
        readmask = allreads;
        int rc = select(socket_fd+1, &readmask, NULL, NULL, NULL);
        if(rc <= 0)
        {
            perror("select failed");
            return -1;
        }

        if(FD_ISSET(socket_fd, &readmask))
        {
            n = read(socket_fd, recv_line, MAXLINE);
            if(n < 0)
            {
                perror("read error");
                return -1;
            }
            else if(n == 0)
            {
                printf("server terminated\n");
                return 0;
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }

        if(FD_ISSET(STDIN_FILENO, &readmask))
        {
            if(fgets(send_line, MAXLINE, stdin) != NULL)
            {
                int i = strlen(send_line);
                if(send_line[i - 1] == '\n')
                {
                    send_line[i - 1] = 0;
                }
                printf("now sending %s\n",send_line);
                ssize_t rt = write(socket_fd, send_line, strlen(send_line));
                if(rt < 0)
                {
                    perror("write failed");
                    return -1;
                }
                printf("send bytes: %zu \n",rt);
            }
        }
    }
}

通过 FD_ZERO 初始化了一个描述符集合,这个描述符读集合是空的:

分别使用 FD_SET 将描述符 0,即标准输入,以及连接套接字描述符 3 设置为待检测:

通过 select 来检测套接字描述字有数据可读,或者标准输入有数据可读。比如,当用户通过标准输入使得标准输入描述符可读时,返回的 readmask 的值为:

这个时候 select 调用返回,可以使用 FD_ISSET 来判断哪个描述符准备好可读了。如上图所示,这个时候是标准输入可读
select 调用每次完成测试之后,内核都会修改描述符集合,通过修改完的描述符集合来和应用程序交互,应用程序使用 FD_ISSET 来对每个描述符进行判断
使用 socket_fd+1 来表示待测试的描述符基数。切记需要 +1。套接字描述符就绪条件

小结

  • 描述符基数是当前最大描述符 +1;
  • 每次 select 调用完成之后,记得要重置待测试集合。

select的缺点:

  • 内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。
  • 每次调用select,都需把fd集合从用户态拷贝到内核态,fd很多时开销就很大
  • 同时每次调用select都需在内核遍历传递进来的所有fd,fd很多时开销就很大
  • select支持的文件描述符数量太小了,默认最大支持1024个
  • 主动轮询效率很低
posted @ 2022-03-20 20:50  牛犁heart  阅读(287)  评论(0编辑  收藏  举报