百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

select每个程序员都应该懂得技术 select语句用来干什么

liebian365 2024-10-30 04:47 27 浏览 0 评论

来源本人公众号【技术原理君】作者:源理君

熟悉Linux环境编程的程序员们应该对select系统调用了如指掌吧,因为它的功能比较强大,可以用来定时,监听等等。可你们是否想过它的实现原理?这篇文章就来聊聊它的实现原理。

首先按照惯例,简单的讲述下select使用方法。

select是用来监听多个文件句柄的状态变化。程序会阻塞在select等待,直到监听的句柄有一个或者多个状态发生变化。函数的原型如下:


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

函数声明中的参数就不一一解释了,默认你们都是烂熟于心了。典型的用法如下(网络接收数据,并写入文件):


main()
{
 int sock; int fd;
 fd_set fds;
 struct timeval timeout={0,3}; //select等待3微秒,3微秒轮询,要非阻塞就置0
 char buffer[256]={0}; //256字节的接收缓冲区
 /* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
 sock=socket(...);
 bind(...);
 fd=open(...); */
 while⑴
 {
 FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
 FD_SET(sock,&fds); //添加描述符
 FD_SET(fd,&fds); //同上
 timeout.tv_sec=3;
 timeout.tv_usec=0;//select函数会不断修改timeout的值,所以每次循环都应该重新赋值[windows不受此影响]
 maxfdp=sock>fd?sock+1:fd+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(fd,&fds)) //测试文件是否可写
 write(fd,buffer...);//写入文件
 buffer清空;
 }// end if break;
 }// end switch
 }//end while
}//end main

由于文章重点是在于原理,所以具体解释参考代码注释。下面进入原理正题。

原理

select在设备中需要驱动程序的支持,也就是说需要实现fops中的select()接口。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内部实现顺序:sys_select -> core_sys_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等待队列。

到这里我们明白了select如何将当前进程插入所有所监测的fd_set关联的驱动内的等待队列,那进程究竟是何时让出CPU进入睡眠状态的呢?

进入睡眠状态是在do_select内调用schedule_timeout(__timeout)实现的。当select遍历完fd_set内的所有设备文件,发现没有文件可操作时(即retval=0),则调用schedule_timeout(__timeout)进入睡眠状态。

唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。

觉得不错,记得转发点赞“在看”!

相关推荐

精品博文嵌入式6410中蓝牙的使用

BluetoothUSB适配器拥有一个BluetoothCSR芯片组,并使用USB传输器来传输HCI数据分组。因此,LinuxUSB层、BlueZUSB传输器驱动程序以及B...

win10跟这台计算机连接的前一个usb设备工作不正常怎么办?

前几天小编闲来无事就跑到网站底下查看粉丝朋友给小编我留言询问的问题,还真的就给小编看到一个问题,那就是win10跟这台计算机连接的一个usb设备运行不正常怎么办,其实这个问题的解决方法时十分简单的,接...

制作成本上千元的键盘,厉害在哪?

这是稚晖君亲自写的开源资料!下方超长超详细教程预警!!全文导航:项目简介、项目原理说明、硬件说明、软件说明项目简介瀚文智能键盘是一把我为自己设计的——多功能、模块化机械键盘。键盘使用模块化设计。左侧的...

E-Marker芯片,USB数据线的“性能中枢”?

根据线缆行业的研究数据,在2019年搭载Type-C接口的设备出货量已达到20亿台,其中80%的笔记本电脑和台式电脑采用Type-C接口,50%的智能手机和平板电脑也使用Type-C接口。我们都知道,...

ZQWL-USBCANFD二次开发通讯协议V1.04

修订历史:1.功能介绍1.1型号说明本文档适用以下型号:  ZQWL-CAN(FD)系列产品,USB通讯采用CDC类实现,可以在PC机上虚拟出一个串口,串口参数N,8,1格式,波特率可以根据需要设置(...

win10系统无法识别usb设备怎么办(win10不能识别usb)

从驱动入手,那么win10系统无法识别usb设备怎么办呢?今天就为大家分享win10系统无法识别usb设备的解决方法。1、右键选择设备管理器,如图:  2、点击更新驱动程序,如图:  3、选择浏览...

微软七月Win8.1可选补丁有内涵,含大量修复

IT之家(www.ithome.com):微软七月Win8.1可选补丁有内涵,含大量修复昨日,微软如期为Win7、Win8.1发布7月份安全更新,累计为6枚安全补丁,分别修复总计29枚安全漏洞,其中2...

如何从零开始做一个 USB 键盘?(怎么制作usb)

分两种情况:1、做一个真正的USB键盘,这种设计基本上不涉及大量的软件编码。2、做一个模拟的USB键盘,实际上可以没有按键功能,这种的需要考虑大量的软件编码,实际上是一个单片机。第一种设计:买现成的U...

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题注意:有些方法会清除USB设备里的数据,请谨慎操作,如果不想丢失数据,可以先连接到其他电脑,看能否将数据复制出来,或者用一些数据恢复软件去扫...

未知usb设备设备描述符请求失败怎么解决

出现未知daousb设备设备描述符请求失du败解决办zhi法如下:1、按下Windows+R打开【运行】;2、在版本运行的权限输入框中输入:services.msc按下回车键打开【服务】;2、在服务...

读《飘》47章20(飘每章概括)

AndAhwouldn'tleaveMissEllen'sgrandchildrenfornotrashystep-patobringup,never.Here,Ah...

英翻中 消失的过去 37(消失的英文怎么说?)

翻译(三十七):消失的过去/茱迪o皮考特VanishingActs/JodiPicoult”我能做什么?“直到听到了狄利亚轻柔的声音,我才意识到她已经在厨房里站了好一会儿了。当她说话的时候,...

RabbitMQ 延迟消息实战(rabbitmq如何保证消息不被重复消费)

现实生活中有一些场景需要延迟或在特定时间发送消息,例如智能热水器需要30分钟后打开,未支付的订单或发送短信、电子邮件和推送通知下午2:00开始的促销活动。RabbitMQ本身没有直接支持延迟...

Java对象拷贝原理剖析及最佳实践(java对象拷贝方法)

作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的...

如何将 Qt 3D 渲染与 Qt Quick 2D 元素结合创建太阳系行星元素?

Qt组件推荐:QtitanRibbon:遵循MicrosoftRibbonUIParadigmforQt技术的RibbonUI组件,致力于为Windows、Linux和MacOSX提...

取消回复欢迎 发表评论: