完成端口(IOCP)的另一种设想——Socket与CompletionPort的多次关联

ygluu 2009-11-04 02:12:41
加精

完成端口(IOCP)的另一种设想——Socket与CompletionPort的多次关联
代码客 http://blog.csdn.net/guestcode


先扯一下题外话。我发表过VC和Delphi版IOCP例程。有不少人提出了很好的建议,也有很多人提出了置疑。有自称内核编程的人说临界段同步不要需要进入内核态,用过临界段的人都知道,当竞争发生的时候,“后来”的线程总被挂起,那么线程挂起会进入什么态?
有人提出线程独立的内存资源管理,可以避免的同步操作,这不失为一个好办法,但带来的麻烦是,分配资源的时需要在取得“当前线程对象”上花费时间,其实临界段在竞争不激烈的情况下导致进入内核态的几率很少,临界段玩得的就是概率。
在工作线程和处理线程之间,有人建议最好做法是复制数据给处理线程,当然这个比较安全点,但数据复制也需要不少时间。甚至有人抨击,HandleData(socket连接的对象)和UserData(登录用户的对象)之间的指针关联方式效率底下,认为哈希表方式效率高,可是未必见得哈希表的操作比指针关联方式有优越之处。
内存管理是服务器性能的关键,有人却忽略了“分页内存”会被交换到“页交换文件”中去的可能。对象化(类)编程能够提高开发效率,但也要注意“级数深”的对象成员及虚函数访问的开销。事实上操作完成端口很容易,难的是如何组织管理内存和设计合理的结构。

进入正题。前面提到HandleData和UserData,做过Tcp服务开发的人都知道,每个客户端连接都有一个Socket对象(下称HandleData)与之对应。做过游戏开发的人也肯定知道,在服务器端每个登录的用户有个对象(下称UserData)记录用户信息,UserData和HandleData也是一一对应关系。但两者很难合并为一体(同一个数据结构或类对象),UserData是在用户输入ID和密码Login之后被会创建(或池分配),用户发送数据Logout之后会被摧毁(或回收入池),而在Logout之前,由于网络等诸多原因,会有N次的重新连接的可能性,如果断线不超过规定时间的话UserData是不能摧毁的,那么UserData在其生命期内对应的HandleData就不是唯一的了。一般HandleData和UserData关联的做法是,根据Login的用户ID在目前的在线用户信息列表中查找UserData,找到则关联(未超时和Logout),找不到则创建后再关联,此后根据关联信息无需遍历很快就可以知道来自这个HandleData的数据就是这个UserData的(用户登录频率远比数据收发频率底得多,以最快方式把Socket的数据定位到用户对象上去是高效率的一个关键)。他们关联的方式有:1、使用指针关联的,HandleData有指针指向UserData,UserData有指针指向HandleData);2、使用哈希表,用socket的值作为索引;3、UserData索引,就是登录以后返回UserData的索引,此后客户端发来的每个数据包里面都包含这个的索引,但会增加数据量,一般用在Udp服务器。上面这些是常用的方式,但是否还有其它刚好的方式呢?
了解完成端口的都知道,完成端口句柄和Socket关联的参数有个完成键(CompletionKey),这个CompletionKey一般是HandleData(或是担任这个角色的其它结构),通常我们仅建立一次CompletionPort与Socket的关联,因此某个客户端从连接到断开,CompletionKey一直是HandleData。假如有这么个设想不知道是否行得通,就是当用户发送ID和密码Login之后,用UserData作为CompletionKey再次把Socket和完成端口关联,那么此后的Io投递的返回数据就可以直接对应UserData。本人信息闭塞,尚未不知道有人提过这个方法,也不知道是否行得通,也希望大家互相探讨。具体做法是:

1、Accept/AcceptEx阶段:
CreateIoCompletionPort(Socket, CompletionPort, HandleData, 0);

2、用户发送数据Login,获得认证并建立UserData以后:
CreateIoCompletionPort(Socket, CompletionPort, UserData, 0);

3、Disconnect之后(恢复最初关联):
CreateIoCompletionPort(Socket, CompletionPort, HandleData, 0);

上面三步主要区别于CreateIoCompletionPort函数的第三个参数。假如行得通的话,那么Iocp Server的编程难度相对就增大了一些,UserData 也要具备HandleData的基本功能。在测试中多次调用CreateIoCompletionPort(Socket, CompletionPort, …)进行关联操作的返回值是正确的,但尚未得知的是多次关联对Iocp内部机制有没有影响,以后会有怎样的异常出现。本人也正在进一步编程求证中。







...全文
1169 67 打赏 收藏 转发到动态 举报
写回复
用AI写文章
67 条回复
切换为时间正序
请发表友善的回复…
发表回复
lin_style 2009-11-07
  • 打赏
  • 举报
回复
楼上几位对关键段讨论都是对的。很大几率上就是靠概率,没必要每回都要死死的进入内核状态。这也是很早前,linux2.4的版本在关键段的效率上不如Win的一个很大原因。 经过讨论头脑风暴,学习了。
NewbieCoder 2009-11-07
  • 打赏
  • 举报
回复
[Quote=引用楼主 guestcode 的回复:]

有自称内核编程的人说临界段同步不要需要进入内核态,用过临界段的人都知道,当竞争发生的时候,“后来”的线程总被挂起,那么线程挂起会进入什么态?

[/Quote]

Windows核心编程第五版P212:
当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态,意味着线程必须从用户模式切换到内核模式。
ygluu 2009-11-06
  • 打赏
  • 举报
回复

由于每个申请自旋锁的处理器均在全局变量 slock 上忙等待


这就是明白的关键了。

我总以为可以串行化CPU时钟周期或者令CPU时钟停止直到获得控制权,从而达到等待目的呢。
ygluu 2009-11-06
  • 打赏
  • 举报
回复
[Quote=引用 60 楼 lin_style 的回复:]
所以,lockinter的指令虽然快,但也有不公平的地方

随着处理器个数的不断增加,竞争也在加剧,自然导致更长的等待时间。

    释放自旋锁时的重置操作将无效化所有其它正在忙等待的处理器的缓存,那么在处理器拓扑结构中临近自旋锁拥有者的处理器可能会更快地刷新缓存,因而增大获得自旋锁的机率。 网管联盟www.bitsCN.com

    由于每个申请自旋锁的处理器均在全局变量 slock 上忙等待,系统总线将因为处理器间的缓存同步而导致繁重的流量,从而降低了系统整体的性能。

蓝色字摘自http://www.bitscn.com/linux/kernel/200806/144491_2.html

说明一切还是以人脑的策略优先。。
[/Quote]

看来,使用InterLocked还要考虑很多了。
要看服务器的CPu数量了。

曾经有人极力推荐内核链栈:Singly-Linked list
也就是:InterlockedPopEntrySList
我在2CPU的机子上测试过,比临界端块N多。

我也查了很多资料,但说如何解决竞争等待问题,还是不很明白。
现在似乎明白了。


ygluu 2009-11-06
  • 打赏
  • 举报
回复
[Quote=引用 57 楼 lin_style 的回复:]
说明EnterCriticalSection和interlock都是采取类似的锁总线方式,没有进行内核的切换。因此只取得了效率,而没有更多的操纵。如果是内核对象,你就可以设置更多的比如超时等这样的细节控制。

至于EnterCriticalSection为什么会竞争内核忘记了。。。当时还做了个笔记。
[/Quote]

EnterCriticalSection 发生竞争后跳转到的地方,应该就是处理竞争进入内核态的。


lin_style 2009-11-06
  • 打赏
  • 举报
回复
所以,lockinter的指令虽然快,但也有不公平的地方

随着处理器个数的不断增加,竞争也在加剧,自然导致更长的等待时间。

释放自旋锁时的重置操作将无效化所有其它正在忙等待的处理器的缓存,那么在处理器拓扑结构中临近自旋锁拥有者的处理器可能会更快地刷新缓存,因而增大获得自旋锁的机率。 网管联盟www.bitsCN.com

由于每个申请自旋锁的处理器均在全局变量 slock 上忙等待,系统总线将因为处理器间的缓存同步而导致繁重的流量,从而降低了系统整体的性能。


蓝色字摘自http://www.bitscn.com/linux/kernel/200806/144491_2.html

说明一切还是以人脑的策略优先。。
lin_style 2009-11-06
  • 打赏
  • 举报
回复
[Quote=引用 58 楼 guestcode 的回复:]
“导致所有CPU去invalidate其相关的Cache line”
可以说竞争的时候,后来者的操作是无效的?

那么后来者如何才能再次操作,直到他获得成功为止?

或者说,lock前缀是让所有CPU串行化,不会在同一个时间内操作?

主要是这个问题。
[/Quote]

这个很简单啊。当被锁住的时候(从 P6 处理器开始,如果指令访问的内存区域已经存在于处理器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。 ),线程必然要不断的检测是否符合条件。 而这个繁忙度就取决于你线程的优先级别
ygluu 2009-11-06
  • 打赏
  • 举报
回复
“导致所有CPU去invalidate其相关的Cache line”
可以说竞争的时候,后来者的操作是无效的?

那么后来者如何才能再次操作,直到他获得成功为止?

或者说,lock前缀是让所有CPU串行化,不会在同一个时间内操作?

主要是这个问题。
lin_style 2009-11-06
  • 打赏
  • 举报
回复
说明EnterCriticalSection和interlock都是采取类似的锁总线方式,没有进行内核的切换。因此只取得了效率,而没有更多的操纵。如果是内核对象,你就可以设置更多的比如超时等这样的细节控制。

至于EnterCriticalSection为什么会竞争内核忘记了。。。当时还做了个笔记。
ygluu 2009-11-06
  • 打赏
  • 举报
回复
感谢LS

lin_style 2009-11-06
  • 打赏
  • 举报
回复
lock解释:这段都采自看雪

http://bbs.pediy.com/showthread.php?t=84326

lin_style 2009-11-06
  • 打赏
  • 举报
回复
InterlockedIncrement

汇编:
7C809776   mov         ecx,dword ptr [esp+4]
7C80977A mov eax,1
7C80977F lock xadd dword ptr [ecx],eax
7C809783 inc eax
7C809784 ret 4



EnterCriticalSection();
7C921005  mov         ecx,dword ptr fs:[18h] 
7C92100C mov edx,dword ptr [esp+4]
7C921010 cmp dword ptr [edx+14h],0
7C921014 jne 7C921065
7C921016 lock inc dword ptr [edx+4]
7C92101A jne 7C921035
7C92101C mov eax,dword ptr [ecx+24h]
7C92101F mov dword ptr [edx+0Ch],eax
7C921022 mov dword ptr [edx+8],1
7C921029 xor eax,eax
7C92102B ret 4


LeaveCriticalSection();
7C9210ED  mov         edx,dword ptr [esp+4] 
7C9210F1 xor eax,eax
7C9210F3 dec dword ptr [edx+8]
7C9210F6 jne 7C92111E
7C9210F8 mov dword ptr [edx+0Ch],eax
7C9210FB lock dec dword ptr [edx+4]
7C9210FF jge 7C921104
7C921101 ret 4


2者用的方式一样,VC08 debug无优化模式

lock解释:
摘:
LOCK总线封锁信号,三态输出,低电平有效。LOCK有效时表示CPU不允许其它总线主控者占用总线。这个信号由软件设置,当前指令前加上LOCK前缀时,则在执行这条指令期间LOCK保持有效,阻止其它主控者使用总线。


从 P6 处理器开始,如果指令访问的内存区域已经存在于处理器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。

4.2) IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
电平,从而锁住总线,直到该指令执行完毕再放开。 而总线加锁会自动invalidate所有CPU对 _该指令涉及的内存_
的Cache,因此barrier就能保证所有CPU的Cache一致性。

4.3) 接着解释。
lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
所有CPU去invalidate其相关的Cache line。


http://www.unixresources.net/linux/clf/linuxK/archive/00/00/65/37/653778.html
http://www.bitscn.com/linux/kernel/200806/144491_2.html


蓝色字取自看雪。看懂看不懂就看自己RP了。
jerry_zhang99 2009-11-06
  • 打赏
  • 举报
回复
学习,学习
ygluu 2009-11-06
  • 打赏
  • 举报
回复
没有不敢乱说!

vincent_1011 2009-11-06
  • 打赏
  • 举报
回复
[Quote=引用 50 楼 guestcode 的回复:]
你可以一次按顺序投递多个发送请求。

如果要确保对方收到,那么要有应答机制,收到应答包后再从队列删除这个包,如果超时不应答,则重发。
如果数据包先后是关联性的,则数据包需要序号排列,接收方需要按序号排列包...
[/Quote]

OK,谢谢,你通信写过商业级代码了?
ygluu 2009-11-06
  • 打赏
  • 举报
回复
你可以一次按顺序投递多个发送请求。

如果要确保对方收到,那么要有应答机制,收到应答包后再从队列删除这个包,如果超时不应答,则重发。
如果数据包先后是关联性的,则数据包需要序号排列,接收方需要按序号排列包...
vincent_1011 2009-11-06
  • 打赏
  • 举报
回复
[Quote=引用 48 楼 guestcode 的回复:]
如果发现了一个包头和合法的包尾,那么包头之前还有数据且不合法的,丢弃
[/Quote]

恩,搞清楚了。那另外有一个写操作的问题。就是wsasend,有可能wsasend不完整。

这情况难道只能建立一个队列,把要send的数据丢进这队列,然后在确定前一包全部发送成功,再出队下个包发送?这样效率会不会低了?但如果不是这样,还能怎样?
这个就和上面问题对应,解决这个,上一个就不会出现。

ygluu 2009-11-06
  • 打赏
  • 举报
回复
如果发现了一个包头和合法的包尾,那么包头之前还有数据且不合法的,丢弃
ygluu 2009-11-06
  • 打赏
  • 举报
回复
如果包尾永远不合法,
超过极限长度的则丢弃。
ygluu 2009-11-06
  • 打赏
  • 举报
回复
这个情况一般是发送问题,还有多线程处理接收的顺序问题。

原则上不会出现在发送,多出现先在接收,所以处理接收的时候,一般需要排序处理投递的缓存,先投递的先处理...
加载更多回复(47)

18,357

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 网络编程
c++c语言开发语言 技术论坛(原bbs)
社区管理员
  • 网络编程
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧