博文

TCP/IP数据报格式(2009-06-15 18:38:00)

摘要:IP数据报格式 图1.IP数据报格式 版本字段长度为4,用来表明建立数据报的IP版本,目前的IP版本是IPv4,IPv6正在发展中。IPv4的字段为0100 。 首部长度(报头长度)指的是首部占32 bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节。15x32/8=60字节.IP首部始终是32 bit的整数倍.IP数据报报头的最小长度为20个字(不含填充字段和IP选项字段的IP报头是最常见的IP报头,为20个字节)
    服务类型TOS(Type Of Service)总长度字段是指整个I P数据报的长度,以字节为单位.由于该字段长16比特,所以I P数据报最长可达6 5 5 3 5字节(64KB).总长度字段是I P首部中必要的内容。数据长度=总长-报头长度。
    标识符长16比特。 标志位长度为3比特,用于分段控制:第0位为预留位,第1位表示可否分段。当该位的值为0时,表示数据报不可分段,值为1时,表示数据报可被分段。第2位为段是否结束位,当该位的值为0时,表示该段是原数据报的最后一段,值为1时,表示后面不有更多的分段。
  当网络设备要发送的数据报长度比所在网络的最大传输单元(MTU,Max Transfer United)大,并且标志位的第1位设置为不能分段(0)时,网络设备会向发送方返回一个因特网控制消息协议ICMP错误消息,并丢弃该数据报。除了最后一个分段外,其余分段的第2位均设置为1。 段偏移13比特长度,用于指定分段在原始数据报中的位置,以8个字节为单位. 生存时间TTL长度为8比特,用于指定数据报允许保留在网络上的时间。 协议字段长度为8比特,用于指定数据报数据区中携带的消息是由哪种高级协议建立的。ICMP为1,TCP为6,UDP为17。 协议号分配RFC790. 报头校验和16比特,仅用于IP报头校验和。 源IP地址及目的IP地址。 选项,填充字段用于确保将选项字段填充为最少32个比特位,以保证IP报头以32位结束。 分段:分段是将一个大的IP数据报分解成几个较小的数据报段的过程。当IP模块需要通过一个具有较小MTU的网络传送较大的数据包时,就必须将其分段。 //定义ip报头数据结构 typedef s......

阅读全文(11440) | 评论:0

基于完成端口的Winsock程序设计(2009-06-15 12:53:00)

摘要:关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。 完成端口对象取代了WSAAsyncSelect中的消息驱动和WSAEventSelect中的事件对象,当然完成端口模型的内部机制要比WSAAsyncSelect和WSAEventSelect模型复杂得多。 IOCP内部机制如下图所示: 在Winsock中编写完成端口程序,首先要调用CreateIoCompletionPort函数创建完成端口。其原型如下: WINBASEAPI HANDLE WINAPI CreateIoCompletionPort(        HANDLE FileHandle,        HANDLE ExistingCompletionPort,        DWORD CompletionKey,        DWORD NumberOfConcurrentThreads ); 第一次调用此函数创建一个完成端口时,通常只关注NumberOfConcurrentThreads,它定义了在完成端口上同时允许执行的线程数量。一般设为0,表示系统内安装了多少个处理器,便允许同时运行多少个线程为完成端口提供服务。每个处理器各自负责一个线程的运行,避免了过于频繁的线程上下文切换。 hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0) 这个类比重叠I/O事件通知模型中(WSA)CreateEvent。  然后再调用GetSystemInfo(&SystemInfo);取得系统安装的处理器的个数SystemInfo.dwNumberOfProcessors,根据CPU数创建线程池,在完成端口上,为已完成的I/O请求提供服务。一般线程池的规模,即线程数 = CPU数 * 2 + 2。 下面的代码片段演示了线程池的创建。 // 创建线程池,规模为CPU数的两倍   for(int i = 0......

阅读全文(6470) | 评论:8

深度探索I/O完成端口(2009-06-15 09:00:00)

摘要: 引言 要想编写一个高性能的服务器应用程序,必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求,都可能导致性能问题。例如,如果一个服务器创建单个线程来处理所有的请求,那么客户端可能长期等待而得不到响应,因为服务器同一时刻只能忙于处理一个请求。当然单个线程也能并发处理多个请求,当I/O操作被启动时,它可以从一个请求切换到另一个请求,但是这种结构相当复杂,并且不能充分利用多处理器的优势。在另一个极端,服务器可以创建一个大规模的线程池,这样几乎每一个客户请求都可以由一个专门的线程来处理。这种情形通常会导致线程频繁切换:大量线程被唤醒,执行CPU处理,阻塞等待I/O,然后在请求完成之后又一次阻塞以等待新的请求。如果没有别的情况,太多的线程将导致过多的上下文切换,因为调度程序不得不将处理器时间在多个活动线程之间分割。 服务器的目标是使线程避免不必要的阻塞,尽量减少上下文切换。同时,还要使用多线程来发挥最大限度的并行。理想的情况是在每一个处理器上运行一个线程来处理一个客户请求,当处理器上的活动线程完成一个请求时,如果还有其他的请求正在等待,则不阻塞。为了使这一优化处理可以有效的进行,应用程序必须有一种可行的方法,使得一个正在处理客户请求的线程在I/O上阻塞时(例如它在处理过程中需要读取一个文件时)另外一个等待线程被激活。 Windows NT 3.5引进了一系列API使得这个目标的实现变得相对容易。这些API主要聚焦在一个叫完成端口的对象上。在本文中,首先我将讲解完成端口的使用,然后再深入其内部,向你展示Windows NT中完成端口的实现机制。   使用I/O完成端口 应用程序将IoCompletion执行体对象当作与多个文件句柄相关的I/O完成的核心。一旦一个文件与一个完成端口相关联,任何在此文件上异步I/O操作的完成都会导致一个完成通知包(completion notification packet)加入到完成端口队列。一个线程只需简单的等待一个完成通知包被排队到此完成端口上,就可以等待在多个文件上的所有正在进行之中的I/O操作的完成事件。Windows API中的WaitForMultipleObjects 提供了类似的功能,但完成端口的优点在于在系统的协助下发挥高效的并发性。这里的并发性可以理解为应用程序主动处理客户请求的线......

阅读全文(6877) | 评论:0

重叠I/O模型(2)(2009-06-15 08:13:00)

摘要:二.获取重叠I/O操作完成结果 当异步I/O请求挂起后,最终要知道I/O操作是否完成。一个重叠I/O请求最终完成后,应用程序要负责取回重叠I/O操作的结果。对于读,直到I/O完成,接收缓冲器才有效(参考IRP缓冲区管理)。对于写,要知道写是否成功,有几种方法可以做到这点,最直接的方法是调用(WSA)GetOverlappedResult,其函数原型如下。 WINBASEAPI BOOL WINAPI GetOverlappedResult( HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumberOfBytesTransferred, BOOL bWait); WINSOCK_API_LINKAGE BOOL WSAAPI WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped,                                                     LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags); l 参数一为的文件/套接字句柄。 l 参数二为参数一关联的(WSA)OVERLAPPED结构,在调用CreateFile、WSASocket或AcceptEx时指定。 l 参数三指向字节计数指针,负责接收一次重叠发送或接收操作实际传输的字节数。 l   参数四是确定命令是否等待的标志。Wait参数用于决定函数是否应该等待一次重叠操作完成。若将Wait设为TRUE,那么直到操作完成函数才返回;若设......

阅读全文(7761) | 评论:1

重叠I/O模型(1)(2009-06-15 08:12:00)

摘要:一.              重叠I/O的概念及使用 当调用ReadFile和WriteFile时,如果最后一个参数lpOverlapped设置为NULL,那么线程就阻塞在这里,直到读写完指定的数据后,它们才返回。这样在读写大文件的时候,很多时间都浪费在等待ReadFile和WriteFile的返回上面。如果ReadFile和WriteFile是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。 为了解决这个问题,windows引进了重叠I/O的概念,它能够同时以多个线程处理多个I/O。其实你自己开多个线程也可以处理多个I/O,但是系统内部对I/O的处理在性能上有很大的优化。它是Windows下实现异步I/O最常用的方式。 Windows为几乎全部类型的文件提供这个工具:磁盘文件、通信端口、命名管道和套接字。通常,使用ReadFile和WriteFile就可以很好地执行重叠I/O。 重叠模型的核心是一个重叠数据结构。若想以重叠方式使用文件,必须用 FILE_FLAG_OVERLAPPED 标志打开它,例如: HANDLE hFile = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);   如果没有规定该标志,则针对这个文件(句柄),重叠I/O是不可用的。如果设置了该标志,当调用ReadFile和WriteFile操作这个文件(句柄)时,必须为最后一个参数提供OVERLAPPED结构: // WINBASE.H typedef struct _OVERLAPPED{         DWORD   Internal;         DWORD   InternalHigh; &nbs......

阅读全文(8947) | 评论:0

套接字的两种模式(2009-06-13 17:27:00)

摘要:   1.同步I/O和异步I/O 当CPU执行代码(当前活动线程)时遇上一个I/O请求(例如调用ReadFile/WriteFile或recv/send)时,系统产生一个中断,当前活动线程阻塞在此,让CPU去完成这个I/O请求,等到完成后,系统再次产生一个中断让原先的程序继续运行。也就说通过中断保持这两者间的同步。可以将终端理解为硬件化的信号量。 这就是所谓的同步I/O,一个线程中只可能同时处理一个I/O请求。因为一个I/O操作是非常耗时的,所以代码挂起后等待I/O完成的这段时间内,这个线程浪费了很多个指令周期。如果要同时反复读写大文件,则同步I/O的效率是很低的。 当然,可以考虑使用多线程来处理。例如在设计服务器时可以使用多线程来处理客户请求,每有一个客户连接请求,就创建一个新线程,专门处理它的通信请求。对于小型服务器来说,这不是问题。对于同时处理成千上万个请求的大型服务器而言,使用多线程是无效的,因为系统能够支持的线程数量毕竟是有限制的。另外一种解决方案就是使用共享负载的线程池,这涉及到异步I/O。 在异步I/O中,当CPU执行你的代码遇上一个I/O请求时,使用一个线程去处理I/O请求,并且当前调用线程并不挂起。如果后续代码和这个I/O有关(例如需要等待I/O操作的结果),那么它就要等到这个I/O操作完成。通常在一个线程中调用WaitForSingleObject/WaitForMultipleObjects、WSAWaitForMultipleEvents、(WSA)GetOverlappedResult等函数就可以得到I/O完成的消息,然后再对数据进行处理。但如果后续的代码和这个I/O操作无关,你就可以以更快的速度执行下去了,而无需等待I/O请求的完成了。这就是异步I/O了。  据此,套接字有锁定和非锁定两种模式。 2.套接字的两种模式    (1)锁定模式套接字 对于锁定模式的套接字而言,调用任何一个Winsock API函数(accept/recv/send)都会耗费或长或短的时间“等待”返回。大多数Winsock应用都是遵照一种“生产者-消费者”模型来编制。在这种模型中,应用程序需要读取(或写入)指定数量的字节,然后再对读取的数据执行一些计算。 在应用程序中,可以为......

阅读全文(3249) | 评论:0

线程的调度方案(2009-06-13 15:37:00)

摘要:时间片(quantum) 前面已经提到,“时间片”就是在Windows检查是否有另外一个同优先级的线程要执行前,线程运行的时间。如果一个线程结束了它的时间片,而又没有另外一个同优先级的线程,Windows重新调度这个线程运行另外一个时间片。每个线程都有一个时间片的数值,它代表了线程可以运行多长时间,直到时间片届满。这个数值不是一个时间的计时长度,而是一个整数,我们称之为“时间片单位(quantum units)”。 缺省的情况下,Windows 2000/XP Professional线程运行2个时钟周期(6个时间片单位),Windows Server版线程运行12个时钟周期(36个时间片单位)。每次时钟中断,时钟中断程序从线程时间片中扣除一个固定的值3(1个时钟周期)。 Windows Server版设置了较长的缺省值是为了减少上下文转换。当有客户请求到来时,服务器应用程序被唤醒后,如果有较长的时间片,它就可以更好的完成这个请求,然后在时间片结束前返回等待状态。 如果线程时间片没有剩余,时间片结束处理进程就会被触发,然后另外一个线程可能被选择执行。当时钟中断发生时,如果系统正处于DPC/Dispatch级别或者更高,例如正在执行一个DPC或者一个中断服务程序,在这种情况下,就算当前的线程在整个时钟中断间隔都没有运行,它的时间片仍然会被减少。如果不是这样做,而设备中断或者DPC又总是刚好发生在时钟间隔中断前,线程的时间片可能因此总不减少。 不同的硬件平台,时钟间隔的长度是不同的。时钟中断的频率是由HAL(硬件层)负责,而不是内核负责。例如,大部分x86单处理器的时钟间隔是10毫秒,大部分x86多处理器的时钟间隔是15毫秒。 每个时钟滴答表示3个时间片单位,而不是一个时间片单位,这种表示方法是为了线程在等待完成的时候,其时间片可以被减少一部分。基本优先级少于14的线程执行了等待的函数,如WaitForSingleObject 或者 WaitForMultipleObjects后,它的时间片就减少1个单位;优先级是14或更高的线程等待后,其时间片会被重新设置。 线程的时间片可以部分被减少的原因是,当线程在时钟间隔计数器激发前进入等待状态,如果没有对其时间片进行调整,那么有可能这个线程的时间片就从不减少。例如:一个线程运行,然后进入等待状态,然后再......

阅读全文(4288) | 评论:0

线程调度的优先级(2009-06-13 14:23:00)

摘要: 概述 Windows 实现了一个由优先级驱动,抢占式的调度系统,也就是最高优先级的可运行的(就绪状态下的)线程总是先运行。 有一种现象称之为“处理器亲合(processor affinity)”,即线程可能受处理器限制,只运行在那些允许它运行的处理器上。缺省的设置是线程可运行在任何可用的处理器上,用户可以通过使用一个Win32的调度函数修改处理器的“亲合性”。 当一个线程被选择运行,它所运行的时间称之为“时间片”。Windows中断这个线程,去查找是否有别的同优先级或更高优先级的线程正在等待执行,或者这个线程的优先级需要被降低,在这之前这个线程运行的时间长度就是一个“时间片”。不同的线程,其时间片的值可以不同,Window 2000专业版和服务器版的时间片的值是不同的。然而,因为Windows实现的是一种抢占式的调度,一个线程可能未完成其时间片。如果另外有一个更高优先级的线程就绪,正在运行的这个线程就可能在未完成其时间片前被抢占。事实上,一个线程甚至会在未开始其时间片前就被抢占了,而要等待下一次被选择运行。 Windows的没有单独的调度模块或程序,调度的代码是在内核中实现的,广泛分布在内核中那些与调度相关的事件发生的地方。这些负责调度的程序被总称为“内核的调度器”。线程调度发生在DPC/Dispatch级别。 以下这些事件发生时会触发线程调度: 变成就绪状态的线程。例如:一个新创建的线程,或者从等待状态释放出来的线程。 因其时间片结束而离开运行状态的线程,它或者结束了,或者进入等待状态。 线程的优先级改变了,是因为系统调用,或者是Windows自己改变了优先级。 正在运行的线程的处理器亲合性改变了。 在每一个上述情况的衔接点,Windows必须决定下一个运行的线程是哪一个。一旦选择了一个新的线程运行,Windows将对其执行一个上下文转换的操作,即保存正在运行的线程的相关的机器状态,装载另一个线程的状态,开始新线程的执行。 Windows的调度是以线程为粒度调度的。调度的决策被严格限制在以线程为基础,并不考虑这个线程属于哪一个进程。当考虑到进程并不运行,而仅为其线程提供资源和运行的上下文环境时,这种方法就有意义了,例如,进程A有10个可运行的线程,进程B有2个可运行的线程,而且这12个线程的优先级别相同,那么,每一......

阅读全文(5630) | 评论:0

线程的数据结构(2009-06-13 14:08:00)

摘要:除非显式地声明,否则,你可以假设以下讨论的内容既适合用户模式的线程,也适用于内核模式的线程。 在系统级别上,Windows线程是由一个线程块执行体(ETHREAD)来表示的,如图6.7所示。ETHREAD块和它所指向的结构都位于系统地址空间中,唯一的例外是线程环境快(TEB),它位于进程地址空间中。而且,Windows子系统进程(Csrss)为Windows进程中创建的每个线程维护了一个平行结构。另外,对于那些调用了任何一个Windows子系统USER或GDI函数的线程,Windows子系统内核模式部分(Win32k.sys)为它维护了一个称为W32THREAD的数据结构,线程的ETHREAD块指向此结构。 图6.7中的大多数字段的意义已经很明了,不用多说了。第一个字段是内核线程块(KTHREAD)。紧随其后的是线程标识信息、进程标识信息(包括一个指向所有进程的指针,因而可以访问它的环境信息)、安全信息(包括一个指向访问令牌的指针以及身份模拟信息),最后是与LPC消息和待处理I/O请求有关的字段。关于ETHREAD块的信息,你可以使用内核调试器的lkd> dt nt!_ethread(对应进程的是_eprocess)命令来显示其数据结构。 现在让我们来看一看两个关键的线程数据结构:KTHREAD和TEB。KTHREAD块包含了Windows内核为这些正在运行的线程执行线程调度和同步而需要访问的信息。它的布局结构如图6.8所示。 关于KTHREAD块的信息,你可以使用内核调试器的lkd> dt nt!_kthread(对应进程的是_kprocess)命令来显示其数据结构。 图6.9所示的TEB是本节中介绍的唯一位于进程地址空间中的数据结构。 TEB存储了有关映像加载器和各种Windows DLL的环境信息。因为这些组件运行在用户模式下,所以它们需要一个在用户模式下可写的数据结构。为什么该结构位于进程地址空间中而不是位于系统地址空间中呢?因为系统空间只能在内核模式下才可写。通过内核调试器的lkd> dt nt!thread命令你可以看到TEB的地址,同样,你也可以用lkd> dt nt!_teb(对应进程的是_peb)命令来查看TEB的数据结构。 线程的上下文 线程的上下文本质上是一组处理器的寄存器,有正在执行程序中的指针......

阅读全文(4645) | 评论:0

I/O管理器及IRP缓冲区管理(2009-06-13 13:11:00)

摘要:I/O管理器     I/O 管理器定义了有序的结构,或者说是模型,在这个模型里将I/O请求发送给设备驱动程序。I/O系统是包驱动的,大部分I/O请求用I/O请求包(IRP)代表,从一个I/O系统组件传送到另一个组件。(注意:快速I/O不是IRP。)这种设计允许一个应用程序线程可以同时管理多个I/O请求。一个IRP是一个数据结构,包含了完整地描述一个I/O请求的信息。(可以通过Windbg内核调试扩展命令lkd> dt nt!_irp来查看IRP数据结构) I/O管理器创建一个IRP代表一个I/O操作,将指向IRP的指针传递给正确的驱动程序,并且当I/O操作完成时删除IRP。相反,驱动程序收到一个IRP,执行IRP指定的操作,不管是完成操作还是要将IRP传递给另外一个驱动程序进行更进一步的处理,驱动程序都将IRP传回给I/O管理器。 除了创建和删除IRP,I/O管理器还为不同的驱动程序提供公共的代码,驱动程序可以调用这些公共代码进行I/O处理。将I/O管理器中公共的任务合并,可以使驱动程序变得更简单轻便。例如,I/O管理器提供一个函数,允许驱动程序调用其它驱动程序。I/O管理器还管理为I/O请求而设立的缓冲区,为驱动程序提供超时支持,记录操作系统装载了哪个可安装的文件系统。I/O管理器中有将近100个不同的例程可以被设备驱动程序调用。 I/O管理器还提供了灵活的I/O服务,允许环境子系统,如Win32和POSIX,实现它们各自的I/O函数。这些服务包括了用于异步I/O的复杂的服务,用于开发者构造可升级的、高性能的服务器应用程序。 驱动程序提供统一的、模块化的接口,允许I/O管理器可以调用任何驱动程序,而不需要知道驱动程序内部的结构或者是细节的任何信息。前面已经提到,操作系统将所有的I/O请求当作向一个文件发出的请求,驱动程序将发送给虚拟文件的请求转换成硬件特有的请求。驱动程序通过I/O管理器可以互相调用,从而完成对一个I/O请求分层的、各自独立的处理。 除了通常的打开、关闭、读和写函数,Windows 2000 I/O 系统还提供了几个高级特性,如异步,直接,缓冲和分散/集中 I/O。 I/O管理器还负责管理I/O请求缓冲区。 IRP缓冲区管理 当应用程序或者是设备驱动程序通过使用 NtReadFile......

阅读全文(5021) | 评论:0