??xml version="1.0" encoding="utf-8" standalone="yes"?>
[版权声明]Q此文档遵@GNU自由文档许可?GNU Free Documentation License).M人可以自由复?分发,修改,不过如果方便,h明出处和作?)
(1)DQ?/font>
首先Q我强烈大家阅读Richard Stevens著作《TCP/IP Illustracted Volume 1,2,3》和《UNIX Network Programming Volume 1,2》。虽然他d我们大家已经5q多了,但是他的书依然是q入|络~程的最直接的道路。其中的3L《TCP/IP Illustracted》卷1是必读-如果你不了解tcp协议各个选项的详l定义,你就失去了优化程序重要的一个手Dc卷2,3可以选读一下。比如卷2讲解的是4.4BSD内核TCP/IP协议栈实?---q个版本的协议栈几乎影响了现在所有的LosQ但是因为年代久q,内容不一定那么vogue.在这里我多推荐一本《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,?.4内核讲解Linux TCP/IP实现Q相当不?作ؓ一个现实世界中的实玎ͼ很多时候你必须作很多权衡,q时候参考一个久l考验的系l更有实际意义。D个例?linux内核中sk_buffl构Zq求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多?sk_buff最也?72的字?
其实对于socket应用层程序来_《UNIX Network Programming Volume 1》意义更大一?2003q的时候,q本书出了最新的W?版本Q不q主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevensl出了网lIO的基本模型。在q里最重要的莫q于select模型和Asynchronous I/O模型.从理Z_AIOg是最高效的,你的IO操作可以立即q回Q然后等待os告诉你IO操作完成。但是一直以来,如何实现没有一个完的Ҏ。最著名的windows完成端口实现的AIO,实际上也是内部用U程池实现的|了Q最后的l果是IO有个U程池,你应用也需要一个线E池......很多文档其实已经指出了这带来的线Econtext-switch带来的代仗?/font>
在linuxq_上,关于|络AIO一直是改动最多的地方Q?.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布Q网l模块的AIO一直没有进入稳定内核版?大部分都是用用LE模拟方法,在用了NPTL的linux上面其实和windows的完成端口基本上差不多了)?.6内核所支持的AIOҎ盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支?是l过VFSpȝbuffer直接写硬盘,对于服务器在内存^Ex上有相当帮??/font>
所以,剩下的select模型基本上就是我们在linux上面的唯一选择Q其实,如果加上no-block socket的配|,可以完成一??AIO的实玎ͼ只不q推动力在于你而不是os而已。不q传l的select/poll函数有着一些无法忍受的~点Q所以改q一直是2.4-2.5开发版本内核的dQ包?dev/pollQrealtime signal{等。最l,Davide Libenzi开发的epollq入2.6内核成ؓ正式的解x?/font>
(2)epoll的优?/font>
<1>支持一个进E打开大数目的socket描述W?FD)
select最不能忍受的是一个进E所打开的FD是有一定限制的Q由FD_SETSIZE讄Q默认值是2048。对于那些需要支持的上万q接数目的IM服务器来说显然太了 。这时候你一是可以选择修改q个宏然后重新编译内核,不过资料也同时指样会带来|络效率的下降,二是可以选择多进E的解决Ҏ(传统的ApacheҎ)Q不q虽然linux上面创徏q程的代h较小Q但仍旧是不可忽视的Q加上进E间数据同步q比不上U程间同步的高效Q所以也不是一U完的Ҏ。不qepoll则没有这个限Ӟ它所支持的FD上限是最大可以打开文g的数目,q个数字一般远大于2048,举个例子,?GB内存的机器上大约?0万左叻I具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和pȝ内存关系很大?/font>
<2>IO效率不随FD数目增加而线性下?/font>
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合Q不q由于网lgӞM旉只有部分的socket?z跃"的,但是select/poll每次调用都会U性扫描全部的集合Q导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"z跃"的socketq行操作---q是因ؓ在内核实Cepoll是根据每个fd上面的callback函数实现的。那么,只有"z跃"的socket才会d的去调用callback函数Q其他idle状态socket则不会,在这点上Qepoll实现了一??AIOQ因时候推动力在os内核。在一些benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境Qepollq不比select/poll有什么效率,相反Q如果过多用epoll_ctl,效率相比q有E微的下降。但是一旦用idle connections模拟WAN环境,epoll的效率就q在select/poll之上了?/font>
<3>使用mmap加速内怸用户I间的消息传递?/font>
q点实际上涉及到epoll的具体实C。无论是select,pollq是epoll都需要内核把FD消息通知l用L_如何避免不必要的内存拯很重要Q在q点上,epoll是通过内核于用L间mmap同一块内存实现的。而如果你x一样从2.5内核关注epoll的话Q一定不会忘记手工mmapq一步的?/font>
<4>内核微调
q一点其实不epoll的优点了Q而是整个linuxq_的优炏V也怽可以怀疑linuxq_Q但是你无法回避linuxq_赋予你微调内核的能力。比如,内核TCP/IP协议栈用内存池理sk_buffl构Q那么可以在q行时期动态调整这个内存pool(skb_head_pool)的大?--通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参?TCP完成3ơ握手的数据包队列长?Q也可以Ҏ你^台内存大动态调整。更甚至在一?/font>数据?/font>面数目巨大但同时每个数据包本w大却很小的特D系l上试最新的NAPI|卡驱动架构?/font>
(3)epoll的?/font>
令h高兴的是Q?.6内核的epoll比其2.5开发版本的/dev/epollz了许多Q所以,大部分情况下Q强大的东西往往是简单的。唯一有点ȝ是epoll?U工作方?LT和ET?/font>
LT(level triggered)是缺省的工作方式Qƈ且同时支持block和no-block socket.在这U做法中Q内核告诉你一个文件描q符是否qA了,然后你可以对q个qA的fdq行IO操作。如果你不作M操作Q内核还是会l箋通知你的Q所以,q种模式~程出错误可能性要一炏V传l的select/poll都是q种模型的代表.
ET(edge-triggered)是高速工作方式,只支持no-block socket。在q种模式下,当描q符从未qA变ؓqAӞ内核通过epoll告诉你。然后它会假设你知道文g描述W已l就l,q且不会再ؓ那个文g描述W发送更多的qA通知Q直C做了某些操作D那个文g描述W不再ؓqA状态了(比如Q你在发送,接收或者接收请求,或者发送接收的数据于一定量时导致了一个EWOULDBLOCK 错误Q。但是请注意Q如果一直不对这个fd作IO操作(从而导致它再次变成未就l?Q内怸会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark认?/font>
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系l调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html Q?br>在http://www.kegel.com/rn/也有一个完整的例子Q大家一看就知道如何使用?/font>
(4)Leader/follower模式U程pool实现Q以及和epoll的配?/font>
.....未完成,主要是要避免q多的epoll_ctl调用,以及试使用EPOLLONESHOT加?.....
(5)benchmark
.......未完?/font>
当在C中定义了一个结构类型时Q它的大是否等于各字段(field)大小之和Q编译器如何在内存中放|这些字D?ANSI C对结构体的内存布局有什么要求?而我们的E序又能否依赖这U布局Q这些问题或许对不少朋友来说q有Ҏp,那么本文p着探究它们背后的秘密?
首先Q至有一点可以肯定,那就是ANSI C保证l构体中各字D在内存中出现的位置是随它们的声明顺序依ơ递增的,q且W一个字D늚首地址{于整个l构体实例的首地址。比如有q样一个结构体Q?br>
struct vector{int x,y,z;} s;
int *p,*q,*r;
struct vector *ps;
p = &s.x;
q = &s.y;
r = &s.z;
ps = &s;
assert(p < q);
assert(p < r);
assert(q < r);
assert((int*)ps == p);
// 上述断言一定不会失?/p>
q时Q有朋友可能会问:"标准是否规定盔R字段在内存中也相?"?唔,对不PANSI C没有做出保证Q你的程序在M时候都不应该依赖这个假设。那q是否意味着我们永远无法勑Zq更清晰更精的l构体内存布局图?哦,当然不是。不q先让我们从q个问题中暂时抽w,x一下另一个重要问题————内存对齐?/p>
许多实际的计机pȝ对基本类型数据在内存中存攄位置有限Ӟ它们会要求这些数据的首地址的值是某个数k(通常它ؓ4?)的倍数Q这是所谓的内存寚wQ而这个k则被UCؓ该数据类型的寚w模数(alignment modulus)。当一U类型S的对齐模C另一U类型T的对齐模数的比值是大于1的整敎ͼ我们qcdS的对齐要求比T?严格)Q而称T比S?宽松)。这U强制的要求一来简化了处理器与内存之间传输pȝ的设计,二来可以提升d数据的速度。比如这么一U处理器Q它每次d内存的时候都从某?倍数的地址开始,一ơ读出或写入8个字节的数据Q假如Y件能保证doublecd的数据都?倍数地址开始,那么L写一个doublecd数据只需要一ơ内存操作。否则,我们可能需要两ơ内存操作才能完成这个动作,因ؓ数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满寚w要求的情况下可能会出错,但是Intel的IA32架构的处理器则不数据是否对齐都能正工作。不qIntel奉劝大家Q如果想提升性能Q那么所有的E序数据都应该尽可能地对齐。Win32q_下的微YC~译?cl.exe for 80x86)在默认情况下采用如下的对齐规? M基本数据cdT的对齐模数就是T的大,即sizeof(T)。比如对于doublecd(8字节)Q就要求该类型数据的地址L8的倍数Q而charcd数据(1字节)则可以从M一个地址开始。Linux下的GCC奉行的是另外一套规?在资料中查得Qƈ未验证,如错误请指正):M2字节大小(包括单字节吗?)的数据类?比如short)的对齐模数是2Q而其它所有超q?字节的数据类?比如long,double)都以4为对齐模数?/p>
现在回到我们兛_的struct上来。ANSI C规定一U结构类型的大小是它所有字D늚大小以及字段之间或字D尾部的填充区大之和。嗯Q填充区Q对Q这是Z使结构体字段满内存寚w要求而额外分配给l构体的I间。那么结构体本n有什么对齐要求吗Q有的,ANSI C标准规定l构体类型的寚w要求不能比它所有字D中要求最严格的那个宽松,可以更严?但此非强制要求,VC7.1׃仅是让它们一样严?。我们来看一个例?以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1Q内存对齐编译选项?默认"Q即不指?Zp?pack选项):
typedef struct ms1
{
char a;
int b;
} MS1;
假设MS1按如下方式内存布局(本文所有示意图中的内存地址从左臛_递增):
_____________________________
| | |
| a | b |
| | |
+---------------------------+
Bytes: 1 4
因ؓMS1中有最强对齐要求的是b字段(int)Q所以根据编译器的对齐规则以及ANSI C标准QMS1对象的首地址一定是4(intcd的对齐模?的倍数。那么上q内存布局中的b字段能满intcd的对齐要求吗Q嗯Q当然不能。如果你是编译器Q你会如何y妙安排来满CPU的癖好呢Q呵呵,l过1毫秒的艰苦思考,你一定得Z如下的方案:
_______________________________________
| |\\\\\\\\\\\| |
| a |\\padding\\| b |
| |\\\\\\\\\\\| |
+-------------------------------------+
Bytes: 1 3 4
q个Ҏ在a与b之间多分配了3个填?padding)字节Q这样当整个struct对象首地址满4字节的对齐要求时Qb字段也一定能满int型的4字节寚w规定。那么sizeof(MS1)昄应该是8Q而b字段相对于结构体首地址的偏Ud?。非常好理解Q对吗?现在我们把MS1中的字段交换一下顺?
typedef struct ms2
{
int a;
char b;
} MS2;
或许你认为MS2比MS1的情况要单,它的布局应该是
_______________________
| | |
| a | b |
| | |
+---------------------+
Bytes: 4 1
因ؓMS2对象同样要满?字节寚w规定Q而此时a的地址与结构体的首地址相等Q所以它一定也?字节寚w。嗯Q分析得有道理,可是却不全面。让我们来考虑一下定义一个MS2cd的数l会出现什么问题。C标准保证QQ何类?包括自定义结构类?的数l所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话_数组各元素之间不会有I隙。按照上面的ҎQ一个MS2数组array的布局是:
|<- array[1] ->|<- array[2] ->|<- array[3] .....
__________________________________________________________
| | | | |
| a | b | a | b |.............
| | | | |
+----------------------------------------------------------
Bytes: 4 1 4 1
当数l首地址?字节寚wӞarray[1].a也是4字节寚wQ可是array[2].a呢?array[3].a ....呢?可见q种Ҏ在定义结构体数组时无法让数组中所有元素的字段都满_齐规定,必须修改成如下Ş?
___________________________________
| | |\\\\\\\\\\\|
| a | b |\\padding\\|
| | |\\\\\\\\\\\|
+---------------------------------+
Bytes: 4 1 3
现在无论是定义一个单独的MS2变量q是MS2数组Q均能保证所有元素的所有字D都满寚w规定。那么sizeof(MS2)仍然?Q而a的偏UMؓ0Qb的偏UL4?/p>
好的Q现在你已经掌握了结构体内存布局的基本准则,试分析一个稍微复杂点的类型吧?/p>
typedef struct ms3
{
char a;
short b;
double c;
} MS3;
我想你一定能得出如下正确的布局?
padding
|
_____v_________________________________
| |\| |\\\\\\\\\| |
| a |\| b |\padding\| c |
| |\| |\\\\\\\\\| |
+-------------------------------------+
Bytes: 1 1 2 4 8
sizeof(short){于2Qb字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double){于8Qc字段要从8倍数地址开始,前面的a、b字段加上填充字节已经? bytesQ所以b后面再填?个字节就可以保证c字段的对齐要求了。sizeof(MS3){于16Qb的偏UL2Qc的偏UL8。接着看看l构体中字段q是l构cd的情?
typedef struct ms4
{
char a;
MS3 b;
} MS4;
MS3中内存要求最严格的字D|cQ那么MS3cd数据的对齐模数就与double的一??)Qa字段后面应填?个字节,因此MS4的布局应该?
_______________________________________
| |\\\\\\\\\\\| |
| a |\\padding\\| b |
| |\\\\\\\\\\\| |
+-------------------------------------+
Bytes: 1 7 16
昄Qsizeof(MS4){于24Qb的偏Uȝ??/p>
在实际开发中Q我们可以通过指定/Zp~译选项来更改编译器的对齐规则。比如指?Zpn(VC7.1中n可以?????6)是告诉~译器最大对齐模数是n。在q种情况下,所有小于等于n字节的基本数据类型的寚w规则与默认的一P但是大于n个字节的数据cd的对齐模数被限制为n。事实上QVC7.1的默认对齐选项q当于/Zp8。仔l看看MSDN对这个选项的描qͼ会发现它郑重告诫了程序员不要在MIPS和Alphaq_上用/Zp1?Zp2选项Q也不要?6位^C指定/Zp4?Zp8(xZ么?)。改变编译器的对齐选项Q对照程序运行结果重新分析上?U结构体的内存布局是一个很好的复习?/p>
Cq里Q我们可以回{本文提出的最后一个问题了。结构体的内存布局依赖于CPU、操作系l、编译器及编译时的对齐选项Q而你的程序可能需要运行在多种q_上,你的源代码可能要被不同的人用不同的编译器~译(试想你ؓ别h提供一个开放源码的?Q那么除非绝对必需Q否则你的程序永q也不要依赖q些诡异的内存布局。顺便说一下,如果一个程序中的两个模块是用不同的寚w选项分别~译的,那么它很可能会生一些非常微妙的错误。如果你的程序确实有很难理解的行为,不防仔细查一下各个模块的~译选项?/p>
思考题:请分析下面几U结构体在你的^C的内存布局Qƈ试着L一U合理安排字D声明顺序的Ҏ以尽量节省内存空间?/p>
A. struct P1 { int a; char b; int c; char d; };
B. struct P2 { int a; char b; char c; int d; };
C. struct P3 { short a[3]; char b[3]; };
D. struct P4 { short a[3]; char *b[3]; };
E. struct P5 { struct P2 *a; char b; struct P1 a[2]; };
参考资?
?】《深入理解计机pȝ(修订?》,
(?Randal E.Bryant; David O'HallaronQ?br> (?龚奕?雯春,
中国电力出版C,2004
?】《C: A Reference Manual?影印?Q?br> (?Samuel P.Harbison; Guy L.SteeleQ?br> 人民邮电出版C,2003