Select函数实现原理分析

本文详细解析了select机制在Linux系统中的工作原理,包括其如何利用设备驱动程序的poll函数监测文件描述符的状态,并通过等待队列机制实现进程的阻塞与唤醒。

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

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

1. select 的睡眠过程

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

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

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

下面分析一下代码是如何实现的。

select 的调用 path 如下: sys_select -> core_sys_select -> do_select

其中最重要的函数是 do_select, 最主要的工作是在这里 , 前面两个函数主要做一些准备工作。 do_select 定义如下:

  1. int do_select( int n,fd_set_bits*fds,s64*timeout)

  2. {

  3. struct poll_wqueuestable;

  4. poll_table*wait;

  5. int retval,i;


  6. rcu_read_lock();

  7. retval=max_select_fd(n,fds);

  8. rcu_read_unlock();


  9. if (retval<0)

  10. return retval;

  11. n=retval;


  12. poll_initwait(&table);

  13. wait=&table.pt;

  14. if (!*timeout)

  15. wait=NULL;

  16. retval=0; //retval用于保存已经准备好的描述符数,初始为0

  17. for (;;){

  18. unsigned long *rinp,*routp,*rexp,*inp,*outp,*exp;

  19. long __timeout;


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


  21. inp=fds->in;outp=fds->out;exp=fds->ex;

  22. rinp=fds->res_in;routp=fds->res_out;rexp=fds->res_ex;


  23. for (i=0;i<n;++rinp,++routp,++rexp){ //遍历每个描述符

  24. unsigned long in,out,ex,all_bits,bit=1,mask,j;

  25. unsigned long res_in=0,res_out=0,res_ex=0;

  26. const struct file_operations*f_op=NULL;

  27. struct file*file=NULL;


  28. in=*inp++;out=*outp++;ex=*exp++;

  29. all_bits=in|out|ex;

  30. if (all_bits==0){

  31. i+=__NFDBITS; ////如果这个字没有待查找的描述符,跳过这个长字(32位)

  32. continue ;

  33. }


  34. for (j=0;j<__NFDBITS;++j,++i,bit<<=1){ //遍历每个长字里的每个位

  35. int fput_needed;

  36. if (i>=n)

  37. break ;

  38. if (!(bit&all_bits))

  39. continue ;

  40. file=fget_light(i,&fput_needed);

  41. if (file){

  42. f_op=file->f_op;

  43. MARK(fs_select, "%d%lld" ,

  44. i,( long long )*timeout);

  45. mask=DEFAULT_POLLMASK;

  46. if (f_op&&f_op->poll)

  47. /*在这里循环调用所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数*/

  48. mask=(*f_op->poll)(file,retval?NULL:wait);

  49. fput_light(file,fput_needed);

  50. if ((mask&POLLIN_SET)&&(in&bit)){

  51. res_in|=bit; //如果是这个描述符可读,将这个位置位

  52. retval++; //返回描述符个数加1

  53. }

  54. if ((mask&POLLOUT_SET)&&(out&bit)){

  55. res_out|=bit;

  56. retval++;

  57. }

  58. if ((mask&POLLEX_SET)&&(ex&bit)){

  59. res_ex|=bit;

  60. retval++;

  61. }

  62. }

  63. cond_resched();

  64. }

  65. //返回结果

  66. if (res_in)

  67. *rinp=res_in;

  68. if (res_out)

  69. *routp=res_out;

  70. if (res_ex)

  71. *rexp=res_ex;

  72. }

  73. wait=NULL;

  74. /*到 这里遍历结束。retval保存了检测到的可操作的文件描述符的个数。如果有文件可操作,则跳出for(;;)循环,直接返回。若没有文件可操作且 timeout时间未到同时没有收到signal,则执行schedule_timeout睡眠。睡眠时间长短由__timeout决定,一直等到该进程 被唤醒。

  75. 那该进程是如何被唤醒的?被谁唤醒的呢?

  76. 我们看下面的select唤醒过程*/

  77. if (retval||!*timeout||signal_pending(current))

  78. break ;

  79. if (table.error){

  80. retval=table.error;

  81. break ;

  82. }


  83. if (*timeout<0){

  84. /*Waitindefinitely*/

  85. __timeout=MAX_SCHEDULE_TIMEOUT;

  86. } else if (unlikely(*timeout>=(s64)MAX_SCHEDULE_TIMEOUT-1)){

  87. /*WaitforlongerthanMAX_SCHEDULE_TIMEOUT.Doitinaloop*/

  88. __timeout=MAX_SCHEDULE_TIMEOUT-1;

  89. *timeout-=__timeout;

  90. } else {

  91. __timeout=*timeout;

  92. *timeout=0;

  93. }

  94. __timeout=schedule_timeout(__timeout);

  95. if (*timeout>=0)

  96. *timeout+=__timeout;

  97. }

  98. __set_current_state(TASK_RUNNING);


  99. poll_freewait(&table);


  100. return retval;

  101. }


2. select 的唤醒过程

前面介绍了 select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列 ( 如读 / 写等待队列 ) ,然后返回一个 bitmask 告诉 select 当前资源哪些可用。

一个典型的驱动程序 poll 函数实现如下:

( 摘自《 Linux Device Drivers – ThirdEditionPage 165)

  1. static unsigned int scull_p_poll( struct file*filp,poll_table*wait)

  2. {

  3. struct scull_pipe*dev=filp->private_data;

  4. unsigned int mask=0;

  5. /*

  6. *Thebufferiscircular;itisconsideredfull

  7. *if"wp"isrightbehind"rp"andemptyifthe

  8. *twoareequal.

  9. */

  10. down(&dev->sem);

  11. poll_wait(filp,&dev->inq,wait);

  12. poll_wait(filp,&dev->outq,wait);

  13. if (dev->rp!=dev->wp)

  14. mask|=POLLIN|POLLRDNORM; /*readable*/

  15. if (spacefree(dev))

  16. mask|=POLLOUT|POLLWRNORM; /*writable*/

  17. up(&dev->sem);

  18. return mask;

  19. }


将用户进程插入驱动的等待队列是通过 poll_wait 做的。

Poll_wait 定义如下:

  1. static inline void poll_wait( struct file*filp,wait_queue_head_t*wait_address,poll_table*p)

  2. {

  3. if (p&&wait_address)

  4. p->qproc(filp,wait_address,p);

  5. }


这里的 p->qprocdo_selectpoll_initwait(&table) 被初始化为 __pollwait ,如下:

  1. void poll_initwait( struct poll_wqueues*pwq)

  2. {

  3. init_poll_funcptr(&pwq->pt,__pollwait);

  4. pwq->error=0;

  5. pwq->table=NULL;

  6. pwq->inline_index=0;

  7. }


__pollwait 定义如下:

  1. /*Addanewentry*/

  2. static void __pollwait( struct file*filp,wait_queue_head_t*wait_address,

  3. poll_table*p)

  4. {

  5. struct poll_table_entry*entry=poll_get_entry(p);

  6. if (!entry)

  7. return ;

  8. get_file(filp);

  9. entry->filp=filp;

  10. entry->wait_address=wait_address;

  11. init_waitqueue_entry(&entry->wait,current);

  12. add_wait_queue(wait_address,&entry->wait);

  13. }


通过 init_waitqueue_entry 初始化一个等待队列项,这个等待队列项关联的进程即当前调用 select 的进程。然后将这个等待队列项插入等待队列 wait_addressWait_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 层维护的两个等待队列 , 分别对应于读和写 : (uarttty 设备的一种 )

struct tty_struct {

……

wait_queue_head_t write_wait;

wait_queue_head_t read_wait;

……

}

uart 设备接收到数据,会调用 tty_flip_buffer_push(tty); 将收到的数据 pushtty 层的 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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值