select 从应用层到内核实现解析
在一个应用中,如果需要读取多个设备文件,这其中有多种实现方式:
1、使用一个进程,并采用同步查询机制,不停的去轮询每一个设备描述符,当设备描述符不可用时,进程睡眠。
2:使用多个进程或者线程分别读取一个描述符,描述符不可用则进程或者线程睡眠。
3、使用select或者poll机制,这是一种多路IO复用机制。
第一种方法的缺点是,当进程在一个描述符上睡眠时,即使有其他描述符已经就绪,进程也不会醒来,这影响了程序的效率。第二种方法可以解决方法一中的问题,但是复杂性提高了,进程间切换或者同步带来复杂性的同时也会影响效率。第三种方法算是一种折中的方案,兼顾了效率和复杂性。
select函数的原型如下:
int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
参数readfds,writefds,exceptfds是设备描述符位映射数组,用来传入待监视的设备描述符,准备好的设备描述符也用这些数组传出。timeout为超时参数,如果这个参数为NULL,则select会一直阻塞,直到有设备准备就绪,如果timeout结构中的时间参数都为0,则select不会阻塞,相当于不停的查询,有准备就绪的设备就进行操作,没有的话就立刻返回,如果timeout结构内是正数,则在没有设备准备就绪的情况下,select最多会阻塞timeout时间,然后返回,如果在这期间有设备准备就绪了,即使没有到达timeout时间,也会立即返回。n为最大描述符加1,描述符fd是一个整形数,例如最大描述符fd=100,则n需要传入101。假设readfds是一个32位的映射数组,我们要监视的读设备描述符为2和5,则传入的readfds为 01001000 00000000 00000000 00000000。传入的n就为6。
select的整体调用流程如下:
select() -> core_sys_select() -> do_select() -> fop->poll()
select进入内核后,内核会使用copy_from_user将三个位映射数组拷贝到内核中,内核中有三个比较重要的结构:
struct poll_wqueues、struct poll_table_page、struct poll_table_entry、struct poll_table_struct。
每一次select进入内核,进程都会创建一个poll_wqueues结构,这个结构用来辅助完成这次select调用中待监测fd的轮询工作起着一个统领的作用,具体如下:
struct poll_wqueues {
poll_table pt;
struct poll_table_page *table;
struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
int triggered; // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
int error; // 错误码
int inline_index; // 数组inline_entries的引用下标
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
select在进入内核后会进行一系列的初始化,具体步骤如下:
步骤1:
创建poll_wqueues结构实例(在do_select中创建),对这个实例中的成员先做部分初始化,例如给poll_table成员中的qproc注册回调函数,这是通过下面的函数实现的:
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = ;
pwq->error = ;
pwq->table = NULL;
pwq->inline_index = ;
}
其中注册的回调函数为__pollwait,如下:
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);
entry->filp = filp;
entry->wait_address = wait_address;
entry->key = p->key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait); // 把p中的entry->wait加入到等待队列
}
可以看到这个函数暂时只初始化了一部分成员,struct poll_table_page *table和struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES]还没有做任何改变,下面就会看到这两个成员也会写入相应的信息。接着往下看。
步骤2:
内核根据传入的n值和各个位映射数组,使用了几个循环,大概完成以下操作,调用每一个fd所对应的驱动中的poll函数,在poll函数中最终会调用到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); //是在do_select中的poll_init_wait中为__pollwait
}
真正调用poll_wait时大概是这种形式:
poll_wait(filp, &dev->r_wait, wait);
dev->r_wait为所在驱动程序的读队列头,每个设备的驱动程序包含多个队列头,例如:读队列头、写队列头、异常队列头。
poll_wait函数最终会调用到我们在步骤1中注册的回调函数__pollwait。
这个函数有三个参数,其中filp是某个所监视的fd对应的file结构指针,dev->r_wait是这个设备描述符fd对应的驱动程序中的读等待队列,wait便是上面提到的辅助结构poll_wqueues中的poll_table成员。
再来梳理一下,在for循环中会调用驱动程序的poll函数,然后会调用到poll_wait,然后进一步调用到__pollwait,在__pollwait函数中根据传入的poll_table指针先算出poll__wqueues地址(这在linux内核中是一个很常用的技巧),得到了poll_wqueues的地址后,就可以进行具体的操作了,目的只有一个,就是为每一个fd的每一个监视事件初始化一个poll_table_entry结构,初始化的过程中会涉及到对以下两个成员的操作:
struct poll_table_page *table
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES]
具体有哪些操作呢? 首先填充inline_entries中的一项,每个fd的每个监视事件对应inline_entries中的一项,也就是struct poll_table_entry结构的一个实例,例如对于一个fd=5来说,监视它的读事件和写事件,这就对应两个struct poll_table_entry实例,也就是每调用一次poll_wait就会初始化一个struct poll_table_entry实例。
但是inline_entries数组又是有限的,所以当这个数组用完后,如果还有fd的事件需要监视,就申请一个struct poll_table_page结构挂在table指针上,这个结构具体如下:
struct poll_table_page { // 申请的物理页都会将起始地址强制转换成该结构体指针
struct poll_table_page * next; // 指向下一个申请的物理页
struct poll_table_entry * entry; // 指向entries[]中首个待分配(空的) poll_table_entry地址
struct poll_table_entry entries[]; // 该page页后面剩余的空间都是待分配的
// poll_table_entry结构体
};
申请完struct poll_table_page后,然后申请poll_table_entry结构挂在里面,poll_table_entry结构如下:
struct poll_table_entry {
struct file *filp; // 指向特定fd对应的file结构体;
unsigned long key; // 等待特定fd对应硬件设备的事件掩码,如POLLIN、
// POLLOUT、POLLERR;
wait_queue_t wait; // 代表调用select()的应用进程,等待在fd对应设备的特定事件
// (读或者写)的等待队列头上,的等待队列项;
wait_queue_head_t *wait_address; // 设备驱动程序中特定事件的等待队列头;
};
对poll_table_entry结构的初始化包括,将当前fd对应的file结构体填入filp成员,构造等待队列项并填入wait成员(这是通过init_waitqueue_func_entry(&entry->wait, pollwake)函数完成的,同时也注册了唤醒回调函数pollwake),将驱动程序的对应的等待队列头填入wait_address成员。
步骤3:
通过add_wait_queue(wait_address, &entry->wait)将上面构造的等待队列项,加入到这个fd所对应的驱动程序中的等待队列中,这个等待队列项包含当前进程的信息。
当for循环执行完,包含当前进程的等待队列元素会加入到所有待监视的fd的驱动程序的等待队列中,这样的话,不管哪一个fd准备就绪了,都会唤醒当前进程。当前进程被唤醒后,会再执行一次驱动中的poll,检查是否有其他设备准备就绪,最后根据所有就绪设备的fd设置对应的位图,并拷贝回用户空间。例如发现fd=5的读准备就绪了,就将readfds中的对应位置位,如果fd=2的写准备就绪了,就将writefds中的相应位置位。最终返回的是准备就绪的描述符的数量。
select返回之前,会将当前进程从设备队列中脱链。
放一张总体的框图:

select存在一些缺点:
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,每个fd_set是1024,则三个就是3072
3、select支持的描述符的数量比较小,默认是1024,即数据类型fd_set的大小
4、select每次返回时,readfds和writefds都将相应的准备就绪的fd对应的位置位,下次调用时,需要重新初始化这些参数。
5、select返回的只是准备就绪的描述符的数量,具体哪一个描述符准备好了还需要应用程序一个一个进行判断。
6、select每次返回后都会将poll_wqueues结构销毁,下次调用时还要重新申请,并重新初始化。
7、select只能监视读、写、异常事件,不够精细,属于粗放型的,poll系统调用可以更精细化。带外数据属于异常事件,如果是在poll中则为POLLRDBAND。
总结:
select的调用过程:陷入内核,构造辅助数据结构poll_wqueues,第一次执行poll_wait,为每一个待监视的设备描述符fd构造一个poll_table_entry加入到poll_w_queues中,并将当前进程加入到驱动程序的相应的等待队列中,同时记录下每个设备状态,循环完所有的fd之后,如果有设备就绪,则可以立刻返回,否则进入睡眠。被唤醒时,还会再循环一次所有设备的驱动中的poll函数,不管是设备准备就绪唤醒的还是超时时间到唤醒的都会执行这个操作,只是这次不会再有将进程加入队列的操作。这次同样记录下每一个设备的状态,并拷贝到用户空间中,然后将当前进程从队列中脱链,然后返回到用户空间。
select 从应用层到内核实现解析的更多相关文章
- poll 从应用层到内核实现解析
poll函数的原型如下所示: int poll(struct pollfd *fds, nfds_t nfds, int timeout); poll可以监视多个描述符的属性变化,其参数的意义如下: ...
- /proc/sys/ 下内核参数解析
http://blog.itpub.net/15480802/viewspace-753819/ http://blog.itpub.net/15480802/viewspace-753757/ ht ...
- ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57
转自: ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57 前不久ARM正式宣布推出新款ARMv8架构的Cortex-A50处理器系列 ...
- uC/OS-II内核架构解析(1)---嵌入式RTOS(转)
uC/OS-II内核架构解析(1)---嵌入式RTOS 1. 嵌入式系统基本模型 2. RTOS设计原则 采用各种算法和策略,始终保持系统行为的可预测性.即在任何情况下,在系统运行的任何时刻,OS的资 ...
- sysctl内核参数解析
sysctl内核参数解析 kernel.参数 kernel.shmall = 2097152 ## 1> 表示所有内存大小.可以分配的所有共享内存段的总和最大值.(以页为单位) ## 2& ...
- ARM内核全解析
前不久ARM正式宣布推出新款ARMv8架构的Cortex-A50处理器系列产品,以此来扩大ARM在高性能与低功耗 领域的领先地位,进一步抢占移动终端市场份额.Cortex-A50是继Cortex-A1 ...
- ksar、sar及相关内核知识点解析
关键词:sar.sadc.ksar./proc/stat./proc/cpuinfo./proc/meminfo./proc/diskstats. 在之前有简单介绍过sar/ksar,最近在使用中感觉 ...
- MySQL存储引擎之Spider内核深度解析
作者介绍 朱阅岸,中国人民大学博士,现供职于腾讯云数据库团队.研究方向主要为数据库系统理论与实现.新硬件平台下的数据库系统以及TP+AP型混合系统. Spider是为MySQL/MariaDB开发 ...
- Linux内核配置解析 - Boot options
1. 前言 本文将介绍ARM64架构下,Linux kernel和启动有关的配置项. 注1:本系列文章使用的Linux kernel版本是“X Project”所用的“Linux 4.6-rc5”,具 ...
随机推荐
- Python 以指定宽度格式化输出
当对一组数据输出的时候,我们有时需要输出以指定宽度,来使数据更清晰.这时我们可以用format来进行约束 mat = "{:20}\t{:28}\t{:32}" print(mat ...
- Navicat+Premium+12+破解补丁
链接:https://pan.baidu.com/s/1BsEWQ__X-RQPuw2ymfxhtg 提取码:j2kb
- django QueryDict对象
类的原型:class QueryDict[source] 在HttpRequest对象中,GET和POST属性都是一个django.http.QueryDict的实例.也就是说你可以按本文下面提供的方 ...
- [ios]object-c math.h里的数学计算公式介绍
参考:http://blog.csdn.net/yuhuangc/article/details/7639117 头文件:<math.h> 1. 三角函数 double sin (dou ...
- Tomcat启动之异常java.lang.IllegalStateException
严重: Exception sending context destroyed event to listener instance of class org.springframework.web. ...
- 移动端视频h5表现问题汇总
1. 同屏播放视频 <video src="" x-webkit-airplay="true" webkit-playsinline="true ...
- 构造函数用return 会出显什么情况
首先我们都知道js中构造函数一般应该是这样的 function Super (a) { this.a = a; } Super.prototype.sayHello = function() { al ...
- Struts2 简介图
Struts2官方提供的,strus2的内部工作机制图解.
- thinkphp数组处理
1.array_unique() 移除数组中的重复的值,并返回结果数组.当几个数组元素的值相等时,只保留第一个元素,其他的元素被删除,对每个值只保留第一个遇到的键名,接着忽略所有后面的键名.返回的数组 ...
- Leetcode 115
Ø r a b b b i t Ø r a b b i t class Solution { public: int numDistinct(string s, string t) { ; ; int ...