2007年03月18日

 

这一章讲述Windows2000的内核模式驱动程序的网络结构,这章包含以下几个部分:

 

l         2.1Windows2000网络结构和OSI模型

l         2.2NDIS网络驱动程序

l         2.3TDI驱动程序

l         2.4、网络驱动程序环境

 

2.1Windows2000网络结构和OSI模型

 

Windows2000网络结构是以国际标准化组织(ISO)制定的七层网络模型为基础的,1978年,ISO制定的开发式系统(OSI)参考模型,将网络描述为一系列的协议层,在每个协议层中完成一系列的特定功能。每一层都向上一层提供明确的服务,同时将本层服务的实现封装起来。一个在相邻层之间完善的接口定义了下层对上层所提供的服务以及如何访问这些服务。

Windows2000的网络驱动程序实现了这个网络结构的下面四层。

l         物理层:

这是OSI模型中的最底层,这一层用来通过物理介质接收和发送的原始的没有结构的二进制数据流,它描述了对于物理介质的电/光,机械和功能的接口,物理层为所有的上层传送信号,在Windows2000中,物理层通过网络接口卡(NIC)来实现,物理层的收发是依附于NIC介质的。使用穿行端口的网络组件,物理层也同样包含了底层的网络软件,它定义了串行位流是如何分成数据包的。

l         数据链路层:

电气电子工程师协会(IEEE)将该层进一步分成了两个子层,LLCMACLLC子层用于将数据帧从一个结点无错的传输到另一个结点。LLC子层用来建立和终止逻辑链接,控制帧流,对帧排序,接收帧,并且对没有被接收的帧进行重发。LLC子层使用帧应答和帧的重发为经过链路层的上层提供了真正的无错发送。MAC子层控制物理介质的访问,检查帧的错误,并且管理接收帧的地址认证。在Windows2000网络结构中,逻辑链路控制子层在传输驱动程序中实现,而介质访问控制子层则在网络接口卡(NIC)中实现NIC由一个名为NIC驱动程序的设备驱动程序软件控制。Windows2000附带了许多常用的NIC的驱动程序。

l         网络层:

这一层控制着子网的运作,它基于以下因素决定数据的物理路径:

u       网络状况

u       服务优先权

u       其它因素,包括路由、流量控制、帧的分解和重组、逻辑到物理地址的映射、用户帐号。

l         传输层:

这一层确保信息传送的无错传输,连续传输和不丢失或者不重复。它使得上层协议与上层协议之间或与它同层的协议之间通讯不必关心数据的传输。传输层所在的协议栈至少应包括一个可靠的网络层,或在逻辑链路控制子层中提供一个虚电路。例如,因为Windows2000 NetBEUI传输驱动程序包括一个与OSI兼容的LLC子层,它的传输层的功能就很小。如果协议栈不包括LLC子层,并且网络层不可靠,并且/或者支持自带地址信息(例如TCP/IP层或NWLINKIPX层),那么传输层应能进行帧的顺序控制和帧的响应,同时要对未响应帧进行重发。

Windows2000网络结构中,逻辑链路层,物理层和传输层都是通过名为传输驱动程序的软件实现的,它有时也称作协议,协议驱动程序或协议模块。Windows2000附带了TCP/IPIPX/SPXNetBEUIAppleTalk传输驱动程序。

 

2.2NDIS驱动程序

 

网络驱动程序接口说明(NDIS)库将网络硬件抽象为网络驱动程序。NDIS也说明了网络驱动程序间的标准接口,因此它将用来管理硬件的底层驱动程序抽象为上层驱动程序,例如网络传输层。NDIS也维护着状态信息和网络驱动程序的参数,包括指向函数的指针,句柄和链接时参数块的指针,以及其他系统参数。

 

NDIS支持以下几种类型的网络驱动程序:

l         微端口驱动程序

l         中间层驱动程序

l         协议驱动程序

 

2007年03月17日

这一章为网络驱动程序设计指南提供了一个导航,它将以你将编写的内核模式网络驱动程序的类型为基础,告诉你需要参见这个指南的哪些部分。

微软的Windows2000支持三种基本的内河模式网络驱动程序:

l         微端口NIC驱动程序:

一个微端口的驱动程序直接控制一个网络接口卡(NIC),并且为高层的驱动程序提供接口。

l         中间层驱动程序:

一个中层协议驱动程序连接了上层协议,例如早期的传输驱动程序和一个微端口。开发中层协议驱动程序的一个普遍原因是它在早期的传输驱动程序和一个微端口之间实现转换。一个微端口控制了一个NIC,对于传输驱动程序来说,它是一个陌生的新介质类型。

l         协议驱动程序:

一个上层协议驱动程序向网络用户提供服务,它实现了TDI接口,或者也许是为另一个上一层的特殊应用而提供的接口。这种驱动程序在它的下边界提供了一个协议接口,用来向低层驱动程序发送和接收数据包。

另一种协议驱动程序是一种面向连接的呼叫管理器,一个呼叫管理器为面向连接的客户提供了呼叫建立和呼叫撤销服务,呼叫管理器也是协议驱动程序。

Windows2000支持的另外一种内核模式驱动程序类型是一过滤钩子驱动程序。一个过滤钩子驱动程序用来过滤数据包,它扩展了操作系统提供的IP过滤驱动程序的功能。

2007年01月04日

    上一节仅仅生成了控制设备对象。但是不要忘记,去冬开发的主要工作是撰写分发历程(dispatch functions)。接上一节,我们已经知道自己的DriverObject保存在上文代码的driver中。现在我写如下一个函数来指定一个默认的dispatch function给它。

//—————–wdf.h中的代码———————-

typedef PDRIVER_DISPATCH wd_disp_fuc;

_inline wd_void wd_drv_set_dispatch(in wd_drv * driver , in wd_disp_fuc disp)

{

    wd_size i;

    for (i=0 ; i <=IRP_MJ_MAXIMUM_FUNCTION ; i++)

    {

        driver->MajorFunction[i] = disp;

    }

}

    在前边的wd_main中,我只要加

wd_drv_set_dispatch(driver , my_dispatch_func);

    就为这个驱动指定了一个默认的Dispatch Function。所有的IRP请求,都会被发送到这个函数。但是,我可能不希望这个函数处理过于复杂,而希望把一些常见的请求独立出来,如Read、Write、Create、Close,那我又写了几个函数专门用来设置这几个Dispatch Functions。

//—————–wdf.h中的代码———————-

第二部分:Hello World驱动对象与设备对象

    这里所说的驱动对象是一种数据结构,在DDK中名为DRIVER_OBJECT。任何驱动程序都对应一个DRIVER_OBJECT。如何获得本人所写的驱动对应的DRIVER_OBJECT呢?驱动程序的入口函数为DriverEntry,因此,当你写一个驱动的开始,你会写下如下的代码:

    NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject , IN PUNICODE_STRING RegistryPath )

   {

    }

    这个函数就相当于与喜欢C语言的您所常用的main()。IN是无意义的宏,仅仅表明后面的参数是一种输入,而对应的OUT则代表这个参数是一种返回。这里没有使用引用,因此如果想在参数中返回结果,一律传入指针。

    DriverObject就是您所写的驱动对应的DRIVER_OBJECT,是系统在加载您的驱动时所分配的。RegisteryPath是专用于您记录驱动相关参数的注册表路径。

    DriverObject重要之处,在于它拥有一组函数指针,称为dispatch functions。

    开发驱动的主要任务就是亲手撰写这些dispatch functions。当系统用到你的驱动,会向你的DO发送IRP(这是Windows所有驱动的共同工作方式)。您的任务是在这些dispatch function中处理这些请求。您可以让IRP失败,也可以成功返回,也可以修改这些IRP,甚至可以自己发出IRP。

    设备对象则是指DEVICE_OBJECT。下边简称DO。

    但是实际上每个IRP都是针对DO发出的。只有针对由该驱动所生成的DO的IRP,才会发给该驱动来处理。

    当一个应用程序打开文件并读写文件的时候,Windows系统将这些请求变成IRP发送给文件系统驱动。

    文件系统过滤驱动可以iguolv这些IRP。这样,您就拥有了捕获和改变文件系统的能力。

    像Fat32、NTFS这样的文件系统(File System简称FS),可能生成好几种设备。首先文件系统驱动本身往往生成一个控制设备(CDO)。这个设备的主要任务是修改驱动的内部配置。因此一个Driver只对应一个CDO。

    另一种设备是被这个文件系统Mount的Volume。一个FS可能有多个Volume,也可能一个都没有。解释一下,如果您有C:、D:、E:、F:四个分区。C:、D:为NTFS,E:、F:为Fat32,那么C:、D:则是Fat的两个Volume设备对象。(不是太明白,谁能给解释一下?)

    实际上“C:”是该设备的符号连接(Symbolic Link)名。而不是真正的设备名。可以打开Symbolic Links Viewer,能看到:C: DeviceHarddiskVolume1。因此该设备的设备名为:“DeviceHarddiskVolume1”。

    这里也看出来,文件系统驱动是针对每个Volume来生成一个DeviceObject,而不是针对每个文件的。实际上对文件的读写IRP都发送到Volume设备对象上去了。并不会生成一个“文件设备对象”。

    掌握了这些概括的话,我们现在用简单的代码来生成我们的CDO,作为我们开发文件系统驱动的第一步小试牛刀。

    我不喜欢用微软风格的代码。太长而且难看。我对大部分的数据结构和函数进行了重定义。为此我写了一个名为wdf.h的头文件帮助我转换。有兴趣的读者可以发邮件向我索取这个文件。没有也没有关系,我总是会写出WD_XXX系列的东西在DDK中的原型。

// —————–wdf_filter.c中的内容————————-

#include "wdf.h"

wd_stat wdff_cdo_create(in wd_drv *driver , in wd_size exten_len , in wd_ustr *name , out wd_dev **device)

{

    return wd_dev_create(driver , exten_len , name , wd_dev_disk_fs , wdf_dev_secure_open , wd_false , device);

}

wd_stat wd_main(in wd_drv * driver , in wd_ustr * reg_path)

{

    wd_ustr name;

    wd_stat status = wd_stat_suc;

    //然后我生成控制设备,虽然现在我的控制设备什么都不干

    wd_ustr_init(&name , L"\FileSystem\Filters\our_fs_filter");

    status = wdff_cdo_create(driver , 0 , &name , &g_cdo);

    if (!wd_suc(status))

    {

        if (status == wd_stat_path_not_found)

       {

            //这种情况发生于FileSystemFilters路径不存在。这个路径是

            //在XP上才加上的。所以2000下会运行到这里。

            wd_ustr_init(&name , L"\FileSystem\our_fs_filter");

        }

        if (!wd_suc(status))

        {

            wd_printf0("Error : Create CDO Failed.m");

            return status;

        }

    }

    wd_printf0("Success : Create CDO Ok.m");

    return status;

}

    为了让代码看起来像上边的那样,我不得不作了很多转换。如:#define DriverEntry wd_main

    一种爽得感觉,终于可以在写看起来更像是main()的函数中工作了。wd_dev_create这个函数内部调用的是IoCreateDevice。而wd_suc实际上是SUCCESS()这样的宏。

// ———————-wdf.h中的内容——————————

#include "ntifs.h"

#define in IN
#define out OUT
#define optional     OPTIONAL
#define wd_ustr     UNICODE_STRING
#define wdp_ustr   PUNICODE_STRING
#define wd_main    DriverEntry

// 设备、驱动对象类型
typedef DRIVER_OBJECT   wd_drv;
typedef DEVICE_OBJECT   wd_dev;
typedef PDRIVER_OBJECT wd_pdrv;
typedef PDEVICE_OBJECT wd_pdev;

enum {
              wd_dev_disk_fs        = FILE_DEVICE_DISK_FILE_SYSTEM,
              wd_dev_cdrom_fs    = FILE_DEVICE_CD_ROM_FILE_SYSTEM,
              wd_dev_network_fs = FILE_DEVICE_NETWORK_FILE_SYSTEM
         };

// 状态相关的类型和宏
typedef NTSTATUS wd_stat;

enum {
              wd_stat_suc                             = STATUS_SUCCESS,
              wd_stat_path_not_found          = STATUS_OBJECT_PATH_NOT_FOUND,
              wd_stat_insufficient_res            = STATUS_INSUFFICIENT_RESOURCES,
              wd_stat_invalid_dev_req           = STATUS_INVALID_DEVICE_REQUEST,
              wd_stat_no_such_dev              = STATUS_NO_SUCH_DEVICE,
              wd_stat_image_already_loaded = STATUS_IMAGE_ALREADY_LOADED,
              wd_stat_more_processing         = STATUS_MORE_PROCESSING_REQUIRED,
              wd_stat_pending                       = STATUS_PENDING
          };

    (inline的使用是有所限制的,inline函数一般必须在头文件内,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接地跪函数(自己内部还调用自己的函数))

_inline wd_bool wd_suc(wd_stat state)    //内聯函數
{
     return NT_SUCCESS(state);
}

#define wd_printf0 DbgPrint

_inline wd_void wd_ustr_init(in out wd_ustr* str, in const wd_wchar* chars)
{
    RtlInitUnicodeString(str,chars);
};

_inline wd_void wd_ustr_init_em(in out wd_ustr *str ,in wd_wchar *chars, in wd_size size)
{
    RtlInitEmptyUnicodeString(str,chars,size);
};

    wdf.h这个文件我仅仅节选了需要的部分。以上您已经拥有了一个简单的“驱动”的完整的代码。它甚至可以编译、安装(请修改sfilter.inf文件,其方法不过是将多处的sfilter修改为“our_fs_filter”,希望这个过程中您不会出现问题)。然后把wdf.h和wdf_filter.c放在您新建立的目录下,这个目录下还应该有另两个文件。一个是MakeFile,请从SFilter目录下拷贝。另一个是SOURCES,请输入如下内容:

    TARGETNAME = our_fs_filter

    TARGETPATH = obj

    TARGETTYPE = DRIVER

    DRIVERTYPE = FS

    BROWSER_INFO = 1

    SOURCES = wdf_filter.c

    使用DDK编译之后您将得到our_fs_filter.sys。把这个文件与前所描述的inf文件同一目录,按上节所述方法安装。

    这个驱动不起任何作用,但是您已经成功的完成了“Hello World”。

2007年01月03日

第一部分:概述,钻研目的和准备

我经常在网上碰到同行请求开发文件系统驱动。Windows的PC机上以过滤驱动居多。其目的不外乎有以下几种:

    1、用于防病毒引擎。希望在文件读写的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。

    2、用于加密文件系统,希望在文件写过程中对数据进行加密,在读的过程中进行解密。

    3、设计透明的文件系统加速。读写磁盘的时候,何时的Cache算法是可以大大提高磁盘的工作效率。Windows本身的Cache算法未必适合一些特殊的读写磁盘操作(如流媒体服务器上读流媒体文件)。

    文件系统驱动是Windows系统中最复杂的驱动种类之一。不能对IFSDDK中的帮助抱太多希望,文件系统的DDK帮助极其简略,很多重要部分仅仅轻描淡写的带过。如果安装了IFSDDK,应该阅读SRC\FileSys\OSR_Docs下的文档。而不仅仅是DDK帮助。

    文件系统驱动开发方面的书籍很少。中文资料仅仅见过候捷翻译过的一本驱动开发的书上有两三章涉及,而且也仅仅只能用于9X的VXD驱动。NT文件系统见过一本英文书。我都不记得这两本书的书名了。

    如果您打算开发9X或者NT文件系统驱动,建议你去网上下载上文提及的书。那两本书都有免费的电子版本下载。如果您打算开发Windows 2000\Windows XP\Windows2003的文件系统驱动,您可以阅读本教程。虽然本教程仅仅讲述文件过滤驱动。但是如果您要开发一个全新的文件系统驱动的话,本教程依然对您有很大的帮助。

    学习文件系统开发之前,应该在机器上安装IFSDDK。DDK版本越高级,其中头文件中提供的系统调用也越多。经常有人询问如XPDDK编译的驱动能不能在2000上运行等等的问题。我想可以这样解释:高级版本的DDK应该总是可以编译低级驱动的代码,而且得到的二进制版本总是可以在低级系统上运行。但反过来就未必可以了。如果在高级系统上编写用于低级系统上的驱动,要非常认真的注意仅仅调用低级系统上有的系统调用。

    IIFDDK可以在某些FTP上免费下载。

    我使用的IFS DDK For XP,但是我市集用来开发的两台机器有一台是Windows 2000,另一台是Windows 2003。我尽量使我编译出来的驱动,可以在2000\XP\2003三种系统上都通过测试。

    安装配置DDK和在VC中开发驱动的方法网上有很多的介绍。IFSDDK安装之后,SRC目录下的FileSys目录下有文件系统驱动的示例。阅读这些代码你就可以快速的学会文件系统驱动开发。

    Filter目录下的SFilter是一个文件系统过滤驱动的例子。另一个FileSpy完全是用这个例子的代码加工得更复杂而已。

    如何用DDK编译这个例子请自己察看相关的资料。

    文件系统过滤驱动编译出来以后,您得到的是一个扩展名为SYS的文件。同时,您需要写一个.INF文件来实现这个驱动的安装。我这里不讨论.INF文件的细节,您可以直接用SFilter目录下的INF文件修改。

    对INF文件点鼠标探出菜单选择“安装”,即可安装这个过滤驱动。但是必须重新启动系统才生效。

    如果重启后蓝屏无法启动,可以用其他的方式引导系统后到System32\Drivers目录下删除你的.SYS文件再重启即可。我尝试这种情况下用安全模式结果还是蓝屏。所以我后来不得不在机器上装了两个系统。双系统情况下,一个系统崩溃了用另一个系统启动,删除原来的驱动即可。

    如果要调试代码,请安装SoftICE。

    打开SFilter目录下的文件Sources(这个文件没有扩展名),加入一行:BROWSER_INFO=1,然后打开Symbol Loader,File->Open选中你编译出来的XXX.SYS,Modul->Load,Modul->Translate,然后就可以调试了。

    打开SoftICE,输入File *就可以看见代码。

    如果准备好了,我们就可以开始琢磨Windows文件系统过滤驱动的开发了。

2006年11月15日

IRP处理的“标准模型”


粒子物理学里有关于宇宙的“标准模型”,WDM也是这样。图5-5显示了一个典型的IRP在各个处理阶段的所有权流程。并不是每种IRP都经过这些步骤,由于设备类型和IRP种类的不同某些步骤会改变或根本不存在。尽管这个过程可能有各种变化形式,但这个图为我们将要展开的讨论提供了一个很好的起点。

图5-5. IRP处理的“标准模型”

创建IRP

IRP开始于某个实体调用I/O管理器函数创建它。在上图中,我使用术语“I/O管理器”来描述这个实体,尽管系统中确实有一个单独的系统部件用于创建IRP。事实上,更精确地说,应该是某个实体创建了IRP,并不是操作系统的某个例程创建了IRP。例如,你的驱动程序有时会创建IRP,而此时出现在图中第一个方框中的实体就应该是你的驱动程序。

可以使用下面任何一种函数创建IRP:

  • IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
  • IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
  • IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
  • IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。

前两个函数中的Fsd表明这些函数专用于文件系统驱动程序(FSD)。虽然FSD是这两个函数的主要使用者,但其它驱动程序也可以调用这些函数。DDK还公开了一个IoMakeAssociatedIrp函数,该函数用于创建某些IRP的从属IRP。WDM驱动程序不应该使用这个函数。

决定该调用哪一个函数,和决定对IRP执行什么额外的初始化是更复杂的问题,我将在本章的结尾再回到这个问题上。

发往派遣例程

创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序:

PDEVICE_OBJECT DeviceObject; 		//something gives you this
PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
stack->MajorFunction = IRP_MJ_Xxx;
<other initialization of "stack">
NTSTATUS status = IoCallDriver(DeviceObject, Irp);

IoCallDriver函数的第一个参数是你在某处获得的设备对象的地址。我将在本章的结尾处描述获得设备对象指针的两个常用方法。在这里,我们先假设你已经有了这个指针。

IRP中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这个指针被初始化为指向一个不存在的“-1”元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是“下一个”堆栈单元。IoCallDriver将沿着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码,在上例中为IRP_MJ_Xxx。然后IoCallDriver函数将利用DriverObject指针找到设备对象中的MajorFunction表。IoCallDriver将使用主功能代码索引这个表,最后调用找到的地址(派遣函数)。

你可以把IoCallDriver函数想象为下面代码:

NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp)
{
  IoSetNextIrpStackLocation(Irp);
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  stack->DeviceObject = device;
  ULONG fcn = stack->MajorFunction;
  PDRIVER_OBJECT driver = device->DriverObject;
  return (*driver->MajorFunction[fcn])(device, Irp);
}

派遣例程的职责

IRP派遣例程的原型看起来像下面这样:

NTSTATUS DispatchXxx(PDEVICE_OBJECT device, PIRP Irp)
{
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);				<--1
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension;			<--2
  ...
  return STATUS_Xxx;											<--3
}
  1. 你通常需要访问当前堆栈单元以确定参数或副功能码。
  2. 你可能还需要访问你创建的设备扩展。
  3. 你将向IoCallDriver函数返回某个NTSTATUS代码,而IoCallDriver函数将把这个状态码返回给它的调用者。

在本书中,我使用DispatchXxx(如DispatchReadDispatchPnp,等等)来代表例子驱动程序中的派遣例程。其它人可能会使用另外的约定,但Microsoft推荐用这样的方法,例如,如果你的驱动程序名为RANDOM.SYS,那么你应该命名IRP_MJ_READ派遣函数为RandomDispatchRead。这个方法使驱动程序调试跟踪起来更容易,但它同时也需要你输入更多的文字。由于这些名称在驱动程序的名空间之外是不可见的,所以由你自己决定是使用Microsoft推荐的命名方案,还是使用你认为更有意义的命名方法。

在上面派遣函数原型中省略号的地方,是派遣函数必须做出决定的地方,有三种选择:

  • 派遣函数立即完成该IRP。
  • 把该IRP传递到处于同一堆栈的下层驱动程序。
  • 排队该IRP以便由这个驱动程序中的其它例程来处理。

我将在本章详细讨论这三种选择,但在这里我仅讨论排队的可能性,因为这个过程就是IRP处理标准模型所描述的。你知道,当有大量读写请求进入设备时,通常需要把这些请求放入一个队列中,以便使硬件访问串行化。

每个设备对象都自带一个请求队列对象,下面是使用这个队列的标准方法:

NTSTATUS DispatchXxx(...)
{
  ...
  IoMarkIrpPending(Irp);								<--1
  IoStartPacket(device, Irp, NULL, NULL);						<--2
  return STATUS_PENDING;								<--3
}
  1. 无论何时,当你的派遣例程返回STATUS_PENDING状态代码时,你应该先调用这个IoMarkIrpPending函数,以帮助I/O管理器避免内部竞争。我们必须在放弃IRP所有权之前做这一点。
  2. 如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把设备置成忙并调用StartIo例程。IoStartPacket的第三个参数是用于排序队列的键(ULONG)的地址,例如磁盘驱动程序将在这里指定一个柱面地址以提供顺序搜索的排队。如过你在这里指定一个NULL,则该请求被加到队列的尾部。最后一个参数是取消例程的地址。我将在本章的后面讨论取消例程,这种例程比较复杂。
  3. 返回STATUS_PENDING以通知调用者我们没有完成这个IRP。

注意,一旦我们调用了IoStartPacket函数,就不要再碰IRP。因为在该函数返回之前,IRP可能已经被完成并且其占用的内存可能被释放,而我们拥有的该IRP的指针也许是无效的。

StartIo例程

每处理一个IRP,I/O管理器就调用一次StartIo例程:

VOID StartIo(PDEVICE_OBJECT device, PIRP Irp)
{
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension;
  ...
}

StartIo例程在DISPATCH_LEVEL级上获得控制,这意味着该函数不能生成任何页故障。另外,设备对象的CurrentIrp域和Irp参数都指向I/O管理器送来的IRP。

StartIo的工作是就着手处理IRP。如何做要完全取决于你的设备。通常你需要访问硬件寄存器,但可能有其它例程,如你的中断服务例程,或者是驱动程序中的其它例程也需要访问这些寄存器。实际上,有时着手一个新操作的最容易的方式是在设备扩展中保存某些状态信息,然后伪造一个中断。由于这些方法的执行都需要在一个自旋锁的保护之下,而这个自旋锁与保护你的ISR所使用的是同一个自旋锁,所以正确的方法是调用KeSynchronizeExecution函数。例如:

VOID StartIo(...)
{
  ...
  KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx);
}

BOOLEAN TransferFirst(PVOID context)
{
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context;
  ...
  return TRUE;
}

这里的TransferFirst例程是同步关键段(SynchCritSection)的一个例子,之所以这样做是因为StartIo需要与ISR同步。我将在第七章中详细讨论同步关键段(SynchCritSection)的概念。

一旦StartIo使设备忙于处理新请求,它就立即返回。当设备完成传输并发出中断时你将看到下一个请求。

中断服务例程

当设备完成数据传输后,它将以硬件中断形式发出通知。在第七章中,我将讲述如何用IoConnectInterrupt函数“钩住”一个中断,该函数的一个参数就是ISR的地址。因此当中断发生时,硬件抽象层(HAL)就调用你的ISR。ISR运行在DIRQL上,并由ISR专用的自旋锁保护。ISR的函数原型如下:

BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID context)
{
  ...
}

ISR的第一个参数是中断对象的地址,中断对象由IoConnectInterrupt函数创建,但是你不太可能用到这个参数。第二个参数是在调用IoConnectInterrupt时你指定的任意上下文值;它可能是设备对象或设备扩展的地址,完全由你决定。

我将在第七章中详细讨论ISR的职责。为了继续标准模型的讨论,我要告诉你一点,一个ISR最可能做的事就是调度DPC例程(推迟过程调用)。而DPC的目的就是让你做某些事情,如调用IoCompleteRequest,而该调用不可能运行在ISR运行的DIRQL级上。所以,你的ISR中将有下面一行语句(device是指向设备对象的指针):

IoRequestDpc(device, device->CurrentIrp, NULL);

那么下一次你将在DPC例程中看到这个IRP,这个DPC例程是你在AddDevice函数中用IoInitializeDpcRequest寄存的。DPC例程的传统名称为DpcForIsr,因为它是由ISR请求的。

DPC例程

DpcForIsr例程在DISPATCH_LEVEL级上获得控制。通常,它的工作就是完成IRP(导致最近的中断发生)。但一般情况下,它通过调用IoCompleteRequest函数把剩余的工作交给完成例程来做。

VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP Irp, PDEVICE_EXTENSION pdx)
{
  ...
  IoStartNextPacket(device, FALSE);						<--1
  IoCompleteRequest(Irp, boost);						<--2
}
  1. IoStartNextPacket 取出设备队列中的下一个IRP并发送到StartIo。FALSE参数指出该IRP不能以通常方式取消。
  2. IoCompleteRequest 完成第一个参数指定的IRP。第二参数是等待线程的优先级提高值。注意在调用IoCompleteRequest之前你还要填充IRP中的IoStatus块。

调用IoCompleteRequest例程是处理I/O请求的标准结束方式。在这个调用之后,I/O管理器(或者是任何在开始处创建该IRP的实体)将再次拥有该IRP。最后该IRP被这个实体销毁并解除等侍线程的阻塞状态。

定制队列

有些设备的操作需要多个请求队列。一个常见的例子就是串行口,它可以同时地并且分开地处理输入输出请求流。IoStartPacket和IoStartNextPacket函数(以及其它含有键排序功能的等价函数)都使用设备对象自带的队列。创建与标准队列有相同工作方式的附加队列要相对容易一些。

为了使我们更容易讨论问题,让我们假设你需要一个单独的队列来管理IRP_MJ_SPECIAL(并不存在这个主功能码,使用它是为了使问题更具体一些)请求。你将写两个与StartIo和DpcForIsr例程功能类似的,但专用于处理这些假想IRP的辅助例程:

  • 与StartIo类似的函数 — 我们称它为StartIoSpecial — 它启动下一个IRP_MJ_SPECIAL请求。
  • 与DPC类似的函数 —  我们称它为DpcSpecial — 它处理IRP_MJ_SPECIAL请求的完成。

你还需要在你的设备扩展中创建一个KDEVICE_QUEUE对象,并在AddDevice例程中初始化这个队列对象:

NTSTATUS AddDevice(...)
{
  ...
  KeInitializeDeviceQueue(&pdx->dqSpecial);
  ...
}

dqSpecial就是KDEVICE_OBJECT对象的名字,用于排队IRP_MJ_SPECIAL请求。设备队列对象是一种三态对象(见图5-6)。这三种状态反映了设备队列例程是如何操作设备队列的:

  • idle状态是指设备不忙于处理任何请求并且队列为空。KeInsertDeviceQueueKeInsertByKeyDeviceQueue函数把队列标记为busy-empty状态并返回FALSE。你不应该在队列为idle状态时调用KeRemoveDeviceQueueKeRemoveByKeyDeviceQueue函数。
  • busy-empty状态是指设备忙但队列中没有IRP。KeInsertDeviceQueue和KeInsertByKeyDeviceQueue函数向队列尾加入新IRP,使队列进入busy-not empty状态,并返回TRUE。KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue函数返回NULL并使队列进入idle状态。
  • busy-not empty状态是指设备忙且队列中至少存有一个IRP。KeInsertDeviceQueue和KeInsertByKeyDeviceQueue函数向队列尾加入新IRP并返回TRUE,但队列状态不变。KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue函数提取队列的第一个IRP并返回其地址,此时,如果队列为空,这些函数将把队列置成busy-empty状态。

图5-6. KDEVICE_QUEUE队列的三种状态

在下面代码中,我们在派遣例程和DPC例程中使用了这些支持例程和我们专用的设备队列:

NTSTATUS DispatchSpecial(PDEVICE_OBJECT fdo, PIRP Irp)
{
  IoMarkIrpPending(Irp);								<--1
  KIRQL oldirql;
  KeRaiseIrql(DISPATCH_LEVEL, &oldirql);						<--2
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  if (!KeInsertDeviceQueue(&pdx->dqSpecial, &Irp->Tail.Overlay.DeviceQueueEntry))	<--3
    StartIoSpecial(fdo, Irp);
  KeLowerIrql(oldirql);
  return STATUS_PENDING;
}

VOID DpcSpecial(...)
{
  ...
  PKDEVICE_QUEUE_ENTRY qep = KeRemoveDeviceQueue(&pdx->dqSpecial);			<--4
  if (qep)
    StartIoSpecial(fdo, CONTAINING_RECORD(qep, IRP, Tail.Overlay.DeviceQueueEntry));
  ...
}
  1. 作为一个“规矩”的派遣例程,我们把该IRP标记为pending,因为我们要让它进入队列,然后派遣例程返回STATUS_PENDING状态。
  2. KeInsertDeviceQueue函数和我们自己的StartIoSpecial例程希望在DISPATCH_LEVEL级上被调用。所以我们明确地提升了IRQL,之后很快我们又调用KeLowerIrql函数把IRQL降低到原来的级别(可能是PASSIVE_LEVEL)。
  3. KeInsertDeviceQueue函数把IRP加入到专用队列中,如果返回值为TRUE,表明IRP已被加入队列中,所以我们不用再做任何关于IRP的事。如果设备正空闲,那么返回值应该为FALSE并且IRP也用不着排入队列,我们直接调用StartIoSpecial例程。
  4. DPC中的这个KeRemoveDeviceQueue调用将产生两种结果。如果队列当前为空,则返回值为NULL并且我们也不用启动新请求。否则,返回值将为某IRP内嵌连接域的地址。我们可以使用CONTAINING_RECORD宏来获得外围的真正的IRP地址。然后我们把这个地址传递给StartIoSpecial例程。注意,这个DPC例程已经运行在DISPATCH_LEVEL级上,所以我们不需要在删除队列表项或调用StartIo之前调整IRQL。

我以前描述的StartPacket和StartNextPacket函数使用一个名为DeviceQueue的KDEVICE_QUEUE对象。该对象是设备对象中的一个不透明域,其工作原理与管理私有设备队列相同。

有两个数据结构对I/O请求的处理至关重要:I/O请求包(IRP)本身和IO_STACK_LOCATION结构。下面我将详细描述这两个结构。

IRP结构

图5-1显示了IRP的数据结构,阴影部分代表不透明域。下面是该结构中重要域的简要描述。

MdlAddress(PMDL)域指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。为了访问用户模式缓冲区,驱动程序必须做一点额外工作。

图5-1. I/O请求包数据结构

Flags(ULONG)域包含一些对驱动程序只读的标志。但这些标志与WDM驱动程序无关。

AssociatedIrp(union)域是一个三指针联合。其中,与WDM驱动程序相关的指针是AssociatedIrp.SystemBuffer。 SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。对于IRP_MJ_READ和IRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志,则I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代码指出需要缓冲区(见第九章),则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了METHOD_BUFFERED的I/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区,然后I/O管理器再把数据复制到用户模式的输出缓冲区。

IoStatus(IO_STATUS_BLOCK)是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码,而IoStatus.Information的类型为ULONG_PTR,它将收到一个信息值,该信息值的确切含义要取决于具体的IRP类型和请求完成的状态。Information域的一个公认用法是用于保存数据传输操作,如IRP_MJ_READ,的流量总计。某些PnP请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。

RequestorMode将等于一个枚举常量UserModeKernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。

PendingReturned(BOOLEAN)如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。

Cancel(BOOLEAN)如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。取消IRP是一个相对复杂的主题,我将在本章的最后详细描述它。

CancelIrql(KIRQL)是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。

CancelRoutine(PDRIVER_CANCEL)是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine函数设置这个域而不是直接修改该域。

UserBuffer(PVOID) 对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。

Tail.OverlayTail联合中的一种结构,它含有几个对WDM驱动程序有潜在用途的成员。图5-2是Tail联合的组成图。在这个图中,以水平方向从左到右是这个联合的三个可选成员,在垂直方向是每个结构的成员描述。Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)和Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare内一个未命名联合的两个可选成员(只能出现一个)。I/O管理器把DeviceQueueEntry作为设备标准请求队列中的连接域。当IRP还没有进入某个队列时,如果你拥有这个IRP你可以使用这个域,你可以任意使用DriverContext中的四个指针。Tail.Overlay.ListEntry(LIST_ENTRY)仅能作为你自己实现的私有队列的连接域。

 

图5-2. IRP中Tail联合的组成图

CurrentLocation (CHAR)和Tail.Overlay.CurrentStackLocation(PIO_STACK_LOCATION)没有公开为驱动程序使用,因为你完全可以使用象IoGetCurrentIrpStackLocation这样的函数获取这些信息。但意识到CurrentLocation就是当前I/O堆栈单元的索引以及CurrentStackLocation就是指向它的指针,会对驱动程序调试有一些帮助。

I/O堆栈

任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用(见图5-3)。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。图5-4显示了堆栈单元的结构。

 

图5-3. 驱动程序和I/O堆栈之间的平行关系

注意

我将在本章稍后处讨论IRP的创建机制。将帮助你了解DEVICE_OBJECT的StackSize域,该域指出IRP应为其目标设备驱动程序保留多少堆栈单元。

图5-4. I/O堆栈单元数据结构

MajorFunction(UCHAR)是该IRP的主功能码。这个代码应该为类似IRP_MJ_READ一样的值,并与驱动程序对象中MajorFunction表的某个派遣函数指针相对应。如果该代码存在于某个特殊驱动程序的I/O堆栈单元中,它有可能一开始是,例如IRP_MJ_READ,而后被驱动程序转换成其它代码,并沿着驱动程序堆栈发送到低层驱动程序。我将在第十一章(USB总线)中举一个这样的例子,USB驱动程序把标准的读或写请求转换成内部控制操作,以便向USB总线驱动程序提交请求。

MinorFunction(UCHAR)是该IRP的副功能码。它进一步指出该IRP属于哪个主功能类。例如,IRP_MJ_PNP请求就有约一打的副功能码,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。

Parameters(union)是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数。这些子结构包括Create(IRP_MJ_CREATE请求)、Read(IRP_MJ_READ请求)、StartDevice(IRP_MJ_PNP的IRP_MN_START_DEVICE子类型),等等。

DeviceObject(PDEVICE_OBJECT)是与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。

FileObject(PFILE_OBJECT)是内核文件对象的地址,IRP的目标就是这个文件对象。驱动程序通常在处理清除请求(IRP_MJ_CLEANUP)时使用FileObject指针,以区分队列中与该文件对象无关的IRP。

CompletionRoutine(PIO_COMPLETION_ROUTINE)是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。

Context(PVOID)是一个任意的与上下文相关的值,将作为参数传递给完成例程。你绝对不要直接设置该域;它由IoSetCompletionRoutine函数自动设置,其值来自该函数的某个参数。

2006年06月16日

以后填表的时候得填已婚了。哈哈,帅。

2006年03月04日

 

    网上有许多介绍IOCP(Input/Output Completion Port)技术的文章,但是TMD在真正做项目的时候大多用不上。把《Windows网络编程第二版》快翻烂了才终于弄明白了这项技术。

    我的的目标是:

        1、使用IPV4技术构建一个支持65000个并发连接的代理服务器

 2、能够在网络上传输大型文件

        3、写出助手类TIOCPNet

    对关键字的一些描述:

 1、IOCP:
     IOCP以内核模式发现Socket事件,并且IOCP提供了立即连接用户数据和Socket对象的机制。

 2、AcceptEx:
     在超过30,000个并发连接连接到服务器的时候,使用Accept或WSAAccept将会得到WSAENOBUFS(10055)错误。出现这个错误的原因是因为系统无法准备好资源来捕捉这么多并发连接所发送过来的数据。因此,必须在使用Socket资源前准备好这些Socket资源,这就只能使用AcceptEx了。使用AcceptEx的主要优势是:在使用Socket之前就将Socket准备好了。不过AcceptEx使用起来相当繁琐,而且非常麻烦。

 3、Static Memory:
     用户在服务器端使用(Per-Allocated Memory)。当接收到一个发送过来的数据报后,必须使用静态内存。在TIOCPNet中将使用类TPerllocator来获取Per-Allocated Memory。

 4、Sliced Data Chunk:
     曾经遇到过这种情况:即在使用WriteFile、WSASend或Send函数发送超过1KB字节的数据包时接受端无法正确接收数据。造成这个问题的原因应该是发送的数据报的大小超过了网络设备(路由器、HUB等)的MUT(Most Transfer Unit)的最小值。网络设备MUT的最小值为576字节。因此应该将一个大的数据包拆分为比576字节小的数据包,在TIOCPNet中,我定义数据单元块的大小(BUFFER_UNIT_SIZE)为512字节。

 5、线程Thread:
     如果服务器逻辑中需要进行一些I/O操作的话,最好构建一些线程,但是线程的数量也不是越多越好的。这是因为线程的调度和上下文切换也都是需要耗费CPU资源的,在一个系统中有超过10000个的线程并发运行时,CPU估计就要挂了,在TIOCPNet中我为每个CPU分配了两个线程。


    TIOCPNET是实现上述想法的具体实现类。TIOCPNet的操作步骤如下:
 
 1、TIOCPet将自行准备那些诸如Per-Allocated内存块、完成端口、其他句柄等资源;

 2、TIOCPNet将创建监听Socket对象

 3、TIOCPNet生成Pre-Generates Sockets和它自己的缓冲Socket,并将他们放置到AcceptEx的AccepTable中。

 4、接受用户发出的连接请求。

 5、当一个Socket对象读取数据包时,TIOCPNet对象将把他们放置到他们自己的Pre-Allocated Reading Slots中,并向服务器发送一个事件。

 6、当服务器写数据包时,TIOCPNet将这些数据放置到Pre-Allocated Writing Blocks中并调用PostQueuedCompletionStatus函数,然后使用工作器线程发送这些数据包

 7、当客户端用户关闭连接时,TIOCPNet关闭相应Socket对象,但是TIOCPNet并不会真正释放内存缓冲区,紧紧再分配这些缓冲区。

2006年01月17日

 
dir 21:34:28
80
弘川科技 21:36:45
水水呢?
&水水|Eddie 21:38:12
我早都瞎玩了
&水水|Eddie 21:38:16
下完了
弘川科技 21:38:26
你先别写类呢。
弘川科技 21:38:30
咱们开始讨论。
弘川科技 21:39:11
首先,从咱们的工作开始。
&水水|Eddie 21:39:19

弘川科技 21:39:53
戈戈和dir分别说说你们遇到的困难,先。
dir 21:40:43
财务?
戈戈 21:40:58
现在就是分销了
弘川科技 21:41:01
都算上。
弘川科技 21:41:07
遇到的具体困难。
dir 21:41:48
流程没找到
弘川科技 21:42:49
嗯。这是一个很严重的问题,在所有的软件开发中,这个东西是最可怕的。
dir 21:43:03
恩.定不下来.
dir 21:43:43
各种知识串不到一起
弘川科技 21:44:02
流程定不下来,那么软件写完以后基本上就不可能完全符合用户的要求。这句话对不对?
弘川科技 21:44:15
串不到一起的原因是因为你写的程序太少。
dir 21:44:20
恩.
dir 21:44:35
 
弘川科技 21:45:52
想想怎么解决这个问题?
弘川科技 21:46:11
脑袋跟着我转,别整别的都。
dir 21:46:22
要慢慢的积累.多动手
弘川科技 21:46:44
戈戈的看法呢?
戈戈 21:46:55
这几天找了很多资料,分销可以做得很大很大。原创那一块具体需要怎样呢
弘川科技 21:47:27
应该很大
弘川科技 21:48:57
这些问题都是很正常的。不仅仅像你们俩刚刚接触软件的人会遇到这样的困难,所有程序员都会碰到。
弘川科技 21:49:05
软件开发永远都是面向未知领域的。
弘川科技 21:49:21
所以,XP敏捷开发所要求的第一条就是勇气。
&水水|Eddie 21:50:04
 
戈戈 21:50:08
时间怎么办
弘川科技 21:50:27
呵呵,问题没那么可怕。
弘川科技 21:50:57
理解代价这个词么?
弘川科技 21:50:52
现在说到时间了,咱们不仅仅谈论时间,应该去谈论代价。
戈戈 21:51:22
不太清楚
弘川科技 21:51:24
说说这个软件开发砸了的话,咱们大伙会付出的代价。
dir 21:51:36

弘川科技 21:52:09
哈,脑袋不开窍,最简单的,MMD,我们家把所有资源都投到公司里面了,如果公司黄了,我没钱娶媳妇。这个代价对我来说最TMD惨痛。
戈戈 21:52:24
 
弘川科技 21:52:23
这个就是代价,你为了获取某些东西而必须付出的东西。
&水水|Eddie 21:52:26
 
弘川科技 21:52:35
包括时间、金钱、经理、感情等等。
&水水|Eddie 21:52:49
MMD,做砸了,我的笔记本白买了
弘川科技 21:53:08
 
弘川科技 21:53:28
所以说,不要仅仅考虑时间因素,要整体考虑。
dir 21:53:29
 
弘川科技 21:54:46
整体考虑的时候,时间就只能作为一个要素了,可能是一个比较关键的要素,但它远远不能代表所有付出的代价。
弘川科技 21:55:03
明白了么?
戈戈 21:55:09

弘川科技 21:55:15
dir呢?
弘川科技 21:55:18
水水呢?
&水水|Eddie 21:55:27
我早都明白
弘川科技 21:55:40
dir?
dir 21:55:43
明白
&水水|Eddie 21:55:54
我相信,也做不砸
dir 21:56:00
我机子越来越慢
弘川科技 21:56:16
 我有信心,肯定做不砸。
弘川科技 21:56:21
应该是把握。
&水水|Eddie 21:56:31
啥时候把机子拿来?
弘川科技 21:56:43
dir和戈戈,你们俩是不是觉得刚才扯了那么多跟今天要讨论的技术没有什么关系?
dir 21:56:46
你有信心就是我们的信心
&水水|Eddie 21:56:52
 
弘川科技 21:57:00
我不是由信心,是有把握。
戈戈 21:57:32
也许吧,我没见过这种场面,所以不好说
&水水|Eddie 21:57:50
肯定砸不了
弘川科技 21:58:21
琢磨琢磨我刚才那句话。
弘川科技 21:58:13
不讨论砸还是不砸呢。
弘川科技 21:58:28
弘川科技(86779411) 22:05:42
dir和戈戈,你们俩是不是觉得刚才扯了那么多跟今天要讨论的技术没有什么关系?
 
dir 21:58:33
所以说流传有信心仅是咱们的信心
弘川科技 21:59:20
 
弘川科技 21:59:26
弘川科技(86779411) 22:07:20
琢磨琢磨我刚才那句话。
弘川科技(86779411) 22:07:27
弘川科技(86779411) 22:05:42
dir和戈戈,你们俩是不是觉得刚才扯了那么多跟今天要讨论的技术没有什么关系?
 
戈戈 21:59:51

dir 22:00:20
开场白么
弘川科技 22:00:49
呵呵,这个不是开场白。是要告诉你们一些道理。
弘川科技 22:01:03
软件开发不仅仅考验的是写程序的能力。
弘川科技 22:01:13
考验的是程序员的综合素质。
弘川科技 22:01:37
勇气、分析问题的能力、解决问题的能力……
弘川科技 22:03:47
这些能理解么?
dir 22:03:59

戈戈 22:04:09
可以
dir 22:04:16
我要没勇气了
&水水|Eddie 22:04:22
……
弘川科技 22:04:40
还有,我可能不止一次跟你们说过这些话,软件开发的最高层次是玩儿哲学。
弘川科技 22:04:46
你为啥要没勇气啊?
dir 22:05:05

&水水|Eddie 22:05:14
这都是你没经验了
dir 22:05:23

&水水|Eddie 22:05:26
乱过去以后就明朗了
弘川科技 22:05:30
这是你做的第一个项目,你要是不觉得乱,我把我楼上的桌子给吃喽。
&水水|Eddie(6072921) 22:05:40
谁都乱过
弘川科技(86779411) 22:14:41
一口一口的咬。
弘川科技(86779411) 22:15:14
如果你不乱,只有三种可能性,第一种,你根本就没投入。
&水水|Eddie(6072921) 22:06:16
咱们一起学的java,我现在乱过来一半了,看到一半的太阳了
弘川科技(86779411) 22:15:19
第二种,你是天才。
弘川科技(86779411) 22:15:26
第三种,你是畜牲。
弘川科技(86779411) 22:15:29
没别的可能性了。
dir(87034226) 22:06:34
是呀,慢慢的履思路呢
弘川科技(86779411) 22:17:48
戈戈,聊聊你写Java程序的体会。
&水水|Eddie(6072921) 22:08:48
多动手,多思考
戈戈(393341021) 22:09:28
到公司还没怎么写过正经东西呢
弘川科技(86779411) 22:18:41
以前。
&水水|Eddie(6072921) 22:10:33
写程序靠思想,java体会更明显
戈戈(393341021) 22:11:14
原来做jsp,swing也都是用面向对象的语言做面向过程的开发
 
弘川科技(86779411) 22:20:30
呵呵,终于认识到了阿。
戈戈(393341021) 22:11:33
比如那个小网站
戈戈(393341021) 22:11:39
是啊
弘川科技(86779411) 22:21:18
都下载完了么?
戈戈(393341021) 22:12:43
可以了
&水水|Eddie(6072921) 22:12:54
呵呵,戈戈 来到公司还真没写程序呢,竞搞业务了
弘川科技(86779411) 22:22:19
上来就写程序那是瞎忙活,做无用功阿。
戈戈(393341021) 22:13:20
革命分工不同嘛
戈戈(393341021) 22:13:35
这点我体会到了
戈戈(393341021) 22:13:51
业务没有白搞
&水水|Eddie(6072921) 22:13:59
 
弘川科技(86779411) 22:23:05
dir体会到了么?
戈戈(393341021) 22:14:06
起码明白了许多东西
&水水|Eddie(6072921) 22:14:19
  现在就是有不明白的问 王铮
弘川科技(86779411) 22:23:34
举个例子。
弘川科技(86779411) 22:23:48
咱们写的程序,咱们认为应该是A+B+C+D
弘川科技(86779411) 22:24:05
给客户看,她说,哇塞,真好。
弘川科技(86779411) 22:24:36
等你给她写完了,TMD她马上告诉你,不对,不对应该是A*B+C*D。
弘川科技(86779411) 22:24:45
到时候你们觉得应该怎么办?
&水水|Eddie(6072921) 22:15:58
  
dir(87034226) 22:16:11
 
弘川科技(86779411) 22:25:22
戈戈的解决方案呢?
戈戈(393341021) 22:16:47
一个字,改
dir(87034226) 22:17:04
我支持.客户就是衣食父母
弘川科技(86779411) 22:26:17
如果TMD程序里面80000个这样的问题呢。 
戈戈(393341021) 22:17:33
赔钱
dir(87034226) 22:17:50
和我想的一样
&水水|Eddie(6072921) 22:17:54
你改完了以后,可能又变了,变成A+B*C+D 了
弘川科技(86779411) 22:27:03
这个是我老师给我讲的例子。
戈戈(393341021) 22:18:04
为什么让他有8000问题呢
&水水|Eddie(6072921) 22:18:23
客户往往是在项目开始的时候,没有任何要求
弘川科技(86779411) 22:27:30
8000个是随口一说,但是有个三五百个应该是非常正常的。
&水水|Eddie(6072921) 22:18:32
他会说:你都做去吧,
&水水|Eddie(6072921) 22:18:46
等你做完了,他的要求也就都出来了。
弘川科技(86779411) 22:28:17
跟你们说说我老师给我提供的答案吧
弘川科技(86779411) 22:28:33
第一个解决方案,杀死她,然后携款潜逃;
弘川科技(86779411) 22:28:47
第二个方案,强奸她,让她变成你的情妇;
弘川科技(86779411) 22:29:05
第三个方案,拒不承认,后面的钱我也不要了;
弘川科技(86779411) 22:29:17
第四个方案,从自己身上找毛病,为什么早没想到。
&水水|Eddie(6072921) 22:20:26
  第二个方案好
戈戈(393341021) 22:20:49
水水,死
弘川科技(86779411) 22:29:50
那行,如果咱们的软件做不好,那个刘志玲就交给你了。
&水水|Eddie(6072921) 22:21:09
刘志玲?
弘川科技(86779411) 22:30:17
长的还不错。
弘川科技(86779411) 22:30:23
就是30出头了。

   您刚才发送的消息:"就是30出头了。 "没有发送成功(服务器超时).

dir(87034226) 22:21:42
有钱
弘川科技(86779411) 22:30:49
就是30出头了。 
dir(87034226) 22:22:16
向钱看
弘川科技(86779411) 22:31:45
其实出现这种问题N正常。
弘川科技(86779411) 22:32:40
你琢磨一下,TMD,客户他在那个职位上工作了多少年了,然后他/她那么多年的经验要我们程序员几个星期内学会,这不是欺负咱们么。但是咱们还必须学会。这个就是做程序员最吸引人也最让人恶心的地方。
戈戈(393341021) 22:24:16
刘川,这也是我心里最想说的,被你说中了
弘川科技(86779411) 22:34:00
但是,这个是你的职业,所以,这些问题你必须得自己解决。
&水水|Eddie(6072921) 22:25:19
充分发挥中国程序员的想象力
弘川科技(86779411) 22:34:38
因此,一方面程序员得拼命的学习业务,另一方面,也得给自己留点儿后手。在写程序的地方做点儿手脚。
弘川科技(86779411) 22:35:05
就上面的那个例子。
弘川科技(86779411) 22:35:30
A+B+C+D,只有刚学会写程序的和傻X才会那么写。
弘川科技(86779411) 22:36:04
一般的程序员会这么写:A运算符B运算符C运算符D
弘川科技(86779411) 22:36:52
水平高点儿的呢,会这么写:运算项目 运算符 运算项目 运算符 运算项目 运算符 运算项目
弘川科技(86779411) 22:37:09
水平再高点儿怎么写。
弘川科技(86779411) 22:37:19
运算项目用I表示,运算符用P表示
弘川科技(86779411) 22:37:25
琢磨琢磨
弘川科技(86779411) 22:37:48
 
弘川科技(86779411) 22:37:55
都睡着了?
戈戈(393341021) 22:29:13
接口嘛
戈戈(393341021) 22:29:15
process
&水水|Eddie(6072921) 22:29:25
看着呢
dir(87034226) 22:29:31

弘川科技(86779411) 22:38:45
我这个东西是表达式计算,跟接口有什么关系?
戈戈(393341021) 22:30:22
就是昨晚说的"一盘散沙"
 
弘川科技(86779411) 22:39:59
水平再高点儿的,应该就会这么写了:List<P> I List<P> I List<P> I List<P> I List<P>
弘川科技(86779411) 22:40:15
他会把括号什么的都考虑进去。
弘川科技(86779411) 22:41:13
还有没有再高点儿的解决方案?
弘川科技(86779411) 22:41:21
谁说说。

   您刚才发送的消息:"还有没有再高点儿的解决方案? "没有发送成功(服务器超时).

弘川科技(86779411) 22:41:43
还有没有再高点儿的解决方案?
dir(87034226) 22:34:31
没.说吧
弘川科技(86779411) 22:43:58
最恰当的解决方案应该是:计算表达式方法(字符串表达式){这个字符串表达式在配制文件里面写好}。这样TMD无论那个傻X怎么改,我就是改一下配制文件,然后OK。
弘川科技(86779411) 22:46:52
public someType processExpression(String strExpressionName , String strResourceLocation)
{
    String expression = loadFromResource(strExpression , strResourceLocation);
    return doProcessExpression(exression);
}
弘川科技(86779411) 22:46:57
如果这样写呢?
弘川科技(86779411) 22:48:04
都能看明白么?
弘川科技(86779411) 22:48:45
说话阿。
&水水|Eddie(6072921) 22:39:54
明白
&水水|Eddie(6072921) 22:40:07
读取资源文件,然后doprocess
dir(87034226) 22:40:21
差不多
戈戈(393341021) 22:40:27

弘川科技(86779411) 22:49:37
这样处理应该是最灵活的。
弘川科技(86779411) 22:50:34
当然它也有它不利的地方,主要是两点:第一个,程序N复杂了;第二个,执行效率要比直接计算A+B+C+D低得多。
&水水|Eddie(6072921) 22:41:54
有利就有弊
&水水|Eddie(6072921) 22:42:08
这个得上升到哲学的角度说去了
弘川科技(86779411) 22:51:32
但是,他获得到了最大的可扩展性和可维护性。而现在,在不太过分的情形下可扩展性和可维护性是最要去追求的目标。
戈戈(393341021) 22:42:34
鱼和熊掌不能兼得,起码要比该80000个强
弘川科技(86779411) 22:51:47
现在的机器都N块了,丧失的这点儿效率,客户根本体会不出来。
弘川科技(86779411) 22:52:33
我刚才说的这些东西,都赞成不?
&水水|Eddie(6072921) 22:43:48

弘川科技(86779411) 22:53:08
我靠。
dir(87034226) 22:44:27
基本,
&水水|Eddie(6072921) 22:44:39
 
弘川科技(86779411) 22:53:51
现在,再说另外一个问题。现在咱们不再讨论表达式了,上升点儿层次。考虑面向对象程序设计中的“类”这个东西。
弘川科技(86779411) 22:55:46
dir写了一个类来计算工人工资,MMD,到了客户那里,客户说那么计算工资的方法不行了,以前他们那么计算,但是现在改了。MMD,dir的类里面被8000个其它模块调用了,然后dir……(选择题:上吊、跳河、撞汽车、割脉……)选一个吧。
弘川科技(86779411) 22:55:57
 
dir(87034226) 22:47:28
撑死我吧
弘川科技(86779411) 22:57:26
然后dir求戈戈陪他一起改,MMD,奋斗了一个星期,改好了,到客户那里,客户告诉你,不好意思啊,我们又改回以前的那种计算方法了,因为另外那种计算方法不是特别合理。这时候,戈戈拿着一把砍刀挥向dir,一场血案就这么发生了。
&水水|Eddie(6072921) 22:48:39
 
戈戈(393341021) 22:49:21
我不砍dir,要砍那个客户
弘川科技(86779411) 22:59:55
ILaborageCalulater
{
    public Currency calculateLaborage(someParams);
}
弘川科技(86779411) 23:00:02
如果dir这么写程序呢?
弘川科技(86779411) 23:00:17
调用的地方都是在调用ILaborageCalulater
弘川科技(86779411) 23:00:44
然后再配置文件里面配置好到底是哪个类来实现ILaborageCalulater接口呢?
弘川科技(86779411) 23:00:55
可能几分钟就改完了。
弘川科技(86779411) 23:01:19
戈戈也不用玩儿砍刀了,不论是砍dir还是砍客户,反正都是给人放血吧?
弘川科技(86779411) 23:01:53
这个例子你们能明白么?
&水水|Eddie(6072921) 22:52:59
呵呵。I know
戈戈(393341021) 22:53:06

弘川科技(86779411) 23:02:15
dir呢?能明白么?
dir(87034226) 22:53:36
叫人砍了
弘川科技(86779411) 23:03:24
 
弘川科技(86779411) 23:03:45
所以说,写程序得好好的动脑筋。
弘川科技(86779411) 23:03:54
这句话你们现在能理解了么?
dir(87034226) 22:55:07

弘川科技(86779411) 23:05:33
下面继续反思一下,刚才的那两个例子,程序分别不同的写法都能完成任务,但是孰优孰劣一眼就能看出来吧?为什么后面的方法要比前面的方法优秀呢?考虑一下,给我个答案。
弘川科技(86779411) 23:07:33
有答案没有?
弘川科技(86779411) 23:08:16
戈戈?
&水水|Eddie(6072921) 22:59:17
这还用说么
&水水|Eddie(6072921) 22:59:22
很明显的
弘川科技(86779411) 23:08:33
让戈戈来说。
&水水|Eddie(6072921) 22:59:34
第二种方法都把程序做活了
&水水|Eddie(6072921) 23:00:06
先说这,我去洗脸
 
戈戈(393341021) 23:00:07
是的,第一种考虑仅仅是功能的实现
戈戈(393341021) 23:00:22
原来我就是这么做得
戈戈(393341021) 23:01:00
单程序不能是这样的
&水水|Eddie(6072921) 23:01:17
程序不是这样编地~~~
戈戈(393341021) 23:01:36
洗脸去
dir(87034226) 23:01:51
一起
弘川科技(86779411) 23:10:57
答案应该是这样的,第一种方法都是面向具体编成,所以它写得程序都是死的,业务逻辑稍加变化,他就得全盘跟着变动;而第二种方法是面向抽象编成,它将程序的复杂性扔到了程序之外(比如说放到配置文件中)或者封装都某一个特定的实现中。这样在业务逻辑发生变化时,它的变动最小。
弘川科技(86779411) 23:11:18
我操,我看黄片
戈戈(393341021) 23:02:22

弘川科技(86779411) 23:11:37
回来说话。


弘川科技(86779411) 23:18:08
回来了么都?
dir(87034226) 23:09:15

戈戈(393341021) 23:10:23
好了
弘川科技(86779411) 23:19:52
所以咱们应该面向抽象编成而不要面向具体编程,对不对?
&水水|Eddie(6072921) 23:11:28
回来了
弘川科技(86779411) 23:20:45
而接口可以被理解为纯虚抽象类,别的婆婆妈妈的理论不提了,就一个面向抽象编程能获得最大的灵活度来说,咱们是不是应该面向接口编程?
dir(87034226) 23:12:47

弘川科技(86779411) 23:21:59
我靠,你们都吱声行不行?我知道你们理解了没有啊?
戈戈(393341021) 23:13:30
继续
&水水|Eddie(6072921) 23:13:39
理解
弘川科技(86779411) 23:23:00
所以咱们公司写程序的时候,强制要求面向接口编程。
&水水|Eddie(6072921) 23:14:19
知道
弘川科技(86779411) 23:23:29
然后切回来,SpringFramework,它的一项N重要的功能就是管理Bean。
dir(87034226) 23:14:32

弘川科技(86779411) 23:23:49
咱们可以把N多JavaBean写到Spring的配置文件中。
&水水|Eddie(6072921) 23:15:18
了解
弘川科技(86779411) 23:24:20
然后通过Spring的ApplicationContext提供的getBean方法就能把被Spring管理的Bean获取出来。
&水水|Eddie(6072921) 23:15:35
WebApplicationContext
弘川科技(86779411) 23:25:02
SomeInterfacetype interfaceType = ApplicationContext.getBean(String strBeanAlias)
弘川科技(86779411) 23:25:42
嗯。WebApplicationContext和ApplicationContext的共同祖先都是BeanFactory
戈戈(393341021) 23:16:44
Spring是不是光是web,application呢
&水水|Eddie(6072921) 23:17:56
<beans>
  <bean id="xxx" class="com.vsoft…."/>
</beans>
弘川科技(86779411) 23:27:54
还有一个原因,就是SpringFramework在生成Bean的时候用的是JDK提供的动态代理。用动态代理生成的Bean不是原来的Bean,而是实现跟你配置的那个接口的一个从天而降的Bean。所以,如果你不用接口类型作为返回值得类型的话,会抱错的。不信的话,你们可以试试。呵呵。