Linux : select()详解 和 实现原理【转】

转自:http://blog.csdn.net/huntinux/article/details/39289317

原文:http://blog.csdn.net/boboiask/article/details/4055655

Linux—select详解

 

select系统调用时用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。

关于文件句柄,其实就是一个整数,通过socket函数的声明就明白了:

int socket(int domain, int type, int protocol);

我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。

继续上面的select,就是用来监视某个或某些句柄的状态变化的。select函数原型如下:

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

函数的最后一个参数timeout是一个超时时间值。其类型是struct timeval *,即一个struct
timeval结构的变量的指针,所以我们在程序里要声明一个struct timeval
tv;然后把变量tv的地址&tv传递给select函数。struct timeval结构如下:

struct timeval

{

       long tv_sec;  //seconds

       long tv_usec; //microseconds

};

第2、3、4三个参数是一样的类型;fd_set
*,即我们在程序里要申请几个fd_set类型的变量,比如rdfds,wtfds,exfds,然后把这个变量的地址&rdfds,&wtfds,&exfds传递给select函数。这三个参数都是一个句柄的集合,第一个rdfds是用来保存这样的句柄的:当句柄的状态变成可读时系统就告诉select函数返回,同理第二个函数是指向有句柄状态变成可写时系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即句柄上有特殊情况发生时系统会告诉select函数返回。特殊情况比如对方通过一个socket句柄发来了紧急数据。如果我们程序里只想检测某个socket是否有数据可读,我们可以这样:

fd_set  rdfds;

struct timeval tv;

int ret;

FD_ZERO(&rdfds);

FD_SET(socket, &rdfds);

tv.tv_sec = 1;

tv.tv_uses = 500;

ret = select (socket + 1, %rdfds, NULL, NULL, &tv);

if(ret < 0) perror (“select”);

else if (ret = = 0) printf(“time out”);

else {

       printf(“ret = %d/n”,ret);

       if(FD_ISSET(socket, &rdfds)){

    /* 读取socket句柄里的数据 */

recv( );

}

}

注意select函数的第一个参数,是所有加入集合的句柄值的最大那个那个值还要加1.比如我们创建了3个句柄;

int sa, sb, sc;

sa = socket(……);

connect (sa,….);

 

sb = socket(….);

connect (sb,…);

 

sc = socket(….);

connect(sc,…);

 

FD_SET(sa, &rdfds);

FD_SET(sb, &rdfds);

FD_SET(sc, &rdfds);

 

在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下:

int maxfd = 0;

if(sa > maxfd) maxfd = sa;

if(sb > maxfd) maxfd = sb;

if(sc > maxfd) maxfd = sc;

 

然后调用select函数:

ret = select (maxfd+1, &rdfds, NULL, NULL,&tv);

 

同样的道理,如果我们是检测用户是否按了键盘进行输入,我们就应该把标准输入0这个句柄放到select里来检测,如下:

FD_ZERO(&rdfds);

FD_SET(0, &rdfds);

tv.tv_sec = 1;

tv.tv_usec = 0;

ret = select (1, &rdfds,NULL,NULL,&tv);

if(ret < 0) perror(“select”);

else if (ret = = 0) printf (“time out/n”);

else{

       scanf(“%s”,buf);

}


===================select 实现原理=======================

参考:http://linux.chinaunix.NET/techdoc/net/2009/05/03/1109887.shtml

Select函数实现原理分析(转载)

select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。

下面我们分两个过程来分析select:

1. select的睡眠过程

支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。

select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。

select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。

下面分析一下代码是如何实现的。
select的调用path如下:sys_select -> core_sys_select -> do_select
其中最重要的函数是do_select, 最主要的工作是在这里, 前面两个函数主要做一些准备工作。do_select定义如下:
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
       struct poll_wqueues table;
       poll_table *wait;
       int retval, i;

       rcu_read_lock();
       retval = max_select_fd(n, fds);
       rcu_read_unlock();

       if (retval
              return retval;
       n = retval;

       poll_initwait(&table);
       wait = &table.pt;
       if (!*timeout)
              wait = NULL;
       retval = 0;      //retval用于保存已经准备好的描述符数,初始为0
       for (;;) {
              unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
              long __timeout;

              set_current_state(TASK_INTERRUPTIBLE);      //将当前进程状态改为TASK_INTERRUPTIBLE

              inp = fds->in; outp = fds->out; exp = fds->ex;
              rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

              for (i = 0; i 遍历每个描述符
                     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;

                     in = *inp++; out = *outp++; ex = *exp++;
                     all_bits = in | out | ex;
                     if (all_bits == 0) {
                            i += __NFDBITS;  // //如果这个字没有待查找的描述符, 跳过这个长字(32位)
                            continue;
                     }

                     for (j = 0; j 遍历每个长字里的每个位
                            int fput_needed;
                            if (i >= n)
                                   break;
                            if (!(bit & all_bits))
                                   continue;
                            file = fget_light(i, &fput_needed);
                            if (file) {
                                   f_op = file->f_op;
                                   MARK(fs_select, "%d %lld",
                                                 i, (long long)*timeout);
                                   mask = DEFAULT_POLLMASK;
                                   if (f_op && f_op->poll)
/* 在这里循环调用所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数 */
                                          mask = (*f_op->poll)(file, retval ? NULL : wait);
                                   fput_light(file, fput_needed);
                                   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();
                     }
//返回结果
                     if (res_in)
                            *rinp = res_in;
                     if (res_out)
                            *routp = res_out;
                     if (res_ex)
                            *rexp = res_ex;
              }
              wait = NULL;
/*
到这里遍历结束。retval保存了检测到的可操作的文件描述符的个数。如果有文件可操作,则跳出for(;;)循环,直接返回。若没有文件可操作且timeout时间未到同时没有收到signal,则执行schedule_timeout睡眠。睡眠时间长短由__timeout决定,一直等到该进程被唤醒。

那该进程是如何被唤醒的?被谁唤醒的呢?
我们看下面的select唤醒过程*/
              if (retval || !*timeout || signal_pending(current))
                     break;
              if(table.error) {
                     retval = table.error;
                     break;
              }

              if (*timeout
                     /* Wait indefinitely */
                     __timeout = MAX_SCHEDULE_TIMEOUT;
              } else if (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;
              }
              __timeout = schedule_timeout(__timeout);
              if (*timeout >= 0)
                     *timeout += __timeout;
       }
       __set_current_state(TASK_RUNNING);

       poll_freewait(&table);

       return retval;
}

2.  select的唤醒过程
前面介绍了select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。
一个典型的驱动程序poll函数实现如下:
(摘自《Linux Device Drivers – ThirdEdition》Page 165)
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    struct scull_pipe *dev = filp->private_data;
    unsigned int mask = 0;
    /*
     * The buffer is circular; it is considered full
     * if "wp" is right behind "rp" and empty if the
     * two are equal.
     */
    down(&dev->sem);
    poll_wait(filp, &dev->inq,  wait);
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;    /* readable */
    if (spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;   /* writable */
    up(&dev->sem);
    return mask;
}
将用户进程插入驱动的等待队列是通过poll_wait做的。
Poll_wait定义如下:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
       if (p && wait_address)
              p->qproc(filp, wait_address, p);
}
这里的p->qproc在do_select内poll_initwait(&table)被初始化为__pollwait,如下:
void poll_initwait(struct poll_wqueues *pwq)
{
       init_poll_funcptr(&pwq->pt, __pollwait);
       pwq->error = 0;
       pwq->table = NULL;
       pwq->inline_index = 0;
}
__pollwait定义如下:
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                            poll_table *p)
{
       struct poll_table_entry *entry = poll_get_entry(p);
       if (!entry)
              return;
       get_file(filp);
       entry->filp = filp;
       entry->wait_address = wait_address;
       init_waitqueue_entry(&entry->wait, current);
       add_wait_queue(wait_address,&entry->wait);
}
通过init_waitqueue_entry初始化一个等待队列项,这个等待队列项关联的进程即当前调用select的进程。然后将这个等待队列项插入等待队列wait_address。Wait_address即在驱动poll函数内调用poll_wait(filp,

&dev->inq,  wait);时传入的该驱动的&dev->inq或者&dev->outq等待队列。

注: 关于等待队列的工作原理可以参考下面这篇文档:
http://blog.chinaunix.net/u2/60011/showart_1334657.html

到这里我们明白了select如何将当前进程插入所有所监测的fd_set关联的驱动内的等待队列,那进程究竟是何时让出CPU进入睡眠状态的呢?
进入睡眠状态是在do_select内调用schedule_timeout(__timeout)实现的。当select遍历完fd_set内的所有设备文件,发现没有文件可操作时(即retval=0),则调用schedule_timeout(__timeout)进入睡眠状态。

唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
举个例子,比如内核的8250 uart driver:
Uart是使用的Tty层维护的两个等待队列, 分别对应于读和写: (uart是tty设备的一种)
struct tty_struct {
       ……
       wait_queue_head_t write_wait;
       wait_queue_head_t read_wait;
       ……
}
当uart设备接收到数据,会调用tty_flip_buffer_push(tty);将收到的数据push到tty层的buffer。
然后查看是否有进程睡眠的读等待队列上,如果有则唤醒该等待会列。
过程如下:
serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push ->
flush_to_ldisc -> disc->receive_buf
在disc->receive_buf函数内:
if (waitqueue_active(&tty->read_wait))       //若有进程阻塞在read_wait上则唤醒
wake_up_interruptible(&tty->read_wait);

到这里明白了select进程被唤醒的过程。由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。

Referece:
1.       Linux Device Drivers – ThirdEdition
2.       内核等待队列机制原理分析
http://blog.chinaunix.Net/u2/60011/showart_1334657.html
3.       Kernel code : Linux 2.6.18_pro500 - Montavista

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u3/94284/showart_1917293.html


==========另外参考LDD chp6 poll和select==============

用户空间程序在驱动程序关联的文件描述符上执行poll、select、epoll系统调用时,驱动程序中 fops里面的 poll方法被调用。

时间: 2024-11-13 08:51:19

Linux : select()详解 和 实现原理【转】的相关文章

《嵌入式Linux软硬件开发详解——基于S5PV210处理器》——导读

前言 嵌入式Linux软硬件开发详解--基于S5PV210处理器 近年来,嵌入式技术和嵌入式产品发展势头迅猛,其应用领域涉及通信产品.消费电子.汽车工业.工业控制.信息家电.国防工业等各个方面.嵌入式产品在IT产业以及电子工业的经济总额中所占的比重越来越大,对国民经济增长的贡献日益显著.随着智能手机.媒体播放器.数码相机和机顶盒等嵌入式产品的普及,嵌入式系统的知识在广大民众中的传播也越来越广泛.出于对嵌入式高科技知识的追求,广大在校学生纷纷选修嵌入式系统课程,以获得嵌入式系统的理论知识和开发技能

javaScript中的this示例学习详解及工作原理

 这篇文章主要介绍了javaScript中的this示例学习详解及工作原理,大家参考使用吧 this的工作原理   如果一个函数被作为一个对象的方法调用,那么this将被指派为这个对象.   代码如下: var parent = {     method: function () {         console.log(this);     } };   parent.method(); // <- parent       注意这种行为非常"脆弱",如果你获取一个方法的引用

Linux init详解(转)

Linux init详解 一.什么是INIT: init是Linux系统操作中不可缺少的程序之一. 所谓的init进程,它是一个由内核启动的用户级进程. 内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序init的方式,完成引导进程.所以,init始终是第一个进程(其进程编号始终为1). 内核会在过去曾使用过init的几个地方查找它,它的正确位置(对Linux系统来说)是/sbin/init.如果内核找不到init,它就会试着运行/b

Linux 中断详解 【转】

转自:http://blog.csdn.net/tiangwan2011/article/details/7891818 原文地址 http://www.yesky.com/20010813/192117.shtml 方法之三:以数据结构为基点,触类旁通 结构化程序设计思想认为:程序 =数据结构 +算法.数据结构体现了整个系统的构架,所以数据结构通常都是代码分析的很好的着手点,对Linux内核分析尤其如此.比如,把进程控制块结构分析清楚 了,就对进程有了基本的把握:再比如,把页目录结构和页表结构

Linux基本配置和管理 4 Linux系统启动详解

1 系统启动的流程 BIOS -> MBR+boot code -> 执行引导程序: GRUB -> 加载内核 -> 执行init -> 运行runlevel 2 启动详解 1 BIOS 1 BIOS 指的是基本的输入输出系统(basic input output system),一般保存在主板上的BIOS芯片中 2 计算机启动的第一步一般都是先启动BIOS,然后BIOS去检查硬件以及查找可启动的设备 3 可启动的设备可以在BIOS中进行设置,包括USB , CDROM ,

详解堆排序算法原理及Java版的代码实现_java

概述堆排序是一种树形选择排序,是对直接选择排序的有效改进. 堆的定义如下:具有n个元素的序列(k1,k2,...,kn), 当且仅当满足: 时称之为堆.由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)或最大项(大顶堆). 若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点(有子女的结点)的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的. (a)大顶堆序列:(96, 83, 27, 38, 11, 09) (b)小顶堆序列:(12, 36,

Linux系统调用详解(实现机制分析)--linux内核剖析(六)

系统调用概述 计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制.也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call).在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口. 一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程.在Unix世界,最流行的API

最强大脑“人机大战”第二轮战平,吴恩达详解背后技术原理

如果说在上一轮人机大战的人脸识别对决中,由于小度对阵了并不擅长人脸识别的王峰,令比赛意义打了折扣,那么昨天进行的第二轮声音识别的人机大战,虽然最终只是战平,但对于AI 界的意义却似乎更大. 原因有两点:一.公认实力顶尖的对手:二.业界公认困难的比赛内容. 在对手上,此次迎战百度小度的是名人堂公认最擅长声音辨别的选手孙亦廷,他辨别声音细节的能力在名人堂无出其右,能通过水球从0~70米高空坠地破碎的声音,来准确辨别水球下落时的高度.这相当于在一根70米长的琴弦上拉奏任意位置,他都通过音高准确辨别拉弦

linux系统服务详解

1.NetworaManager:在无线和有线网络之间快速切换.一般用于移动终端. 2.NetworkManagerDispatcher:在多种网络环境之间来回切换,与NetworkManager相同. 3.acpid:(没有预设端口)电源管理配置服务. 4.anacron:(没有预设端口)一个自动化运行任务.Red Hat Linux 随带四个自动化任务的工具:cron.anacron.at.和 batc.当你的Linux主机并不是全天候开机,这个anacron就可以帮你执行在"chontab