ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

网络编程:select

2022-03-20 21:05:38  阅读:192  来源: 互联网

标签:set struct 编程 网络 fds fd table poll 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个
  • 主动轮询效率很低

标签:set,struct,编程,网络,fds,fd,table,poll,select
来源: https://www.cnblogs.com/whiteBear/p/16029394.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有