相关视频推荐面试正经“八卦随笔”网络原理tcp/udp、网络编程epoll/reactorepoll原理分析及reactor模型应用epoll原理分析及三手四波处理LinuxC++后台服务器开发架构师免费学习地址1.等待队列Linux内核中的等待队列有很多用途,可以用于中断处理、进程同步和定时。我们这里只是说一个进程经常要等待某个事件的发生。等待队列实现有条件地等待事件:希望等待特定事件的进程将自己放入适当的等待队列并放弃控制。因此,等待队列代表一组休眠进程,当某个条件为真时,这些进程被内核唤醒。等待队列由一个循环链表实现,由一个等待队列头(wait_queue_head_t)和一个等待队列项(wait_queue)组成,其元素(等待队列项)包含指向进程描述符的指针。每个等待队列都有一个等待队列头(waitqueuehead),等待队列头是一个wait_queue_head_t类型的数据结构来定义等待队列头(相关内容可以在linux/include/wait.h中找到)waitingqueueheadstructure主体定义:structwait_queue_head{spinlock_tlock;//自旋锁变量,用于等待队头structlist_headtask_list;//指向等待队列的list_head};typedef结构__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;【文章福利】:小编整理了一些个人认为比较好的学习书籍和视频资料分享到群档,需要的可以自行添加!(需要自己去挑)1.select/poll的缺点select/poll的缺点是:1.每次调用都需要从用户态反复读入参数。2.每次调用都会重复扫描文件描述符。3、每次调用开始时,要把当前进程放入每个文件描述符的等待队列中。调用结束后,进程从每个等待队列中删除。内核实现2.1主要数据结构:(1)structpoll_table_entry{structfilefilp;wait_queue_twait;//里面有个指针指向一个进程wait_queue_head_twait_address;//等待队列的头部(等待队列由多个wait_queue_t组成,通过双链表连接)};(2)structpoll_table_page{structpoll_table_page下一个;结构poll_table_entry条目;structpoll_table_entryentries[0];};(3)structpoll_wqueues{poll_tablept;//函数指针,通常指向__pollwait或nullstructpoll_table_page*table;interror;};(4)structpoll_list{structpoll_list*next;//按内存页连接,因为kmalloc有申请数据限制intlen;//从用户空间传入的fd个数structpollfdentries[0];//Store存储在用户空间的数据};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);kernel2.6.9poll实现代码分析[fs/select.c-->sys_poll]asmlinkagelongsys_poll(structpollfd__user*ufds,unsignedintnfds,longtimeout){structpoll_wqueues表;结构poll_list*head;结构轮询列表*步行;……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;elsewalk->next=pp;walk=pp;if(copy_from_user(pp->entries,ufds+nfds-i,sizeof(structpollfd)*pp->len)){err=-EFAULT;gotoout_fds;}i-=pp->len;}/这一大堆代码就是创建一个链表,链表的每个节点是一个页面大小(通常是4k)。该链表节点由指向structpoll_list的指针控制。每个poll_list的entries成员指向一个structpollfd。上面的循环是将用户模式结构pollfd复制到这些条目中。通常用户程序的poll调用会监听几个fd,所以上面的链表通常只需要一个节点,也就是操作系统的一页。但是,当用户传入大量fd时,由于poll系统调用每次都要将所有structpollfds复制到内核中,此时参数传递和页面分配成为poll系统调用的性能瓶颈。/fdcount=do_poll(nfds,head,&table,timeout);}其中poll_initwait比较关键。从字面上看,它应该初始化变量表。注意table是整个poll执行过程中的一个关键变量。structpoll_table实际上只包含一个函数指针。现在让我们看看poll_initwait在做什么void__pollwait(structfilefilp,wait_queue_head_twait_address,poll_table*p);voidpoll_initwait(structpoll_wqueues*pwq){&(pwq->pt)->qproc=__pollwait;/setCallbackfunction/...}显然,poll_initwait的主要作用是将表变量的成员poll_table对应的回调函数设置为__pollwait。这个__pollwait不仅poll系统调用需要,select系统调用也需要。说白了,这是操作系统异步操作的“皇后”回调函数。当然epoll不用这个。它添加了一个新的回调函数来实现其高效运行。最后一句do_poll,我们跟进:staticintdo_poll(unsignedintnfds,structpoll_listlist,structpoll_wqueueswait,longtimeout){intcount=0;poll_table*pt=&wait->pt;对于(;;){结构poll_list*walk;set_current_state(TASK_INTERRUPTIBLE);步行=列表;while(walk!=NULL){do_pollfd(walk->len,walk->entries,&pt,&count);步行=步行->下一步;}pt=NULL;如果(计数||!超时||signal_pending(当前))中断;计数=等待->错误;如果(计数)中断;超时=schedule_timeout(超时);/*让current挂掉,其他进程运行,超时后再回来运行current*/}__set_current_state(TASK_RUNNING);返回计数;}注意set_current_state和signal_pending,这两句保证当用户程序调用poll后挂起时,signal可以让程序快速发起poll调用,而平时的系统调用不会被signal打断。再看do_poll函数,主要是在循环中等待count大于0才跳出循环,而count主要是do_pollfd函数处理的。注意标记为红色的while循环。当用户传入很多fds时(比如1000个),do_pollfd会被调用很多次。轮询效率瓶颈的另一个原因就在这里。do_pollfd是为传入的每个fd调用它们对应的poll函数,简化调用过程,如下:[fs/select.c-->sys_poll()-->do_poll()]staticvoiddo_pollfd(unsignedintnum,structpollfdfdpage,poll_tablepwait,intcount){...structfile*file=fget(fd);file->f_op->poll(file,&(table->pt));...}如果fd对应的是socket,do_pollfd调用网络设备驱动实现的poll;如果fd对应于ext3文件系统上的打开文件,则do_pollfd调用由ext3文件系统驱动程序实现的轮询。一句话,这个file->f_op->poll是由设备驱动实现的,那么设备驱动的poll实现通常是什么样子的呢?其实设备驱动的标准实现是:调用poll_wait,也就是使用设备自己的等待队列作为参数(通常设备都有自己的等待队列,否则一个不支持异步操作的设备会让人很郁闷)调用structpoll_table回调。作为驱动的代表,我们看一下使用tcp时socket的代码:[net/ipv4/tcp.c-->tcp_poll]unsignedinttcp_poll(structfilefile,structsocketsock,poll_table*wait){...poll_wait(文件,sk->sk_sleep,等待);tcp_poll的核心实现是poll_wait,poll_wait是调用structpoll_table对应的回调函数,那么poll系统调用对应的回调函数就是__poll_wait,所以到这里tcp_poll几乎可以理解为一条语句:__poll_wait(file,sk->sk_sleep,等待);从这里也可以看出,每个socket本身都有一个等待队列sk_sleep,所以我们上面说的“设备的等待队列”其实不止一个。这个时候我们看一下__poll_wait的实现:[fs/select.c-->__poll_wait()]void__pollwait(structfilefilp,wait_queue_head_twait_address,poll_table*_p){...}__poll_wait创建了上图的数据结构如图(一个__poll_wait,即一次设备poll调用只创建一个poll_table_entry),通过structpoll_table_entry的wait成员,将当前挂在设备的等待队列上。这里的等待队列是wait_address,对应tcp_pollsk->sk_sleep。现在我们可以回顾一下poll系统调用的原理:首先注册回调函数__poll_wait,然后初始化表变量(类型为structpoll_wqueues),然后复制用户传入的structpollfd(实际上主要是fd)(瓶颈1)),然后依次调用所有fd对应的polls(将current挂到每个fd对应的设备等待队列中)(瓶颈2)。设备收到消息(网络设备)或填写文件数据(磁盘设备)后,会唤醒设备等待队列上的进程,然后current被唤醒。current唤醒后离开sys_poll的操作比较简单,这里就不逐行分析了。
