一、一个线程服务多个客户端,使用非阻塞I/O和水平触发的就绪通知
把网络句柄设置为非阻塞模型,然后使用select()或poll()来告知哪个句柄已有数据在等待处理。此模型是最传统的,在此模型下,由内核告知你某个文件描述符是否准备好,是否已经完成你的任务自从上次内核告知已准备好以来(“水平触发”这个名字来源计算机硬件设计,与其相对的是“边缘触发”,JonathonLemon在它的关于kqueue()的论文中介绍了这两个术语)。
注意:牢记内核的就绪通知仅仅只是个提示,当你试图从一个文件描述符读取数据时,该文件描述符可能并没有准备好。这就是为什么需要在使用就绪通知的时候使用非阻塞模型的原因。
一个重要的瓶颈是read()或sendfile()从磁盘块读取时,如果该页当前并不在内存中。设置磁盘文件描述符为非阻塞没有任何影响。同样的问题也发生在内存映射磁盘文件中。首先一个服务需要磁盘I/O时,进程块和所有的客户端都必须等待,因此最初的非线程的性能就被消耗了。
这也是异步I/O的目的,当然仅限于没有AIO的系统。处理磁盘I/O的工作线程或工作进程也可能遭遇此瓶颈。一条途径就是使用内存映射文件,如果mincore()指明I/O必需的话,那么要求一个工作线程来完成此I/O,然后继续处理网络事件。JefPoskanzer提到Pai,Druschel和Zwaenepoel的Flashweb服务器使用了这个方法,并且他们就此在Usenix'99上做了一个演讲,看上去就好像FreeBSD和Solaris中提供了mincore()一样,但是它并不是SingleUnix Specification的一部分,在Linux的2.3.51的内核中提供了该方法,感谢ChuckLever。
在2003.11的freebsd-hackerslist中,VivekPei上报了一个不错的成果,他们利用系统剖析工具剖析它们的FlashWeb服务器,然后再攻击其瓶颈。其中找到的一个瓶颈就是mincore(猜测毕竟不是好办法),另外一个就是sendfile在磁盘块访问时。他们修改了sendfile(),当需要读取的页不在内存中时则返回类似EWOULDBLOCK的值,从而提高了性能。Theend result of their optimizations is a SpecWeb99 score of about 800on a 1GHZ/1GB FreeBSD box, which is better than anything on file atspec.org.
在非阻塞套接字的集合中,关于单一线程是如何告知哪个套接字是准备就绪的,以下列出了几种方法:
SeePoller_select(cc,h)for an example of how to use select() interchangeably with otherreadiness notification schemes.
有些操作系统(如Solaris8)通过使用了pollhinting技术改进了poll(),该技术由NielsProvos在1999年实现并利用基准测试程序测试过。
SeePoller_poll(cc,h,benchmarks)for an example of how to use poll() interchangeably with otherreadiness notification schemes.
/dev/poll的背后思想就是利用poll()在大部分的调用时使用相同的参数。使用/dev/poll时,首先打开/dev/poll得到文件描述符,然后把你关心的文件描述符写入到/dev/poll的描述符,然后你就可以从/dev/poll的描述符中读取到已就绪的文件描述符。
/dev/poll在Solaris7(seepatchid 106541) 中就已经存在,不过在Solaris8 中才公开现身。在750个客户端的情况下,thishas 10% of the overhead of poll()。
关于/dev/poll在Linux上有多种不同的尝试实现,但是没有一种可以和epoll相比,不推荐在Linux上使用/dev/poll。
SeePoller_devpoll(cc,hbenchmarks) for an example of how to use /dev/poll interchangeably with manyother readiness notification schemes. (Caution - the example is forLinux /dev/poll, might not work right on Solaris.)
-
kqueue()
这是在FreeBSD系统上推荐使用的代替poll的方法(and,soon, NetBSD).
kqueue()即可以水平触发,也可以边缘触发,具体请看下面.
二、一个线程服务多个客户端,使用非阻塞I/O和就绪改变时通知
Readinesschange notification(或边缘触发就绪通知)的意思就是当你给内核一个文件描述符,一段时间后,如果该文件描述符从没有就绪到已经准备就绪,那么内核就会发出通知,告知该文件描述符已经就绪,并且不会再对该描述符发出类似的就绪通知直到你在描述符上进行一些操作使得该描述符不再就绪(如直到在send,recv或者accept等调用上遇到EWOULDBLOCK错误,或者发送/接收了少于需要的字节数)。
当使用Readinesschange notification时,必须准备好处理乱真事件,因为最常见的实现是只要接收到任何数据包都发出就绪信号,而不管文件描述符是否准备就绪。
这是水平触发的就绪通知的相对应的机制。It'sa bit less forgiving of programming mistakes, since if you miss justone event, the connection that event was for gets stuck forever.然而,我发现edge-triggeredreadiness notification可以使编写带OpenSSL的非阻塞客户端更简单,可以试下。
[Banga,Mogul, Drusha '99]详细描述了这种模型.
有几种APIs可以使得应用程序获得“文件描述符已就绪”的通知:
FreeBSD4.3及以后版本,NetBSD(2002.10)都支持kqueue()/kevent(),支持边沿触发和水平触发(请查看JonathanLemon 的网页和他的BSDCon2000关于kqueue的论文)。
就像/dev/poll一样,你分配一个监听对象,不过不是打开文件/dev/poll,而是调用kqueue()来获得。需要改变你所监听的事件或者获得当前事件的列表,可以在kqueue()返回的描述符上调用kevent()来达到目的。它不仅可以监听套接字,还可以监听普通的文件的就绪,信号和I/O完成的事件也可以.
Note:在2000.10,FreeBSD的线程库和kqueue()并不能一起工作得很好,当kqueue()阻塞时,那么整个进程都将会阻塞,而不仅仅是调用kqueue()的线程。
SeePoller_kqueue(cc,h,benchmarks)for an example of how to use kqueue() interchangeably with many otherreadiness notification schemes.
使用kqueue()的例程和库:
2001.7.11,DavideLibenzi提议了一个实时信号的可选方法,他称之为/dev/epoll</a>,该方法类似与实时信号就绪通知机制,但是结合了其它更多的事件,从而在大多数的事件获取上拥有更高的效率。
epoll在将它的接口从一个/dev下的指定文件改变为系统调用sys_epoll后就合并到2.5版本的Linux内核开发树中,另外也提供了一个为2.4老版本的内核可以使用epoll的补丁。
unifyingepoll, aio, 2002 年万圣节前夕的Linux内核邮件列表就统一epoll,aio和其它的eventsources 展开了很久的争论,itmay yet happen,butDavide is concentrating on firming up epoll in general first.
-
Polyakov'skevent (Linux 2.6+) 的最后新闻:2006.2.9和2006.7.9,EvgeniyPolyakov发表了融合epoll和aio的补丁,他的目标是支持网络AIO.See:
-
Drepper的最新网络接口(proposalfor Linux 2.6+)
在2006OLS上,UlrichDrepper提议了一种最新的高速异步网络API.See:
2.4的linux内核可以通过实时信号来分派套接字事件,示例如下:
/* Mask off SIGIO and the signal you want to use. */ sigemptyset(&sigset); sigaddset(&sigset, signum); sigaddset(&sigset, SIGIO); sigprocmask(SIG_BLOCK, &m_sigset, NULL); /* For each file descriptor, invoke F_SETOWN, F_SETSIG, and set O_ASYNC. */ fcntl(fd, F_SETOWN, (int) getpid()); fcntl(fd, F_SETSIG, signum); flags = fcntl(fd, F_GETFL); flags |= O_NONBLOCK|O_ASYNC; fcntl(fd, F_SETFL, flags);
当正常的I/O函数如read()或write()完成时,发送信号。要使用该段的话,在外层循环中编写一个普通的poll(),在循环里面,当poll()处理完所有的描述符后,进入sigwaitinfo()循环。如果sigwaitinfo()或sigtimedwait()返回了实时信号,那么siginfo.si_fd和siginfo_si_band给出的信息和调用poll()后pollfd.fd和pollfd.revents的几乎一样。如果你处理该I/O,那么就继续调用sigwaitinfo()。
如果sigwaitinfo()返回了传统的SIGIO,那么信号队列溢出了,你必须通过临时改变信号处理程序为SIG_DFL来刷新信号队列,然后返回到外层的poll()循环。
SeePoller_sigio(cc,h)for an example of how to use rtsignals interchangeably with manyother readiness notification schemes.
SeeZachBrown's phhttpd 示例代码来如何直接使用这些特点.(Or don't; phhttpd is a bit hard to figure out...)
[Provos,Lever, and Tweedie 2000]描述了最新的phhttp的基准测试,使用了不同的sigtimewait()和sigtimedwait4(),这些调用可以使你只用一次调用便获得多个信号。有趣的是,sigtimedwait4()的主要好处是它允许应用程序测量系统负载(soit could behaveappropriately)(poll()也提供了同样的系统负载测量)。
-
Signal-per-fd
Signal-per-fd是由Chandra和Mosberger提出的对实时信号的一种改进,它可以减少甚至削除实时信号的溢出通过oalescingredundant events。然而是它的性能并没有epoll好.论文(www.hpl.hp.com/techreports/2000/HPL-2000-174.html)比较了它和select(),/dev/poll的性能.
VitalyLuban在2001.5.18公布了一个实现Signal-per-fd的补丁;授权见www.luban.org/GPL/gpl.html.(到2001.9,在很重的负载情况下仍然存在稳定性问题,利用dkftpbench测试在4500个用户时将引发问题.
SeePoller_sigfd(cc,h)for an example of how to use signal-per-fd interchangeably with manyother readiness notification schemes.
该方法目前还没有在Unix上普遍的使用,可能因为很少的操作系统支持异步I/O,或者因为它需要重新修改应用程序(rethinkingyour applications)。在标准Unix下,异步I/O是由"aio_"接口提供的,它把一个信号和值与每一个I/O操作关联起来。信号和其值的队列被有效地分配到用户的进程上。异步I/O是POSIX1003.1b实时标准的扩展,也属于SingleUnix Specification,version 2.
AIO使用的是边缘触发的完成时通知,例如,当一个操作完成时信号就被加入队列(也可以使用水平触发的完成时通知,通过调用aio_suspend()即可,不过我想很少人会这么做).
glibc2.1和后续版本提供了一个普通的实现,仅仅是为了兼容标准,而不是为了获得性能上的提高。
BenLaHaise编写的LinuxAIO实现合并到了2.5.32的内核中,它并没有采用内核线程,而是使用了一个高效的underlyingapi,但是目前它还不支持套接字(2.4内核也有了AIO的补丁,不过2.5/2.6的实现有一定程序上的不同)。更多信息如下:
-
Thepage "KernelAsynchronous I/O (AIO) Support for Linux"which tries to tie together all info about the 2.6 kernel'simplementation of AIO (posted 16 Sept 2003)
-
Round3: aio vs /dev/epoll by Benjamin C.R. LaHaise(presented at 2002 OLS)
-
AsynchronousI/O Suport in Linux 2.5, by Bhattacharya,Pratt, Pulaverty, and Morgan, IBM; presented at OLS '2003
-
DesignNotes on Asynchronous I/O (aio) for Linux bySuparna Bhattacharya -- compares Ben's AIO with SGI's KAIO and a fewother AIO projects
-
LinuxAIO home page - Ben's preliminary patches,mailing list, etc.
-
libaio-oracle- library implementing standard Posix AIO on top of libaio. Firstmentioned by Joel Becker on 18 Apr 2003.
RedHatAS和SuseSLES都在2.4的内核中提供了高性能的实现,与2.6的内核实现相似,但并不完全一样。
2006.2,在网络AIO有了一个新的尝试,具体请看EvgeniyPolyakov的基于kevent的AIO.
1999,SGI为Linux实现了一个高速的AIO</a>,在到1.1版本时,据说可以很好的工作于磁盘I/O和网络套接字,且使用了内核线程。目前该实现依然对那些不能等待Ben的AIO套接字支持的人来说是很有用的。
O'Reilly的"POSIX.4:Programming for the Real World"一书对aio做了很好的介绍.
这里有一个指南介绍了早期的非标准的aio实现,可以看看,但是请记住你得把"aioread"转换为"aio_read"。
注意AIO并没有提供无阻塞的为磁盘I/O打开文件的方法,如果你在意因打开磁盘文件而引起sleep的话,Linus建议你在另外一个线程中调用open()而不是把希望寄托在对aio_open()系统调用上。
在Windows下,异步I/O与术语"重叠I/O"和"IOCP"(I/OCompletion Port,I/O完成端口)有一定联系。Microsoft的IOCP结合了先前的如异步I/O(如aio_write)的技术,把事件完成的通知进行排队(就像使用了aio_sigevent字段的aio_write),并且它为了保持单一IOCP线程的数量从而阻止了一部分请求。(Microsoft'sIOCP combines techniques from the prior art like asynchronous I/O(like aio_write) and queued completion notification (like when usingthe aio_sigevent field with aio_write) with a new idea of holdingback some requests to try to keep the number of running threadsassociated with a single IOCP constant.)更多信息请看 Markrussinovich在sysinternals.com上的文章InsideI/O Completion Ports,JeffreyRichter的书"ProgrammingServer-Side Applications for Microsoft Windows 2000" (Amazon,MSPress),U.S.patent #06223207, or MSDN.
...让read()和write()阻塞.这样不好的地方在于需要为每个客户端使用一个完整的栈,从而比较浪费内存。许多操作系统仍在处理数百个线程时存在一定的问题。如果每个线程使用2MB的栈,那么当你在32位的机器上运行512(2^30/ 2^21=512)个线程时,你就会用光所有的1GB的用户可访问虚拟内存(Linux也是一样运行在x86上的)。你可以减小每个线程所拥有的栈内存大小,但是由于大部分线程库在一旦线程创建后就不能增大线程栈大小,所以这样做就意味着你必须使你的程序最小程度地使用内存。当然你也可以把你的程序运行在64位的处理器上去。
Linux,FreeBSD和Solaris系统的线程库一直在更新,64位的处理器也已经开始在大部分的用户中所使用。也许在不远的将来,这些喜欢使用一个线程来服务一个客户端的人也有能力服务于10000个客户了。但是在目前,如果你想支持更多的客户,你最好还是使用其它的方法。
Foran unabashedly pro-thread viewpoint, see WhyEvents Are A Bad Idea (for High-concurrency Servers)by von Behren, Condit, and Brewer, UCB, presented at HotOS IX. Anyonefrom the anti-thread camp care to point out a paper that rebuts thisone? :-)
LinuxThreads
LinuxTheads是标准Linux线程库的命名。它从glibc2.0开始已经集成在glibc库中,并且高度兼容Posix标准,不过在性能和信号的支持度上稍逊一筹。
NGPT:Next Generation Posix Threads for LinuxPosix线程
NGPT是一个由IBM发起的项目,其目的是提供更好的Posix兼容的Linux线程支持。现在已到2.2稳定版,并且运行良好...但是NGPTteam 公布他们正在把NGPT的代码基改为support-only模式,因为他们觉得这才是支持社区长久运行的最好的方式。NGPT小组将继续改进Linux的线程支持,但主要关注NPTL方面。(Kudosto the NGPT team for their good work and the graceful way theyconceded to NPTL.)
NPTL:Native Posix Thread Library for Linux(Linux本地Posix线程库)
NPTL是由UlrichDrepper ( glibc的主要维护人员)和IngoMolnar发起的项目,目的是提供world-class的PosixLinux线程支持。
2003.10.5,NPTL作为一个add-on目录(就像linuxthreads一样)被合并到glibc的cvs树中,所以很有可能随glibc的下一次release而一起发布。
RedHat 9是最早的包含NPTL的发行版本(对一些用户来说有点不太方便,但是必须有人来打破这沉默[breakthe ice]...)
NPTLlinks:
-
IngoMolnar最初的基准测试表明可以处理10^6个线程
-
Ulrich的基准测试比较了LinuxThreads,NPTL和IBM的NGPT的各自性能,结果看来NPTL比NGPT快的多。
这是我尝试写的描述NPTL历史的文章(也可以参考JerryCooperstein的文章):
2002.3,NGPT小组的BillAbt,glibc的维护者UlrichDrepper 和其它人召开了个会议来探讨LinuxThreads的发展,会议的一个idea就是要改进mutex的性能。RustyRussell 等人随后实现了 fastuserspace mutexes (futexes),(如今已在NGPT和NPTL中应用了)。与会的大部分人都认为NGPT应该合并到glibc中。
然而UlrichDrepper并不怎么喜欢NGPT,他认为他可以做得更好。(对那些曾经想提供补丁给glibc的人来说,这应该不会令他们感到惊讶:-)于是在接下来的几个月里,UlrichDrepper, Ingo Molnar和其它人致力于glibc和内核的改变,然后就弄出了NativePosix Threads Library (NPTL). NPTL使用了NGPT设计的所有内核改进(kernelenhancement),并且采用了几个最新的改进。IngoMolnar描述了一下的几个内核改进:
NPTL 使用了三个由NGPT引入的内核特征:getpid()返回PID,CLONE_THREAD和futexes;NPTL还使用了(并依赖)也是该项目的一部分的一个更为wider的内核特征集。
一些由NGPT引入内核的items也被修改,清除和扩展,例如线程组的处理(CLONE_THREAD).[the CLONE_THREAD changes which impacted NGPT's compatibility gotsynced with the NGPT folks, to make sure NGPT does not break in anyunacceptable way.]
这些为NPTL开发的并且后来在NPTL中使用的内核特征都描述在设计白皮书中,http://people.redhat.com/drepper/nptl-design.pdf...
Ashort list: TLS support, various clone extensions (CLONE_SETTLS,CLONE_SETTID, CLONE_CLEARTID), POSIX thread-signal handling,sys_exit() extension (release TID futex upon VM-release), thesys_exit_group() system-call, sys_execve() enhancements and supportfor detached threads.
Therewas also work put into extending the PID space - eg. procfs crasheddue to 64K PID assumptions, max_pid, and pid allocation scalabilitywork. Plus a number of performance-only improvements were done aswell.
Inessence the new features are a no-compromises approach to 1:1threading - the kernel now helps in everything where it can improvethreading, and we precisely do the minimally necessary set of contextswitches and kernel calls for every basic threading primitive.
NGPT和NPTL的一个最大的不同就是NPTL是1:1的线程模型,而NGPT是M:N的编程模型(具体请看下面).尽管这样,Ulrich的最初的基准测试还是表明NPTL比NGPT快很多。(NGPT小组期待查看Ulrich的测试程序来核实他的结果.)
FreeBSD线程支持
FreeBSD支持LinuxThreads和用户空间的线程库。同样,M:N的模型实现KSE在FreeBSD5.0中引入。具体请查看www.unobvious.com/bsd/freebsd-threads.html.
2003.3.25,JeffRoberson 发表于freebsd-arch:
... 感谢Julian,David Xu, Mini, Dan Eischen,和其它的每一位参加了KSE和libpthread开发的成员所提供的基础,Mini和我已经开发出了一个1:1模型的线程实现,它可以和KSE并行工作而不会带来任何影响。Itactually helps bring M:N threading closer by testing out shared bits....
And2006.7, RobertWatson提议1:1的线程模型应该为FreeBSD7.x的默认实现:
我知道曾经讨论过这个问题,但是我认为随着7.x的向前推进,这个问题应该重新考虑。在很多普通的应用程序和特定的基准测试中,libthr明显的比libpthread在性能上要好得多。libthr是在我们大量的平台上实现的,而libpthread却只有在几个平台上。最主要的是因为我们使得Mysql和其它的大量线程的使用者转换到"libthr",whichis suggestive, also! ... 所以strawman提议:让libthr成为7.x上的默认线程库。
NetBSD线程支持
根据NoriyukiSoda的描述:
内核支持M:N线程库是基于调度程序激活模型,合并于2003.1.18当时的NetBSD版本中。
详情请看NathanJ. Williams, Wasabi Systems, Inc.在2002年的FREENIX上的演示AnImplementation of Scheduler Activations on the NetBSD OperatingSystem。
Solaris线程支持
Solaris的线程支持还在进一步提高evolving...从Solaris2到Solaris8,默认的线程库使用的都是M:N模型,但是Solaris9却默认使用了1:1线程模型.查看Sun多线程编程指南和Sun的关于Java和Solaris线程的note.
Java在JDK1.3.x及更早的线程支持
大家都知道,Java一直到JDK1.3.x都没有支持任何处理网络连接的方法,除了一个线程服务一个客户端的模型之外。Volanomark是一个不错的微型测试程序,可以用来测量在某个时候不同数目的网络连接时每秒钟的信息吞吐量。在2003.5,JDK 1.3的实现实际上可以同时处理10000个连接,但是性能却严重下降了。从Table4 可以看出JVMs可以处理10000个连接,但是随着连接数目的增长性能也逐步下降。
Note:1:1 threading vs. M:N threading
在实现线程库的时候有一个选择就是你可以把所有的线程支持都放到内核中(也就是所谓的1:1的模型),也可以把一些线程移到用户空间上去(也就是所谓的M:N模型)。从某个角度来说,M:N被认为拥有更好的性能,但是由于很难被正确的编写,所以大部分人都远离了该方法。
Novell和Microsoft都宣称已经在不同时期完成了该工作,至少NFS的实现完成了该工作。khttpd在Linux下为静态web页面完成了该工作,IngoMolnar完成了"TUX"(Threaded linUX webserver),这是一个Linux下的快速的可扩展的内核空间的HTTP服务器。Ingo在2000.9.1宣布alpha版本的TUX可以在ftp://ftp.redhat.com/pub/redhat/tux下载,并且介绍了如何加入其邮件列表来获取更多信息。
在Linux内核的邮件列表上讨论了该方法的好处和缺点,多数人认为不应该把web服务器放进内核中,相反内核加入最小的钩子hooks来提高web服务器的性能,这样对其它形式的服务器就有益。具体请看 ZachBrown的讨论对比用户级别和内核的http服务器。在2.4的linux内核中为用户程序提供了足够的权力(power),就像X15服务器运行的速度和TUX几乎一样,但是它没有对内核做任何改变。
Comments
在2001,Tim Brecht和MMichalOstrowski为使用简单的select的服务器做了各种策略的测度测试的数据值得看一看。
在2003,Tim Brecht发表了userver的源码,该服务器是整合了AbhishekChandra, David Mosberger, David Pariag和MichalOstrowski所写的几个服务器而成的,可以使用select(),poll(), epoll()和sigio.
回到1999.3,DeanGaudet发表:
我一直在问“为什么你们不使用基于select/event的模型,它明显是最快的。”...
他们的理由是“太难理解了,并且其中关键部分(payoff)不清晰”,但是几个月后,当该模型变得易懂时人们就开始愿意使用它了。
MarkRussinovich写了一篇评论和文章讨论了在2.2的linux内核只能够I/O策略问题。尽管某些地方似乎有点错误,不过还是值得去看。特别是他认为Linux2.2的异步I/O(请看上面的F_SETSIG)并没有在数据准备好时通知用户进程,而只有在新的连接到达时才有。这看起来是一个奇怪的误解。还可以看看早期的一些comments,IngoMolnar在1999.4.30所举的反例,Russinovich在1999.5.2的comments,Alan Cox的 反例,和各种linux内核邮件.我怀疑他想说的是Linux不支持异步磁盘I/O,这在过去是正确的,但是现在SGI已经实现了KAIO,它已不再正确了。
查看页面sysinternals.com和MSDN了解一下“完成端口”,据说它是NT中独特的技术,简单说,win32的"重叠I/O"被认为是太低水平而不方面使用,“完成端口”是提供了完成事件队列的封装,再加上魔法般的调度,通过允许更多的线程来获得完成事件如果该端口上的其它已获得完成事件的线程处于睡眠中时(可能正在处理阻塞I/O),从而可以保持运行线程数目恒定(schedulingmagic that tries to keep the number of running threads constant byallowing more threads to pick up completion events if other threadsthat had picked up completion events from this port are sleeping(perhaps doing blocking I/O).