linux select的使用及其原理实现

linux select和poll的实现机制其实是类似的,而且最终都会使用到内核驱动提供的具体poll方法,poll的使用详解参考这篇文章:

https://blog.csdn.net/oqqYuJi12345678/article/details/106313768

1 应用层使用举例

1.1函数原型

int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);

1.2函数说明

select()用来等待文件描述词状态的改变。参数n代表最大的文件描述词加1,参数readfds、writefds和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。底下的宏提供了处理这三种描述词组的方式:

FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd的位

FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd的位是否为真

FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位

FD_ZERO(fd_set *set);用来清除描述词组set的全部位

1.3结构体说明

先说明两个结构体:
1) struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。
fd_set集合可以通过一些宏由人为来操作,比如 
清空集合FD_ZERO(fd_set *);
将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set*);
将一个给定的文件描述符从集合中删除FD_CLR(int,fd_set*);
检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。
2) struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。如下所示:

struct timeval

{

time_t tv_sec;

time_t tv_usec;

};


1.4.具体参数说明:

   1) int n:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
    2) fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
     3) fd_set*writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
     4) fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
     5)struct timeval *timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

1.5返回值

负值:select错误

正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件

如果参数timeout设为NULL则表示select()没有timeout。

1.6错误代码

执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。

EBADF 文件描述词为无效的或该文件已关闭

EINTR 此调用被信号所中断

EINVAL 参数n为负值。

ENOMEM 核心内存不足

1.7 范例

main() 
{ 
    int sock; 
    FILE *fp; 
    struct fd_set fds; 
    struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
    char buffer[256]={0}; //256字节的接收缓冲区
    /* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
    sock=socket(...); 
    bind(...); 
    fp=fopen(...); */ 
    while(1) 
   { 
        FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
        FD_SET(sock,&fds); //添加描述符
        FD_SET(fp,&fds); //同上
        maxfdp=sock>fp?sock+1:fp+1;    //描述符最大值加1
        switch(select(maxfdp,&fds,&fds,NULL,&timeout))   //select使用
        { 
            case -1: exit(-1);break; //select错误,退出程序
            case 0:break; //再次轮询
            default: 
                  if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
                  { 
                        recvfrom(sock,buffer,256,.....);//接受网络数据
                        if(FD_ISSET(fp,&fds)) //测试文件是否可写
                            fwrite(fp,buffer...);//写入文件
                         buffer清空;
                   }// end if break; 
          }// end switch 
     }//end while 
}//end main

2 内核实现详解

 typedef struct {  
    unsigned long *in, *out, *ex;  
    unsigned long *res_in, *res_out, *res_ex;  
} fd_set_bits;  
//  long sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp)  
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,  
                fd_set __user *, exp, struct timeval __user *, tvp)  
{  
    struct timespec end_time, *to = NULL;  
    struct timeval tv;  
    int ret;  
    if (tvp) {  
        if (copy_from_user(&tv, tvp, sizeof(tv))) {  
            return -EFAULT;  
        }  
        // 计算超时时间  
        to = &end_time;  
        if (poll_select_set_timeout(to,  
                                    tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),  
                                    (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC)) {  
            return -EINVAL;  
        }  
    }  
    ret = core_sys_select(n, inp, outp, exp, to);  
    // 复制剩余时间到用户空间  
    ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);  
    return ret;  
}  
 int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,  
                    fd_set __user *exp, struct timespec *end_time)  
{  
    fd_set_bits fds;  
    void *bits;  
    int ret, max_fds;  
    unsigned int size;  
    struct fdtable *fdt;  
    //小对象使用栈上的空间,节约内存, 加快访问速度  
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];  
  
    ret = -EINVAL;  
    if (n < 0) {  
        goto out_nofds;  
    }  
  
    rcu_read_lock();  
    // 取得进程对应的 fdtable  
    fdt = files_fdtable(current->files);  
    max_fds = fdt->max_fds;  
    rcu_read_unlock();  
    if (n > max_fds) {  
        n = max_fds;  
    }  
  
    size = FDS_BYTES(n);  
    bits = stack_fds;  
    if (size > sizeof(stack_fds) / 6) {  
        // 栈上的空间不够, 申请内存, 全部使用堆上的空间  
        ret = -ENOMEM;  
        bits = kmalloc(6 * size, GFP_KERNEL);  
        if (!bits) {  
            goto out_nofds;  
        }  
    }  
    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;  
  
    // 复制用户空间到内核  
    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;  
    }  
    // 初始化fd set  
    zero_fd_set(n, fds.res_in);  
    zero_fd_set(n, fds.res_out);  
    zero_fd_set(n, fds.res_ex);  
  
    ret = do_select(n, &fds, end_time);  
  
    if (ret < 0) {  
        goto out;  
    }  
    if (!ret) {  
        // 该返回值会被系统捕获, 并以同样的参数重新调用sys_select()  
        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;  
}  


 int do_select(int n, fd_set_bits *fds, struct timespec *end_time)  
{  
    ktime_t expire, *to = NULL;  
    struct poll_wqueues table;  
    poll_table *wait;  
    int retval, i, timed_out = 0;  
    unsigned long slack = 0;  
  
    rcu_read_lock();  
    // 检查fds中fd的有效性, 并获取当前最大的fd  
    retval = max_select_fd(n, fds);  
    rcu_read_unlock();  
  
    if (retval < 0) {  
        return retval;  
    }  
    n = retval;  
  
    // 初始化 poll_wqueues 结构, 设置函数指针_qproc    为__pollwait  
    poll_initwait(&table);  
    wait = &table.pt;  
    if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {  
        wait = NULL;  
        timed_out = 1;  
    }  
  
    if (end_time && !timed_out) {  
        // 估计需要等待的时间.  
        slack = select_estimate_accuracy(end_time);  
    }  
  
    retval = 0;  
    for (;;) {  
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;  
  
        inp = fds->in;  
        outp = fds->out;  
        exp = fds->ex;  
        rinp = fds->res_in;  
        routp = fds->res_out;  
        rexp = fds->res_ex;  
        // 遍历所有的描述符, i 文件描述符 ,n为所有描述符中值最大的那个,这样会有一个问题
        //没有设置进select 但是值小于n的描述符也会被遍历,有一定的效率问题,但这样实现比较简单
        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;  
            // 检查当前的 slot 中的描述符  
            in = *inp++;  
            out = *outp++;  
            ex = *exp++;  
            all_bits = in | out | ex;  
            if (all_bits == 0) { // 没有需要监听的描述符, 下一个slot  
                i += __NFDBITS;  
                continue;  
            }  
  
            for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {  
                int fput_needed;  
                if (i >= n) {  
                    break;  
                }  
                // 不需要监听描述符 i  
                if (!(bit & all_bits)) {  
                    continue;  
                }  
                // 取得文件结构  
                file = fget_light(i, &fput_needed);  
                if (file) {  
                    f_op = file->f_op;  
                    // 没有 f_op 的话就认为一直处于就绪状态  
                    mask = DEFAULT_POLLMASK;  
                    if (f_op && f_op->poll) {  
                        // 设置等待事件的掩码 
-----------------------------------------------------------(1) 
                        wait_key_set(wait, in, out, bit);  
                        /* 
                        static inline void wait_key_set(poll_table *wait, unsigned long in, 
                        unsigned long out, unsigned long bit) 
                        { 
                        wait->_key = POLLEX_SET;// (POLLPRI) 
                        if (in & bit) 
                        wait->_key |= POLLIN_SET;//(POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR) 
                        if (out & bit) 
                        wait->_key |= POLLOUT_SET;//POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR) 
                        } 
                        */  
                        // 获取当前的就绪状态, 并添加到文件的对应等待队列中  
----------------------------------------------------------------------------(2)
                        mask = (*f_op->poll)(file, wait);  
                        // 和poll完全一样  
                    }  
                    fput_light(file, fput_needed);  
                    // 释放文件  
                    // 检查文件 i 是否已有事件就绪,  
                    if ((mask & POLLIN_SET) && (in & bit)) {  
                        res_in |= bit;  
                        retval++;  
                        // 如果已有就绪事件就不再向其他文件的  
                        // 等待队列中添加回调函数  
                        wait = NULL;  
                    }  
                    if ((mask & POLLOUT_SET) && (out & bit)) {  
                        res_out |= bit;  
                        retval++;  
                        wait = NULL;  
                    }  
                    if ((mask & POLLEX_SET) && (ex & bit)) {  
                        res_ex |= bit;  
                        retval++;  
                        wait = NULL;  
                    }  
                }  
            }  
-----------------------------------------------------------(3)
            if (res_in) {  
                *rinp = res_in;  
            }  
            if (res_out) {  
                *routp = res_out;  
            }  
            if (res_ex) {  
                *rexp = res_ex;  
            }  
            cond_resched();  
        }  
        wait = NULL; // 该添加回调函数的都已经添加了  
---------------------------------------------------------------------(4)
        if (retval || timed_out || signal_pending(current)) {  
            break;   // 信号发生,监听事件就绪或超时  
        }  
        if (table.error) {  
            retval = table.error; // 产生错误了  
            break;  
        }  
        // 转换到内核时间  
        if (end_time && !to) {  
            expire = timespec_to_ktime(*end_time);  
            to = &expire;  
        }  
        // 等待直到超时, 或由回调函数唤醒, 超时后会再次遍历文件描述符  
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,  
                                   to, slack)) {  
            timed_out = 1;  
        }  
    }  
  
    poll_freewait(&table);  
  
    return retval;  
}  

从core_sys_select可以看出,select的实现和poll机制类似,最终会调用驱动程序的poll实现,不过相比于poll来说,select监视的事件会不那么灵活,仅设置了in,out,和ex集中状态

(1)根据传入的参数,设置相应的监听事件,可以看到in,out和ex分别对应一些事件的集合:

#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

(2)调用 驱动的poll方法,如果驱动有数据,返回的mask不为0

(3)当监听的事件就绪时,把值传回给文件描述符,用于向用户层返回就绪事件

(4)遍历完所有文件以后,如果有就绪文件,则跳出for 死循环,不会睡眠,返回用户层,否则睡眠,等待唤醒,执行下一次扫描过程

该文整理自:

https://blog.csdn.net/brandohero/article/details/39099883

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值