poll&&epoll之poll实现

0.等待队列
在linux内核中等待队列有很多用途,可用于中断处理、进程同步及定时。我们在这里只说,进程经常必须等待某些事件的发生。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制全。因此,等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒它们。
等待队列由循环链表实现,由等待队列头(wait_queue_head_t)和等待队列项(wait_queue)组成,其元素(等待队列项)包含指向进程描述符的指针。每个等待队列都有一个等待队列头(waitqueuehead),等待队列头是一个类型为wait_queue_head_t的数据结构
定义等待队列头(相关内容可以在linux/include/wait.h中找到)
等待队列头结构体的定义:
structwait_queue_head{
spinlock_tlock;//自旋锁变量,用于在对等待队列头
structlist_headtask_list;//指向等待队列的list_head
};
typedefstruct__wait_queue_headwait_queue_head_t;
使用等待队列时首先需要定义一个wait_queue_head,这可以通过declare_wait_queue_head宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。
linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。
(2)等待队列中存放的是在执行设备操作时不能获得资源而挂起的进程
定义等待对列:
structwait_queue{
unsignedintflags;//prepare_to_wait()里有对flags的操作,查看以得出其含义
#definewq_flag_exclusive0x01//一个常数,在prepare_to_wait()用于修改flags的值
void*private//通常指向当前任务控制块
wait_queue_func_tfunc;//唤醒阻塞任务的函数,决定了唤醒的方式
structlist_headtask_list;//阻塞任务链表
};
typedefstruct__wait_queuewait_queue_t;
poll实现分析
1.select/poll缺点
select/poll的缺点在于:
1.每次调用时要重复地从用户态读入参数。
2.每次调用时要重复地扫描文件描述符。
3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。
2.内核实现
2.1主要数据结构:
(1)structpoll_table_entry{
structfilefilp;
wait_queue_twait;//内部有一个指针指向一个进程
wait_queue_head_twait_address;//等待队列头部(等待队列有多个wait_queue_t组成,通过双链表连接)
};
(2)structpoll_table_page{
structpoll_table_pagenext;
structpoll_table_entryentry;
structpoll_table_entryentries[0];
};
(3)structpoll_wqueues{
poll_tablept;//一个函数指针,通常指向__pollwait或null
structpoll_table_page*table;
interror;
};
(4)structpoll_list{
structpoll_list*next;//按内存页连接,因为kmalloc有申请数据限制
intlen;//用户空间传入fd的数量
structpollfdentries[0];//存放用户空间存入的数据
};
typedefvoid(*poll_queue_proc)(structfile*,wait_queue_head_t*,structpoll_table_struct*);
typedefstructpoll_tablestruct{
poll_queue_procqproc;
}poll_table;
2.2poll系统调用函数关系总图
intpoll(structpollfd*fds,nfds_tnfds,inttimeout);
3.内核2.6.9poll实现代码分析
[fs/select.c-->sys_poll]
asmlinkagelongsys_poll(structpollfd__user*ufds,unsignedintnfds,longtimeout)
{
structpoll_wqueuestable;
structpoll_list*head;
structpoll_list*walk;
……
poll_initwait(&table);
……
while(i!=0){
structpoll_list*pp;
pp=kmalloc(sizeof(structpoll_list)+sizeof(structpollfd)
*(i>pollfd_per_page?pollfd_per_page:i),gfp_kernel));
if(head==null)
head=pp;
else
walk->next=pp;
walk=pp;
if(copy_from_user(pp->entries,ufds+nfds-i,
sizeof(structpollfd)*pp->len)){
err=-efault;
gotoout_fds;
}
i-=pp->len;
}
/*这一大堆代码就是建立一个链表,每个链表的节点是一个page大小(通常是4k),这链表节点由一个指向structpoll_list的指针掌控每个poll_list的entrys成员指向一个structpollfd。上面的循环就是把用户态的structpollfd拷进这些entries里。通常用户程序的poll调用就监控几个fd,所以上面这个链表通常也就只需要一个节点,即操作系统的一页。但是,当用户传入的fd很多时,由于poll系统调用每次都要把所有structpollfd拷进内核,所以参数传递和页分配此时就成了poll系统调用的性能瓶颈。*/
fdcount=do_poll(nfds,head,&table,timeout);
}
其中poll_initwait较为关键,从字面上看,应该是初始化变量table,注意此处table在整个执行poll的过程中是很关键的变量。而structpoll_table其实就只包含了一个函数指针。
现在我们来看看poll_initwait到底在做些什么
void__pollwait(structfile*filp,wait_queue_head_t*wait_address,poll_table*p);
voidpoll_initwait(structpoll_wqueues*pwq)
{
&(pwq->pt)->qproc=__pollwait;/*设置回调函数*/
……
}
很明显,poll_initwait的主要动作就是把table变量的成员poll_table对应的回调函数置为__pollwait。这个__pollwait不仅是poll系统调用需要,select系统调用也一样是用这个__pollwait,说白了,这是个操作系统的异步操作的“御用”回调函数。当然了,epoll没有用这个,它另外新增了一个回调函数,以达到其高效运转的目的,这是后话,暂且不表。
最后一句do_poll,我们跟进去:
staticintdo_poll(unsignedintnfds,structpoll_list*list,structpoll_wqueues*wait,
longtimeout)
{
intcount=0;
poll_table*pt=&wait->pt;
for(;;){
structpoll_list*walk;
set_current_state(task_interruptible);
walk=list;
while(walk!=null){
do_pollfd(walk->len,walk->entries,&pt,&count);
walk=walk->next;
}
pt=null;
if(count||!timeout||signal_pending(current))
break;
count=wait->error;
if(count)
break;
timeout=schedule_timeout(timeout);/*让current挂起,别的进程跑,timeout到了
以后再回来运行current*/
}
__set_current_state(task_running);
returncount;
}
注意set_current_state和signal_pending,它们两句保障了当用户程序在调用poll后挂起时,发信号可以让程序迅速推出poll调用,而通常的系统调用是不会被信号打断的。纵览do_poll函数,主要是在循环内等待,直到count大于0才跳出循环,而count主要是靠do_pollfd函数处理。注意标红的while循环,当用户传入的fd很多时(比如1000个),对do_pollfd就会调用很多次,poll效率瓶颈的另一原因就在这里。
do_pollfd就是针对每个传进来的fd,调用它们各自对应的poll函数,简化一下调用过程,如下:
[fs/select.c-->sys_poll()-->do_poll()]
staticvoiddo_pollfd(unsignedintnum,structpollfd*fdpage,poll_table**pwait,int*count)
{
……
structfile*file=fget(fd);
file->f_op->poll(file,&(table->pt));
……
}
如果fd对应的是某个socket,do_pollfd调用的就是网络设备驱动实现的poll;如果fd对应的是某个ext3文件系统上的一个打开文件,那do_pollfd调用的就是ext3文件系统驱动实现的poll。一句话,这个file->f_op->poll是设备驱动程序实现的,那设备驱动程序的poll实现通常又是什么样子呢?其实,设备驱动程序的标准实现是:调用poll_wait,即以设备自己的等待队列为参数(通常设备都有自己的等待队列,不然一个不支持异步操作的设备会让人很郁闷)调用structpoll_table的回调函数。
作为驱动程序的代表,我们看看socket在使用tcp时的代码:
[net/ipv4/tcp.c-->tcp_poll]
unsignedinttcp_poll(structfile*file,structsocket*sock,poll_table*wait)
{
……
poll_wait(file,sk->sk_sleep,wait);
tcp_poll的核心实现就是poll_wait,而poll_wait就是调用structpoll_table对应的回调函数,那poll系统调用对应的回调函数就是__poll_wait,所以这里几乎就可以把tcp_poll理解为一个语句:
__poll_wait(file,sk->sk_sleep,wait);
由此也可以看出,每个socket自己都带有一个等待队列sk_sleep,所以上面我们所说的“设备的等待队列”,其实不止一个。
这时候我们再看看__poll_wait的实现:
[fs/select.c-->__poll_wait()]
void__pollwait(structfile*filp,wait_queue_head_t*wait_address,poll_table*_p)
{
……
}
__poll_wait的作用就是创建了上图所示的数据结构(一次__poll_wait即一次设备poll调用只创建一个poll_table_entry),并通过structpoll_table_entry的wait成员,把current挂在了设备的等待队列上,此处的等待队列是wait_address,对应tcp_poll里的sk->sk_sleep。
现在我们可以回顾一下poll系统调用的原理了:先注册回调函数__poll_wait,再初始化table变量(类型为structpoll_wqueues),接着拷贝用户传入的structpollfd(其实主要是fd)(瓶颈1),然后轮流调用所有fd对应的poll(把current挂到各个fd对应的设备等待队列上)(瓶颈2)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上的进程,这时current便被唤醒了。current醒来后离开sys_poll的操作相对简单,这里就不逐行分析了。

西蒙电气获22年人力资源管理杰出奖 Nexperia推低电流稳压二极管
都是旗舰新机,小米6和小米mix2你选谁?区别对比
具有花状纳米结构的硬碳载体助力快充锂电池
模拟芯片能够模仿人脑神经元和突触的活动
一文读懂半导体投资市场
poll&&epoll之poll实现
条码技术的优点
中国5nm芯片最新消息
如何安全拆解CRT电视
霍尔芯片在智能围棋 电子棋盘中的应用及原理
MOS管在AGV无接触供电系统中的应用
机器视觉光源介绍及选型参考
电动汽车电池充电的主要解决方案
电子镇流器热保护器:优势和适用性分析
零欧电阻的八大妙用
金士顿Canvas Go! Plus 系列存储卡详细测评
iPhone8什么时候上市最新消息汇总:iPhone8即将发布备货量仅有300万部,十周年纪念款iPhone8或将一机难求
电动牙刷哪个品牌好用?2020热销声波牙刷排行榜
加贺富仪艾这样玩转物联网落地
stm8s可外部扩展多少IO口 如何设置stm8的中断寄存器 STM8S单片机CC寄存器的作用