大家好,今天来为大家分享三层架构网站源码分享的一些知识点,和三层架构设计的问题解析,大家要是都明白,那么可以忽略,如果不太清楚的话可以看看本篇文章,相信很大概率可以解决您的问题,接下来我们就一起来看看吧!
引言
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容,不过在此之前,先上一张图概述Java-NIO的整体结构:
观察上述结构,其实Buffer、Channel的定义并不算复杂,仅是单纯的三层结构,因此对于源码这块不再去剖析,有兴趣的根据给出的目录结构去调试源码,自然也能摸透其原理实现。
而最关键的是Selector选择器,它是整个NIO体系中较为复杂的一块内容,同时它也作为Java-NIO与内核多路复用模型的“中间者”,但在上述体系中,却出现了之前未曾提及过的SelectorProvider系定义,那么它的作用是干嘛的呢?主要目的是用于创建选择器,在Java中创建一般是通过如下方式:
//创建Selector选择器\nSelectorselector=Selector.open();\n\n//Selector类→open()方法\npublicstaticSelectoropen()throwsIOException{\nreturnSelectorProvider.provider().openSelector();\n}\n复制代码
从源码中可明显得知,选择器最终是由SelectorProvider去进行实例化,不过值得一提的是:Selector的实现是基于工厂模式与SPI机制构建的。对于不同OS而言,其对应的具体实现并不相同,因此在Windows系统下,我们只能观测到WindowsSelectorXXX这一系列的实现,而在Linux系统时,对于的则是EPollSelectorXXX这一系列的实现,所以要牢记的是,Java-NIO在不同操作系统的环境中,提供了不同的实现,如下:
Windows:selectUnix:pollMac:kqueueLinux:epoll
当然,本次则重点剖析Linux系统下的select、poll、epoll的具体实现,对于其他系统而言,原理大致相同。
一、JDK层面的源码入口
简单的对于Java-NIO体系有了全面认知后,接下来以JDK源码作为入口进行剖析。在Java中,会通过Selector.select()方法去监听事件是否被触发,如下:
//轮询监听选择器上注册的通道是否有事件被触发\nwhile(selector.select()>0){}\n\n//Selector抽象类→select()抽象方法\npublicabstractintselect()throwsIOException;\n\n//SelectorImpl类→select()方法\npublicintselect()throwsIOException{\nreturnthis.select(0L);\n}\n//SelectorImpl类→select()完整方法\npublicintselect(longvar1)throwsIOException{\nif(var1<0L){\nthrownewIllegalArgumentException(&34;);\n}else{\nreturnthis.lockAndDoSelect(var1==0L?-1L:var1);\n}\n}\n复制代码
当调用Selector.select()方法后,最终会调用到SelectorImpl类的select(longvar1)方法,而在该方法中,又会调用lockAndDoSelect()方法,如下:
//SelectorImpl类→lockAndDoSelect()方法\nprivateintlockAndDoSelect(longvar1)throwsIOException{\n//先获取锁确保线程安全\nsynchronized(this){\n//在判断当前选择是否处于开启状态\nif(!this.isOpen()){\n//如果已关闭则抛出异常\nthrownewClosedSelectorException();\n}else{//如若处于开启状态\n//获取所有注册在当前选择器上的事件\nSetvar4=this.publicKeys;\nintvar10000;\n//再次加锁\nsynchronized(this.publicKeys){\n//获取所有已就绪的事件\nSetvar5=this.publicSelectedKeys;\n//再次加锁\nsynchronized(this.publicSelectedKeys){\n//真正的调用select逻辑,获取已就绪的事件\nvar10000=this.doSelect(var1);\n}\n}\n//返回就绪事件的数量\nreturnvar10000;\n}\n}\n}\n复制代码
在该方法中,对于其他逻辑不必太过在意,重点可注意:最终会调用doSelect()触发真正的逻辑操作,接下来再看看这个方法:
//SelectorImpl类→doSelect()方法\nprotectedabstractintdoSelect(longvar1)throwsIOException;\n\n//WindowsSelectorImpl类→doSelect()方法\nprotectedintdoSelect(longvar1)throwsIOException{\n//先判断一下选择器上是否还有注册的通道\nif(this.channelArray==null){\nthrownewClosedSelectorException();\n}else{//如果有的话\n//先获取一下阻塞等待的超时时长\nthis.timeout=var1;\n//然后将一些取消的事件从选择器上移除\nthis.processDeregisterQueue();\n//再判断一下是否存在线程中断唤醒\n//这里主要是结合之前的wakeup()方法唤醒阻塞线程的\nif(this.interruptTriggered){\nthis.resetWakeupSocket();\nreturn0;\n}else{//如果没有唤醒阻塞线程的需求出现\n//先判断一下辅助线程的数量(守护线程),多则减,少则增\nthis.adjustThreadsCount();\n//更新一下finishLock.threadsToFinish为辅助线程数\nthis.finishLock.reset();\n//唤醒所有的辅助线程\nthis.startLock.startThreads();\ntry{\n//设置主线程中断的回调函数\nthis.begin();\n\ntry{\n//最终执行真正的poll逻辑,开始拉取事件\nthis.subSelector.poll();\n}catch(IOExceptionvar7){\nthis.finishLock.setException(var7);\n}\n//唤醒并等待所有未执行完的辅助线程完成\nif(this.threads.size()>0){\nthis.finishLock.waitForHelperThreads();\n}\n}finally{\nthis.end();\n}\n//检测状态\nthis.finishLock.checkForException();\nthis.processDeregisterQueue();\n//获取当前选择器监听的事件的触发数量\nintvar3=this.updateSelectedKeys();\n//本轮poll结束,重置WakeupSocket,为下次执行做准备\nthis.resetWakeupSocket();\n//最终返回获取到的事件数\nreturnvar3;\n}\n}\n}\n复制代码
整个过程下来其实也并不短暂,但大体就分为三步:
poll\n
在这里面,有一个辅助线程的概念,这跟最大文件描述符有关,每当选择器上注册的通道数超过1023时,新增一条线程来管理这些新增的通道。其实是1024,但其中有一个要用于唤醒,所以是1023(这里看可能有些懵,但待会分析过后就理解了)。
在这个过程中,最最最关键点在于其中的一行代码:
this.subSelector.poll();\n复制代码
在这里调用了poll方法,执行具体的事件拉取逻辑,进一步往下走:
//WindowsSelectorImpl类→poll()方法\nprivateintpoll()throwsIOException{\nreturnthis.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress,\nMath.min(WindowsSelectorImpl.this.totalChannels,1024),\nthis.readFds,this.writeFds,this.exceptFds,\nWindowsSelectorImpl.this.timeout);\n}\n\n//WindowsSelectorImpl类→poll0()方法\nprivatenativeintpoll0(longvar1,intvar3,int[]var4,\nint[]var5,int[]var6,longvar7);\n复制代码
最后会调用WindowsSelectorImpl.poll()方法,而该方法最终会调用本地的native方法:poll0()方法,而在JVM的源码实现中,该方法最终会调用内核所提供的函数。
OK~,由于Windows有IDEA工具辅助,所以方便调试源码,因此这里以WindowsSelectorXXX系的举例说明,但由于整个Java-NIO的核心组件,都是基于工厂模式编写的源码,所以其他操作系统下的源码位置也相同,仅最终调用的内核函数不同!!!
最终稍做总结,JDK层面的源码入口,核心流程如下:
①Selector抽象类→select()抽象方法②SelectorImpl类→select()方法③SelectorImpl类→lockAndDoSelect()方法④SelectorImpl类→doSelect()方法⑤XxxSelectorImpl类→doSelect()方法⑥XxxSelectorImpl类→poll()方法⑦XxxSelectorImpl类→JNI本地的poll0()方法
如若在Windows系统下,上述的XxxSelectorImpl类则为WindowsSelectorImpl,同理,如若在Linux系统下,XxxSelectorImpl类则为EpollSelectorImpl。
最后,如果大家对于JDK层面的EPoll感兴趣,可自行反编译Linux版的JDK源码,EpollSelectorXXX的相关定义位于:jdk\\src\\solaris\\classes\\sun\\nio\\ch\\目录下。
二、JDK源码级别的入口
经过第一阶段的分析后,会发现最终其实调用了native本地方法poll0(),在之前的《JVM运行时数据区-本地方法栈》的文章提到过,当程序执行时碰到native关键字修饰的方法时,会调用C/C++所编写的本地方法库中的实现,那么又该如何查找native方法对应的源码呢?接着一起来聊一下。
①由于Oracle-jdk是收费的,所以咱们首先下载open-jdk1.8的源码,可以自行在Open-JDK官网下载,但官网下载时,常常会由于网络不稳定而中断,下载起来相当费劲,因此也为大家提供一下《open-jdk1.8》的源码链接。
②下载之后解压源码包,然后进入jdk8-master\\jdk\\src\\目录,在其中你会看到不同操作系统下的Java实现,JDK源码会以操作系统的类型分包,不同系统的对应不同的实现,如下:
但关于Linux系统下的Java-NIO实现,实际上并不在linux目录中,而是在solaris目录,进入solaris目录如下:
solaris目录中还包含了LinuxOS、SunOS(SolarisOS/UnixOS)以及MacOS等操作系统下的Java-NIO实现,但关于MacOS下的Java-NIO完整实现,则位于前面的macosx目录中,这里仅包含一部分,结构如下:
观察上图会发现,solaris目录中包含了KQueue、EPoll、Poll、DevPoll等IO多路复用模型的Java实现,但关于Mac-KQueue的完整实现则在macosx目录。
OK~,到目前为止大家对于JDK源码的目录结构应该有了基本认知。
稍微总结一下,重点就是搞清楚两个位置:
?jdk8-master\\jdk\\src\\xxxOS\\classes\\sun\\nio\\ch:对应nio包下的Java代码。?jdk8-master\\jdk\\src\\xxxOS\\native\\sun\\nio\\ch:对应nio包中native方法的JNI代码。
③搞清楚JDK源码目录的结构后,那以之前分析的Windows-NIO为例:
privatenativeintpoll0(longvar1,intvar3,int[]var4,\nint[]var5,int[]var6,longvar7);\n复制代码
对于poll0()这个本地方法,又该如何查找对应的源码呢?根据上述的源码结构,先去到\\windows\\native\\sun\\nio\\ch目录中,然后找到与之对应的WindowsSelectorImpl.c文件,最终就能在该文件中定位到对应的JNI方法:Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(名字略微有些长)。
④找到对应的JNI方法源码后,其中存在这么一行:
观察之后不难发现,其实最终还会调用到OS内核的提供的select()函数,所以poll0()实际上会依赖OS提供的多路复用函数实现相应的功能,对于其他操作系统而言,也是同理。
但是接下来只会重点叙述Linux下的三大IO多路复用函数:select、poll、epoll,而对于Windows-select、Mac-kqueue不会进行深入讲解(不是不想分析,而是由于Windows、Mac系统都属于闭源的,想分析也无法获取其具体的源码实现过程)。
三、文件描述符与自实现网络服务器
到目前可得知:Java中的NIO最终会依赖于操作系统所提供的多路复用函数去实现,而Linux系统下对应的则是epoll模型,但epoll的前身则是select、poll,因此我们先分析select、poll多路复用函数,再分析其缺点,逐步引出epoll的由来,最终进一步对其进行全面剖析。
相信大家在学习Linux时,都听说过“Linux本质上就是一个文件系统”这句话,在Linux-OS中,万事万物皆为文件,连网络连接也不例外,因此在分析多路复用模型之前,咱们首先对这些基础概念做一定了解。
3.1、文件描述符(FD)
在上述中提到过:Linux的理念就是“一切皆文件”,在Linux中几乎所有资源都是以文件的形式呈现的。如磁盘的数据是文件,网络套接字是文件,系统配置项也是文件等等,所有的数据内容在Linux都是通过文件系统来管理的。
既然所有的内容都是文件,那当我们要操作这些内容时,又该如何处理呢?为了方便系统执行,Linux都是通过文件描述符FileDescriptor对文件进行操作,对于文件描述符这个概念可以通过一个例子来理解:
Objectobj=newObject();\n复制代码
上述是Java创建对象的一行代码,类比Linux的文件系统,后面newObject()实例化出来的对象可以当成是具体的文件内容,而前面的引用obj则可理解为是文件描述符。Linux通过FD操作文件,其实本质上与Java中通过reference引用操作对象的过程无异。
而当出现网络套接字连接时,所有的网络连接都会以文件描述符的形式在内核中存在,也包括后面会提及的多路复用函数select、poll、epoll都会基于FD对网络连接进行操作,因此先阐明这点,作为后续分析的基础。
3.2、自己设计网络连接服务器
在分析之前,我们先自己设想一下,如果有个需求:请自己设计一套网络连接系统,那么此时你会怎么做呢?此刻例如来了5个网络连接,如下:
那么又该如何处理这些请求呢?最简单的方式:
对于每个到来的网络连接都为其创建一条线程,每个连接由单独的线程负责处理,所以最初的BIO也是这样来的,由于设计起来非常简单,所以它成为了最初的网络IO模型,但这种方式的缺陷非常明显,在之前的BIO章节也曾分析过,无法支撑高并发的流量访问,因此这种多线程的方式去实现自然行不通了,兜兜转转又得回到单线程的角度去思考,单线程如何处理多个网络请求呢?最简单的方式,伪代码如下:
//不断轮询监听所有的网络连接\nwhile(true){\n//遍历所有的网络套接字连接\nfor(SocketFDxFD:FDS){\n//判断网络连接中是否有数据\nif(xFD.data!=null){\n//从套接字中读取网络数据\nreadData();\n//将网络数据交给应用程序处理(写入对应的程序缓冲区)\nprocessingData();\n//……\n}\n}\n}\n复制代码
如上代码,当有网络连接到来时,将其加入FDS数组中,然后由单条线程不断的轮询监听所有网络套接字,如果套接字中有数据,则从中将网络数据读取出来,然后将读取到的网络数据交给应用程序处理。
这似乎是不是就通过单线程的方式解决了多个网络连接的问题?答案是Yes,但相较而言,性能自然不堪入目,如果内核是这样去处理网络连接,对于并发支持自然也上不去,那Linux内核具体是如何处理的呢?一起来看看。
四、多路复用函数-select()
在JDK1.8的源码中,刚刚似乎并未发现Selectxxx这系列的定义,这是由于Linux内核2.6之后的版本中,已经使用epoll代替了select,所以对应的JDK1.5之后版本,也将Linux-select的实现给移除了,所以如若想观测到Linux-select相关的实现,那还需先安装一个kernel-2.6以下的Linux系统,以及还需要下载JDK1.5的源码,这样才能分析完整的select实现。
我大致过了一下内核中的源码,对于select函数的实现大致在2000行左右,大致看下来后,由于对C语言没有那么熟悉,并且源码实现较长,因此后续不再以全源码链路的方式剖析,而是适当结合部分核心源码进行阐述。当然,如若你的C语言功底还算扎实,那可以下载《Linux2.6.28.6版本内核源码》解压调试。
先讲清楚接下来的分析思路,在后续分析IO多路复用函数时,大体会以调用入口→函数定义→核心结构体→核心源码→函数缺陷这个思路进行展开。
4.1、Java-select函数的JNI入口
对于Open-JDK1.4、1.5的源码,由于年代较久远了,实在没有找到对应的JDK源码,所以在这里分析Linux-select函数时,就以前面分析的Windows-select思路举例说明,如下:
①Java中通过调用选择器的select()方法监听客户端连接。②线程执行时,会执行到当前平台对应的选择器实现类的doSelect()方法。③接着会调用实现类对应的poll()轮询方法,最终在该方法中会调用其native方法。④当线程需要执行本地方法时,触发JNI调用,会在本地方法库中查找对应的C实现。⑤定位到native本地方法对应的C语言函数,然后执行对应的C代码。⑥在C代码的函数中,最终会发起系统调用,那假设此时系统调用的函数为select()。
此时,对于Java是如何调用底层操作系统内核函数的过程就分析出来了,但是由于这里没有下载到对应版本的源码,因此无法通过源码进行演示,但就算没有对应的源码作为依据也无大碍,因为无论是什么类型的操作系统,也无论调用的是哪个多路复用函数,本质上入口都是相同的,只是JNI调用时会存在些许差异。
4.2、内核select函数的定义
OK~,得知了Java-NIO执行的前因后果后,现在来聊一聊最初NIO会调用的系统函数:select,在Linux中的定义如下:
//定义位于/sys/select.h文件中\nintselect(intnfds,fd_set*readfds,fd_set*writefds,\nfd_set*exceptfds,structtimeval*timeout);\n复制代码
select函数定义中,存在五个参数,如下:
nfds:表示FDS中有效的FD数量,全部文件描述符的最大值+1。readfds:表示需要监控读事件发生的文件描述符集合。writefds:表示需要监控写事件发生的文件描述符集合。exceptfds:表示需要监控异常/错误发生的文件描述符集合。timeout:表示select在没有事件触发的情况下,会阻塞的时间。
4.3、select结构体-fd_set、timeval
在上述中简单了解select的定义与参数后,大家可能会有些晕乎乎的,这是由于这五个参数中涉及到两组类型的定义,分别为fd_set、timeval,先来看看它们是如何定义的:
//相关定义位于linux/types.h、linux/posix_types.h文件中\n//——-linux/types.h———-\n//这里定义了一个__kerenl_fd_set的类型,别名为fd_set。\ntypedef__kerenl_fd_setfd_set;\n省略其他…..\n\n//——-linux/posix_types.h———-\n/*\nunsignedlong表示无符号长整型,占4bytes/32bits\nsizeof()函数是求字节的长度,sizeof(unsignedlong)=4\n因此最终这里的__NFDBITS=(8*4)=32\n*/\ndefine__NFDBITS(8*sizeof(unsignedlong))\n\n//这里限制了最大长度为1024(可修改,不推荐)\ndefine__FD_SETSIZE1024\n\n//根据前面的__NFDBITS求出long数组的最大容量为:1024/32=32个\ndefine__FD_SET_LONGS(__FD_SETSIZE/__NFDBITS)\n\n//这两组定义则是用于置位、复位(清除置位)的\ndefine__FDELT(d)((d)/__NFDBITS)\ndefine__FDMASK(d)(1UL<<(d)%__NFDBITS)\n\n//这里定义了__kerenl_fd_set类型,本质上是一个long数组\ntypedefstruct{\nunsignedlongfds_bits[__FDSET_LONGS];\n}__kerenl_fd_set;\n复制代码
观察上述源码,其实你会发现fd_set的定义是__kerenl_fd_set类型的,而__kerenl_fd_set的定义本质上就是一个long数组,同时在__kerenl_fd_set的定义中,也声明了最大长度为1024,相信了解过多路复用函数的小伙伴都知道select模型的最大缺陷之一就在于:最多只能监听1024个文件描述符,而对于具体是为什么,相信看到这个源码大家就彻底清楚了。
PS:首先基于上述的知识,已经得知最大长度为1024,但这1024并非代表着:数组可以拥有1024个long元素,而是限制了这个long数组最多只能有1024个比特位的长度,也就是数组中最多能拥有1024/32=32个元素。对于这点,在源码中也有定义,大家可参考源码中的注释。
OK~,那这个long类型的数组究竟有什么作用呢?简单来说明一下,在这个fd_set的数组中,其实每个位对应着一个FD文件描述符的状态,0代表没有事件发生,1则代表有事件触发,如下图:
在这个数组中,所有的long元素,在计算机底层本质上都会被转换成bit存储,而每一个bit位都对应着一个FD,所以这个数组本质上就组成了一个位图结构,同时为了方便操作这个位图,在之前的sys/select.h文件中还提供了一组宏函数,如下:
//位于/sys/select.h文件中\n//将一个fd_set数组所有位都置零\nintFD_ZERO(intfd,fd_set*fdset);\n//将指定的某个位复位(赋零)\nintFD_CLR(intfd,fd_set*fdset);\n//将指定的某个位置位(赋一)\nintFD_SET(intfd,fd_set*fd_set);\n//检测指定的某个位是否被置位\nintFD_ISSET(intfd,fd_set*fdset);\n\n//这里则是上述宏函数的实现(位操作过程)\ndefine__FD_SET(d,set)\\\n((void)(__FDS_BITS(set)[__FD_ELT(d)]|=__FD_MASK(d)))\ndefine__FD_ISSET(d,set)\\\n((__FDS_BITS(set)[__FD_ELT(d)]&__FD_MASK(d))!=0)\n复制代码
对于定义的几组宏函数,可以参考上述注释中的解释,而对于这些函数是如何实现的,大家可以自行阅读贴出的源码。接下来再看看timeval结构体是如何定义的:
structtimeval{\nlongtv_sec;/*秒*/\nlongtv_usec;/*毫秒*/\n};\n复制代码
其实这个结构体就是一个阻塞的时间,好比select传入的timeout参数为3,则timeval.tv_sec=3、timeval.tv_usec=3000,代表调用select()没有获取到有效事件的情况下,在3s内会不断循环检测。当然,这个timeout的值会分为三种情况:
0:表示调用select()函数后不等待,没有就绪事件时直接返回。NULL:表示调用select()函数后无限等待,阻塞至出现中断信号或触发事件后返回。正数:表示调用select()函数后,在指定的时间内等待事件触发,超时则返回。
至此,对于select()函数所需参数中,涉及到的两个结构体已经弄明白了,那么再回来看看select()的五个参数。
intselect(intnfds,fd_set*readfds,fd_set*writefds,\nfd_set*exceptfds,structtimeval*timeout);\n复制代码
调用select()时,中间的三个参数要求传入fd_set类型,它们分别对应着:那些文件描述符需要监听读事件发生、那些文件描述符需要监听写事件发生、那些文件描述符需要监听异常错误发生。当调用select()函数后会陷入阻塞,直到有描述符的事件就绪(有数据可读、可写或出现异常错误)或超时后才会返回。而select()函数返回也会存在三种状态:
0:当描述符集合中没有事件触发,并且超出设置的时间后,会返回0。-1:当select执行过程中,出现异常/错误时则会返回-1。正数:如果监视的文件描述符集合中有事件发生(有数据),则会对应的事件数量。
4.4、select()函数的使用案例
在上述中已经对于select()函数的一些基础知识建立了认知,接下来上个伪代码感受一下select()函数的使用过程:
/*———-①———-*/\n//创建服务端socket套接字,并监听客户端连接\nserverSockfd=socket(AF_INET,SOCK_STREAM,0);\n//省略…..\nbind(serverSockfd,IP,Port);\nlisten(serverSockfd,numfds);\n//这里是已经接收的客户端连接集合\nfds[numfds]=accept(serverSockfd,…..);\n\n/*———-②———-*/\n//将所有的客户端连接,分别加入对应的位图中\nFD_SETreadfds,writefds,exceptfds;\nintread_count=0,write_count=0,except_count=0;\nfor(i=0;i<numfds;i++){\nif(fds[i].events==读取事件){\n//加入readfds\n}\nif(fds[i].events==写入事件){\n//加入writefds\n}\n//省略…..\n}\n\n/*———-③———-*/\n//求出最大的fds值\nmaxfds=….;\nstructtimevaltimevalue,*tv;\n//省略…..\n\n/*———-④———-*/\nwhile(1){\n//初始化位图\nFD_ZERO(readfds);\nFD_ZERO(writefds);\nFD_ZERO(exceptfds);\n//分别对每个位图中需要监听的FD进行置位\nfor(i=0;i<numfds;i++){\nif(fds[i].events==读取事件){\n\tFD_SET(fds[i],&readfds);\n\t}\n\t//省略其他置位处理…..\n}\n\n//调用select函数\nintresult=select(maxfds+1,&readfds,&writefds,&exceptfds,tv);\n\n/*———-⑤———-*/\nif(result==0){\n\t//处理超时并返回….\n}\nif(result<0){\n\t//处理异常并返回….\n}\n\n/*———-⑥、⑦———-*/\n//能执行到这里,代表select()返回大于0\nfor(i=0;i<numfds;i++){\n\tif(FD_ISSET(fds[i],&readfds)){\n\t//读取被置位的socket…..\n\tread(fds[i],buffer,0,MAXBUF);\n\t}\n\t//省略其他……\n}\n}\n复制代码
上述的伪代码虽然看着较多,但本质上并不难,大体分为如下几步:
Socket\nselect()\nFD\nFD\n
对于这个伪代码,其实也是调用select()函数的通用模型,以Java的JNI调用为例,其实大体的过程也是相同的,如下:
没有下载到JDK1.5的源码,所以以Windows-select的调用为例。
4.5、内核select函数核心源码
在上述过程中,我们调用了select()函数实现了IO多路复用,但调用之后select()的执行过程,相对而言其实是未知,那么接着再来看看select()的核心源码,剖析一下调用select后,内核究竟会如何处理。
内核源码的执行流程:sys_select()→SYSCALL_DEFINE5()→core_sys_select()→do_select()→f_op->poll/tcp_poll()。
所有的系统调用,都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、select,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_select,因此上述的sys_select()其实就是select()函数在内核中对应的函数。
接着来看看SYSCALL_DEFINE5()、core_sys_select()函数的内容:
//位于fs/select.c文件中(sys_select函数)\nSYSCALL_DEFINE5(select,int,n,fd_set__user*,inp,fd_set__user*,outp,\nfd_set__user*,exp,structtimeval__user*,tvp)\n{\n\tstructtimespecend_time,*to=NULL;\n\tstructtimevaltv;\n\tintret;\n//判断是否传入了超时时间\n\tif(tvp){\n\t\tif(copy_from_user(&tv,tvp,sizeof(tv)))\n\t\t\treturn-EFAULT;\n\n\t\tto=&end_time;\n\t\t//如果已经到了超时时间,则中断执行并返回\n\t\tif(poll_select_set_timeout(to,\n\t\t\t\ttv.tv_sec+(tv.tv_usec/USEC_PER_SEC),\n\t\t\t\t(tv.tv_usec%USEC_PER_SEC)*NSEC_PER_USEC))\n\t\t\treturn-EINVAL;\n\t}\n//未超时或没有设置超时时间的情况下,调用core_sys_select\n\tret=core_sys_select(n,inp,outp,exp,to);\n\tret=poll_select_copy_remaining(&end_time,tvp,1,ret);\n\n\treturnret;\n}\n\n//位于fs/select.c文件中(core_sys_select函数)\nintcore_sys_select(intn,fd_set__user*inp,fd_set__user*outp,\n\t\t\tfd_set__user*exp,structtimespec*end_time)\n{\n\tfd_set_bitsfds;\n\tvoid*bits;\n\tintret,max_fds;\n\tunsignedintsize;\n\tstructfdtable*fdt;\n\t/*由于涉及到了用户态和内核态的切换,因此将位图存储在栈上,\n\t(尽量提升状态切换时的效率,这里采用栈的方式存储)*/\n\tlongstack_fds[SELECT_STACK_ALLOC/sizeof(long)];\n\n\tret=-EINVAL;\n\tif(n<0)\n\t\tgotoout_nofds;\n\n\t//先计算出max_fds值\n\trcu_read_lock();\n\tfdt=files_fdtable(current->files);\n\tmax_fds=fdt->max_fds;\n\trcu_read_unlock();\n\tif(n>max_fds)\n\t\tn=max_fds;\n\n\t//根据前面计算的max_fds值,判断一下前面开栈空间是否足够\n\t//(在这里涉及到一个新的结构体:fd_set_bits,稍后详细分析)\n\tsize=FDS_BYTES(n);\n\tbits=stack_fds;\n\tif(size>sizeof(stack_fds)/6){\n\t\t//如果空间不够则调用内核的kmalloc为fd_set_bits分配更大的空间\n\t\tret=-ENOMEM;\n\t\tbits=kmalloc(6*size,GFP_KERNEL);\n\t\tif(!bits)\n\t\t\tgotoout_nofds;\n\t}\n\t//将fd_set_bits中六个位图指针指向分配好的内存位置\n\tfds.in=bits;\n\tfds.out=bits+size;\n\tfds.ex=bits+2*size;\n\tfds.res_in=bits+3*size;\n\tfds.res_out=bits+4*size;\n\tfds.res_ex=bits+5*size;\n\n//将用户空间提交的三个fd_set拷贝到内核空间\n\tif((ret=get_fd_set(n,inp,fds.in))||\n\t(ret=get_fd_set(n,outp,fds.out))||\n\t(ret=get_fd_set(n,exp,fds.ex)))\n\t\tgotoout;\n\tzero_fd_set(n,fds.res_in);\n\tzero_fd_set(n,fds.res_out);\n\tzero_fd_set(n,fds.res_ex);\n\n//调用select模型的核心函数do_select()\n\tret=do_select(n,&fds,end_time);\n\n\tif(ret<0)\n\t\tgotoout;\n\t//检测到有信号则系统调用退出,返回用户空间执行信号处理函数\n\tif(!ret){\n\t\tret=-ERESTARTNOHAND;\n\t\tif(signal_pending(current))\n\t\t\tgotoout;\n\t\tret=0;\n\t}\n\n\tif(set_fd_set(n,inp,fds.res_in)||\n\tset_fd_set(n,outp,fds.res_out)||\n\tset_fd_set(n,exp,fds.res_ex))\n\t\tret=-EFAULT;\n//goto跳转的对应点\nout:\n\tif(bits!=stack_fds)\n\t\tkfree(bits);\nout_nofds:\n\treturnret;\n}\n复制代码
源码看过去,看起来有些多,对于C语言不太熟悉的小伙伴可能看得会一脸懵,但没关系,我们不去讲细了,重点理解其主干内容,上述源码分为如下几步:
①先判断调用select()时,是否设置了超时时间:core_sys_select()②计算出最大的文件描述符,然后采用开栈方式存储递交的参数值。③根据计算出的max_fds值,判断开栈空间能否可以存储递交的参数值:不能:调用内核的kmalloc分配器为fd_set_bits分配更大的空间(新分配的内存是在堆)。能:更改fd_set_bits中的指针指向,然后将递交的三个fd_set拷贝到内核空间。④上述工作全部已就绪后,调用select()函数中的核心函数:do_select()处理。
在上述过程中,理解起来并不复杂,唯一的疑惑点就在于多出了一个新的结构体:fd_set_bits,那它究竟是什么意思呢?先来看看它的定义:
typedefstruct{\nunsignedlong*in,*out,*ex;\nunsignedlong*res_in,*res_out,*res_ex;\n}fd_set_bits;\n复制代码
很明显,fd_set_bits是由六个元素组成的,这六个元素分别对应着六个位图,其中前三个则对应调用select()函数时递交的三个参数:readfds、writefds、exceptfds,而后三个则对应着select()执行完成之后返回地位图,为什么还需要有后面三个呢?
因为select()在遍历需要监听的文件描述符列表时,也需要三个对应地位图来记录哪些FD中是有数据的,因此也需要有三个位图对应着传入的三个位图,在select()执行完成后,如若有Socket中存在数据需要处理,那则会将这三个位图中对应的Socket位置进行置位,然后从内核空间再将其拷贝回用户空间,以供程序处理。
OK~,了解fd_set_bits结构后,对于core_sys_select函数中做的工作就自然理解了,一句话总结一下这个函数做的工作:
core_sys_select只不过是在为后面要调用的do_select()函数做准备工作而已。
当然,在上述的core_sys_select函数中还涉及到两个函数:get_fd_set()、set_fd_set(),其实现如下:
//调用了copy_from_user()函数,也就是从用户空间拷贝数据到内核空间\nstaticinline\nintget_fd_set(unsignedlongnr,void__user*ufdset,unsignedlong*fdset)\n{\n\tnr=FDS_BYTES(nr);\n\tif(ufdset)\n\t\treturncopy_from_user(fdset,ufdset,nr)?-EFAULT:0;\n\n\tmemset(fdset,0,nr);\n\treturn0;\n}\n\n//调用了__copy_to_user()函数,也就是将数据从内核空间拷贝回用户空间\nstaticinlineunsignedlong__must_check\nset_fd_set(unsignedlongnr,void__user*ufdset,unsignedlong*fdset)\n{\n\tif(ufdset)\n\t\treturn__copy_to_user(ufdset,fdset,FDS_BYTES(nr));\n\treturn0;\n}\n复制代码
从最终调用的copy_from_user()、copy_to_user()两个函数中就能得知,这就是用于用户空间与内核空间之间数据拷贝的函数而已。
关于三层架构网站源码分享的内容到此结束,希望对大家有所帮助。