select

来源:互联网 发布:金鼎智赢交易软件 编辑:程序博客网 时间:2024/06/02 05:03

select的使用

I/O多路转接

while((n = read(socketfd, buf, BUFSIZE) > 0)    if (write(STDOUT_FILENO, buf, n) != n)        err_sys(“write error”);

首先来看上面简单的I/O读写过程。当socketfd对应的文件表项持续阻塞时,会导致整个程序阻塞直到有数据到达。对于要读写多个socket的服务器程序来说,单一socket 阻塞会影响与其它socket的交互。

对于这种情况,可能我们会想到将 socketfd 设置为非阻塞模式,依次利用read函数检查是否有数据到来,有则返回数据长度,没有则返回-1,并向下执行。这种方式看似合理,但实际上非阻塞I/O给予各 socketfd 相同的等待机会,亦可称之为使用“轮询”的方式,read“轮询”时其实大部分时间是没有数据的,这样就浪费了CPU时间。

对于上述问题,比较好的技术是使用I/O多路转接(I/O multiplexing)。使用这种技术,先构造一个感兴趣的描述符列表,然后调用一个函数,指导这些描述符中的一个已经准好进行I/O时,该函数才返回。

select函数的使用

  1. 传给select函数的参数告诉内核
    关心的描述符、每个描述符关心的条件(读、写、异常)、愿意等待的时间(永远、不等待、固定时间)。

  2. 从select函数返回,内核告知
    已准备好的描述符总数量、对于读、写、异常这3个条件中的每一个,哪些已经准备好。

#include <sys/select.h>int select( int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, sturct timeval *tvptr);

maxdp1:最大文件描述符编号值加1,它的用处是指的我们所关注的最大描述符,方便内核只在此范围内寻找打开的位。

readfds、writefds、exceptfds:指向描述符集的指针。分别说明了我们关心的可读、可写或处于异常的描述符集合,存储于 fd_set 数据类型,此数据类型一般可认为是一个很大的字节数组,为每个描述符保持一位。如下四个函数用来进行描述符集中描述符的操作。

#include <sys/select.h>void FD_ZERO(fd_set *fdset);        /* 清空fdset中的所有位 */void FD_SET(int fd, fd_set *fdset); /* 在fdset中打开fd所对应的位 */void FD_CLR(int fd, fd_set *fdset); /* 在fdset中关闭fd所对应的位 */int FD_ISSET(int fd, fd_set *fdset);/* 测试fd是否在fdset中 */

这三个参数中的任一个(或全部)可以是空指针,表示对相应的条件不关心。值得一提的是:如果三个指针全部为空,则select函数提供了比sleep更精确的计时器(sleep等待整数秒,而select函数可以等待少于1秒的时间,具体时间粒度取决于系统时钟)。
tvptr:愿意等待的时间长度。

struct timeval{long tv_sec; /* seconds */long tv_usec; /* and microseconds */} ;

tvptr = = NULL:永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select 返回- 1 , errno 设置为 EINTR 。

tvptr->tvsec = = 0 && tvptr->tvusec = = 0:完全不等待。测试所有指定的描述符并立即返回。这是得到多个描述符的状态而不阻塞 s e l e c t 函数的轮询方法。

tvptr->tvsec ! =0 | | tvptr- >tvusec! =0:等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是 0 ,(如果系统不提供微秒分辨率,则 tvptr- >tvusec 值取整到最近的支持值。)与第一种情况一样,这种等待可被捕捉到的信号中断。

select使用示例

int sock = 0;struct sockaddr_in addrLocal;fd_set rset;struct timeval tv;sock = socket(AF_INET, SOCK_DGRAM, 0);if(ERROR == sock){return 0;}addrLocal.sin_family    = AF_INET;addrLocal.sin_port  =  htons(dnsPort);addrLocal.sin_addr.s_addr = htonl(INADDR_ANY);bind(sock,( struct sockaddr *)&addrLocal,sizeof(addrLocal));tv.tv_sec   = timeout;tv.tv_usec  = 0;FD_ZERO(&rset);FD_SET(sock,&rset);retVal = select(sock + 1, &rset, NULL, NULL, &tv);if (FD_ISSET(sock, &rset)){    recvfrom(sock, …);}

select原理实现

等待队列

对于I/O操作,无论是阻塞I/O还是非阻塞I/O都可能会出现等待I/O就绪的过程,其中非阻塞I/O等待过程可以存在于select(poll、epoll)系统调用过程中。进程在阻塞等待时,通常设备驱动程序默认将该进程置入休眠状态直到请求可继续。要将进程置为休眠的一个前提是,有地方可以将其唤醒,Linux中利用等待队列来维护进入休眠状态而在某特定条件下需要唤醒的进程。

等待队列由循环链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头(wait queue head),等待队列头是一个类型为wait_queue_head_t的数据结构。其定义在linux/wait.h文件中。

struct __wait_queue_head {      spinlock_t lock;      struct list_head task_list;  }; typedef struct __wait_queue_head wait_queue_head_t;  

使用等待队列时首先需要定义一个wait_queue_head,可以通过如下静态或动态方法定义。

/* 静态初始化 */DECLARE_WAIT_QUEUE_HEAD(name);/* 动态初始化 */声明:wait_queue_head_t  wait_que;初始化:init_waitqueue_head( &wait_que);

Linux中等待队列的实现思想如下图 2 1所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。

等待队列
图 等待队列

等待队列结点中存放的是在执行设备操作时不能获得资源而挂起的进程。

struct __wait_queue {      unsigned int flags;  #define WQ_FLAG_EXCLUSIVE   0x01      void *private;              /* 当前进程控制块 */    wait_queue_func_t func;     /* 唤醒阻塞进程函数,决定了唤醒的方式*/    struct list_head task_list;};typedef struct __wait_queue wait_queue_t;  typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);/* 声明等待队列结点并出示为name */#define DECLARE_WAITQUEUE(name, tsk)    \wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)#define __WAITQUEUE_INITIALIZER(name, tsk) {    \ .private        = tsk,                        \ .func           = default_wake_function,      \ .task_list      = { NULL, NULL } }/* 对特定的成员进行赋值 */static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p){  q->flags = 0;  q->private = p;                  /* 私有数据指针 */  q->func = default_wake_function;  /* 使用默认的唤醒函数 */}static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func){  q->flags = 0;  q->private = NULL;  q->func = func;       /* 自定义的唤醒函数 */}

等待队列编程接口。

/* 宏,让当前任务处于等待事件状态。@wq:等待队列;@conditions:等待条件 */wait_event(wq, condition)/* 功能与wait_event类似,多了一个超时机制 */wait_event_timeout(wq, condition, timeout)/* 宏,该宏定义的等待能够被信号唤醒。如果被信号唤醒,那么返回- ERESTARTSYS。@wq:等待队列;@condition:等待条件;@rt:返回值 */wait_event_interruptible(wq, condition)/* 多了超时机制 */wait_event_interruptible_timeout(wq, condition, timeout)/* 唤醒等待队列中的一个任务。@x:等待队列 */wake_up(x)/* 用于唤醒wake_event_interruptible()睡眠的进程 */wake_up_interruptible(x)/*唤醒等待队列中的所有任务 */wake_up_all(x)

设备驱动poll方法

select使用时监控的文件描述符集中,每个文件描述符都对应着一个文件,select实现的支持需要来自文件设备驱动程序的相应支持,即设备驱动程序提供的poll方法。
在Linux系统中一切皆文件,对应着struct file数据结构。那么,对于不同文件系统或设备,file结构又是如何统一表示呢?

struct file {      const struct file_operations    *f_op;      spinlock_t          f_lock;      void               *private_data;  #ifdef CONFIG_EPOLL      /* Used by fs/eventpoll.c to link all the hooks to this file */      struct list_head    f_ep_links;      struct list_head    f_tfile_llink;  #endif /* #ifdef CONFIG_EPOLL */      /* 其他内容 */};struct file_operations {/* poll方法,提供给poll/select/epoll调用,以获取文件状态,以及就绪通知 */      unsigned int (*poll) (struct file *, struct poll_table_struct *);      /* 其他方法,如read、write */  };

可以看到file结构中有两个数据成员file_oprations 和 private_data,其中file_operations是一系列文件操作方法的函数指针集合,它在创建文件时由设备驱动实现,其中就包含了供poll、select、epoll系统调用使用的poll方法,用于获取文件状态和通知文件就绪状态变化。而private_data中存储了文件系统或设备特有的数据。
来看一个poll方法的简单示例。

unsigned int file_f_op_poll (struct file *filp, struct poll_table_struct *wait){    unsigned int mask = 0;    wait_queue_head_t * wait_queue;    /* 根据事件掩码wait->key_和文件实现filep->private_data取得事件掩码对应的一个或多个wait queue head */    some_code();    /* 调用poll_wait 向获得的wait queue head 添加节点 */    poll_wait(filp, wait_queue, wait);    /* 取得当前就绪状态保存到mask */some_code();    return mask;}

其中使用到的poll_table_struct结构,用以select/poll/epoll向文件注册就绪后回调结点。

typedef struct poll_table_struct {    /* 向wait_queue_head添加回调节点(wait_queue_t)的接口函数 */poll_queue_proc _qproc;    /* 关注的事件掩码,文件的实现利用此掩码将等待队列传递给_qproc */    unsigned long   _key;} poll_table;typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

究竟poll_table_struct是怎么用的呢,我们来看看poll_wait函数的实现。

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p){    if (p && p->_qproc && wait_address) {        /* 调用_qproc向等待队列头添加节点,并设置节点的func */        p->_qproc(filp, wait_address, p);    }}

总结来讲,就是利用设备驱动提供的poll方法,我们可以取得文件某个事件掩码对应的等待队列,并将当前进程current添加到此等待队列上,同时设置current对应的等待队列结点的唤醒函数。
当文件状态改变时,文件会调用_wake_up函数,我们来看下。

/* 通过调用等待队列结点的唤醒函数来通知poll的调用者,其中key是文件当前的事件掩码 */void __wake_up(wait_queue_head_t *q, unsigned int mode,               int nr_exclusive, void *key){    unsigned long flags;    spin_lock_irqsave(&q->lock, flags);    __wake_up_common(q, mode, nr_exclusive, 0, key);    spin_unlock_irqrestore(&q->lock, flags);}static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key){    wait_queue_t *curr, *next;    /* 遍历并调用func 唤醒, 通常func会唤醒调用poll的进程 */    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {        unsigned flags = curr->flags;        if (curr->func(curr, mode, wake_flags, key) &&                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) {            break;        }    }}

select函数实现

select系统调用与poll系统调用的功能相同,几乎是同时由两个不同的Unix团体分别实现,故保留了两个函数。其实现方法亦基本一致。基本流程如下图 2 2所示。

select函数基本流程
图 select函数基本流程

关键数据结构

由于select函数通常监控多个文件描述符,故将poll_table进行了具体化实现,以方便实现内存管理。

/* select/poll对poll_table的具体化实现 */struct poll_wqueues {    poll_table pt;    struct poll_table_page *table;    /* 如果inline_entries 空间不足, 从poll_table_page中分配 */    struct task_struct *polling_task; /* 调用poll 或select 的进程 */    int triggered;                    /* 已触发标记 */    int error;      int inline_index;            /* 下一个要分配的inline_entrie索引 */    struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];};  /* 帮助管理select/poll申请的内存 */struct poll_table_page {      struct poll_table_page  * next;       /* 下一个 page */    struct poll_table_entry * entry;      /* 指向第一个entries */    struct poll_table_entry entries[0];};/* 与一个正在poll/select的文件相关联 */struct poll_table_entry {    struct file *filp;               /* 在poll/select中的文件 */    unsigned long key;      wait_queue_t wait;             /* 插入到wait_queue_head_t的节点*/    wait_queue_head_t *wait_address; /*文件的wait_queue_head_t地址*/};

基础函数

/* poll_table初始化函数 */static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc){pt->_qproc = qproc;     pt->_key   = ~0UL;}/* poll_wqueues的初始化 */void poll_initwait(struct poll_wqueues *pwq){      /* 初始化poll_table, 相当于调用基类的构造函数 */    init_poll_funcptr(&pwq->pt, __pollwait);    pwq->polling_task = current;    pwq->triggered = 0;    pwq->error = 0;    pwq->table = NULL;    pwq->inline_index = 0;}/* 向文件wait_queue中添加节点的方法 */static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p){      struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);    struct poll_table_entry *entry = poll_get_entry(pwq);    if (!entry) {        return;    }      get_file(filp); //put_file() in free_poll_entry()    entry->filp = filp;    entry->wait_address = wait_address;    entry->key = p->key;    /* 设置等待队列结点唤醒回调函数为pollwake */    init_waitqueue_func_entry(&entry->wait, pollwake);    entry->wait.private = pwq;    add_wait_queue(wait_address, &entry->wait);}/* 文件就绪后被调用,唤醒调用进程,其中key是文件提供的当前状态掩码 */static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key){    struct poll_table_entry *entry;    /* 取得文件对应的poll_table_entry */    entry = container_of(wait, struct poll_table_entry, wait);    /* 过滤不关注的事件 */    if (key && !((unsigned long)key & entry->key)) {        return 0;    }    /* 唤醒进程 */    return __pollwake(wait, mode, sync, key);}static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key){    struct poll_wqueues *pwq = wait->private;    DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);    smp_wmb();    pwq->triggered = 1; /* 标记为已触发 */    /* 唤醒调用进程 */    return default_wake_function(&dummy_wait, mode, sync, key);  }

select实现流程

select函数实现在fs/select.c,相较其他部分“遍历文件获取文件就绪状态”过程是整个流程的关键,主要在do_select函数中实现。

/*初始化 poll_wqueues结构 */->poll_initwait(&table); wait = &table.pt;/*估计需要等待的时间 */->select_estimate_accuracy(end_time)->死循环->遍历所有文件描述符-> __NFDBITS个进行遍历/* 取得文件结构 */->file = fget_light(i, &fput_needed)/* 设置等待事件的掩码 */->wait_key_set(wait, in, out, bit)      /* 获取当前的就绪状态, 并添加到文件的对应等待队列中 */-> mask = (*f_op->poll)(file, wait)->检查文件 i 是否已有事件就绪->信号发生,监听事件就绪或超时退出循环->poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack)->poll_freewait(&table)

上面代码展示了do_select的实现过程,首先初始化了poll_wqueues与poll_table结构,以便在文件就绪时唤醒select进程再次检查文件就绪状态。在死循环中进行文件描述符遍历,文件描述符是按位设置的,每次进行__NFDBITS(8*sizeof(unsigned long))个描述符描述符遍历,先设置poll_table中目标事件掩码,然后利用文件的poll方法获取文件就绪状态,同时将current进程添加到文件对应事件的对应等待队列中,以便文件事件就绪时唤醒进程。然后检查此文件是否已有事件就绪。当监控事件就绪或信号发生,超时时退出循环,返回select结果。否则设置超时函数进入休眠,在超时或者有文件事件就绪调用唤醒函数时,重新进入循环遍历文件状态。

总结

select实现了同时监控多个文件描述符的I/O多路转接技术。它实现基础是文件poll方法,poll方法提供了文件就绪时主动调用进程的机制。

跟踪实现过程可以发现,对于select来说,即使只有一个文件描述符就绪,重新进入循环时需要遍历所有的文件描述符,这对于仅有个别文件描述符比较活跃时,遍历开销就会显得太大。同时,select也不适用于需要多次被调用的场景,因为每次调用时都需要将全部数据从用户态复制到内核态,增加了开销。

对于select存在的问题,Linux提供了epoll机制来进行优化,有兴趣可以继续学习,其实现主要在fs/eventpoll.c。

原创粉丝点击