Kyle Marsh
Microsoft Corporation
1999 年 11 月
摘要:讨论使应用程序在 Microsoft(R) Windows(R) 2000 上存在不兼容性的几个问题。其中有以下几部分:
介绍
设置和安装问题
Windows 2000 兼容性问题
应用程序稳定性问题
Windows 平台之间的差异
介绍
几个月来,我一直从事一项任务,即找出 Windows 2000 操作系统中的应用程序兼容性问题。在这里我真正要讨论的是,造成应用程序与 Windows 2000 不兼容的原因。没有人真正关心使应用程序兼容的原因。
我一直在与 Windows 2000 测试组合作,他们在过去的几个月中已测试了数百个应用程序。我们已将应用程序在 Windows 2000 上正常或不正常运行的原因进行书面论述。我们发现的问题可以归为四类:
- 无法在 Windows 2000 上安装的应用程序。 这是迄今我们发现的最大问题。应用程序在 Windows 2000 上安装的方式并无甚特殊之处;问题是这些应用程序不让自己安装到这一新版本的操作系统中。
- 我们对操作系统所做的、影响应用程序运行的更改。每当 Microsoft Windows NT(R) 开发组面临选择,是使系统作为平台更稳定或更强大,还是保障应用程序的兼容性,他们总是牺牲后者而取稳定性。Windows 2000 开发工作的一个主要目标就是让系统作为平台更加稳定。遗憾的是,为了实现这一点而必须进行的某些改动,已导致应用程序在 Windows 2000 上不兼容。
- 我们已对操作系统进行的更改不会影响应用程序的兼容性,但会中断某些应用程序。
- 过于依赖 Windows 9x 平台的应用程序。我们在开发 Windows 2000 时,考虑到有众多 Windows 9x 用户需要升级,因此对 Windows 9x 应用程序进行了测试,将它们移植到 Windows 2000 中。我们发现某些应用程序过于依赖 Windows 9x。
设置和安装问题
我们要讨论的第一类问题是设置和安装问题;最常见的问题无疑是无法在 Windows 2000 上安装应用程序。实际上,导致无法安装应用程序的一个最普遍的原因,在于 Windows 2000 是 Windows NT 的 5.0 版。
测试组以多种方式测试应用程序。他们将应用程序安装在基于 Windows 2000 的系统中,或者将应用程序安装在 Windows NT 4.0 或 Windows 95 中,然后再将系统升级到 Windows 2000,以便进行测试。
我们拿来一台未安装任何操作系统的机器后,安装上 Windows 2000,再安装应用程序,与上述升级的情况相比,前者的兼容性数目要少得多。
版本检查
造成应用程序无法安装在 Windows 2000 上的第一位原因,是它们无法正确处理版本号。我们发现很多应用程序都进行以下示例代码所做的操作。它们在运行过程中会调用 GetVersionEX,然后写下一条“if”语句,该语句规定:“如果系统是版本 3,因为没有新的 Shell,我不能正常运行,所以我可能无法安装。如果系统是版本 4,我可以进行安装和设置”。问题出在如果系统是版本 5,这一“if”语句就没有了下文。因为版本号是 5.0,这些应用程序由于自身原因而无法安装,所以我们发现了一系列这样那样的问题。
if (osvi.dwMajorVersion == 3) { // 请这样做 }else if (osvi.dwMajorVersion == 4) { // 请那样做 }
测试组继续寻找解决方案,并蒙蔽了许多此类应用程序。在早期的编译中,我们能够采取措施改变 GetVersionEx 的返回值。我们可以改变其返回值,欺骗应用程序,告诉它版本号就是 4.0,然后程序就能够继续安装,并正常运行。但有部分应用程序的设计思想就是不能安装在 Windows 2000 上。对于病毒扫描程序或其他低级实用程序来说,受限于某一操作系统版本是可以理解的。不过,这些应用程序会显示消息来说明这一点。我们查找的是那些不能安装或无法正常运行、又根本没有通知用户的应用程序。
怎样才能正确地检查版本号?在 Windows 2000 中我们将添加一个新的 API: VerifyVersionInfo,这一 API 在运行时将依次检查主版本号、次版本号以及服务包。如果出现了操作系统的新版本,应用程序仍然能够在其上安装并运行。实际上应用 VerifyVersionInfo 的选项和方式还有很多,但如果只是检查“要是操作系统升级了,应用程序该如何处理”这一类问题,您只需调用这三个标志,然后检查主版本号、次版本号以及服务包。您能够定义以下语句:“我的程序需要运行在 Windows NT 4.0、SP2 上”,然后询问 VerifyVersionInfo“我正在运行的操作系统是否已达到这一标准?”,该 API 将返回真值或假值。
VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, dwlConditionMask);
采用这一方式检查版本,就可以符合 Windows 2000 应用程序的规范,其基本思想是“只要存在新版本的操作系统,就要在新版本上进行安装。”
应用 VerifyVersionInfo 的一个问题是目前该 API 只能在 Windows 2000 平台上运行。为了检查 Windows 95 等旧平台的版本,您必须应用GetVersionEx。查看以下示例代码,即可发现它的功能与 VerifyVersionInfo 基本相同:依次检查主版本号、次版本号以及服务包。
BOOL bIsWindowsVersionOK(DWORD dwMajor, DWORD dwMinor, DWORD dwSPMajor ) { OSVERSIONINFO osvi; // 初始化 OSVERSIONINFO 结构 // ZeroMemory(&osvi, sizeof(OSVERSIONINFO)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); GetVersionEx((OSVERSIONINFO*)&osvi); // 首先,主版本 if ( osvi.dwMajorVersion > dwMajor ) return TRUE; else if ( osvi.dwMajorVersion == dwMajor ) { // 然后,次版本 if (osvi.dwMinorVersion > dwMinor ) return TRUE; else if (osvi.dwMinorVersion == dwMinor ) { // 对,最好检查一下 Service Pack if ( dwSPMajor && osvi.dwPlatformId == VER_PLATFORM_WIN32_NT ) { HKEY hKey; DWORD dwCSDVersion; DWORD dwSize; BOOL fMeetsSPRequirement = FALSE; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Control\\Windows", 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS) { dwSize = sizeof(dwCSDVersion); if (RegQueryValueEx(hKey, "CSDVersion", NULL, NULL, (unsigned char*)&dwCSDVersion, &dwSize) == ERROR_SUCCESS) { fMeetsSPRequirement = (LOWORD(dwCSDVersion) >= dwSPMajor); } RegCloseKey(hKey); } return fMeetsSPRequirement; } return TRUE; } } return FALSE; }//// 此示例适用于 Windows 2000 和更新版本//BOOL bIsWindowsVersionOK(DWORD dwMajor, DWORD dwMinor, DWORD dwSPMajor ) { OSVERSIONINFOEX osvi; ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); osvi.dwMajorVersion = dwMajor; osvi.dwMinorVersion = dwMinor; osvi.wServicePackMajor = dwSPMajor; // 设置条件掩码。 VER_SET_CONDITION( dwlConditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL ); VER_SET_CONDITION( dwlConditionMask, VER_MINORVERSION, VER_GREATER_EQUAL ); VER_SET_CONDITION( dwlConditionMask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL ); // 执行测试。 return VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR,dwlConditionMask); }
首先,需要检查主版本号。如果当前操作系统的主版本号高于所需主版本号,则不需要进行任何检查,直接向下运行即可。如果主版本号相等,则以同样方式检查次版本号。最后再检查服务包号。在我们获得发布的某一版本时,就可以说“没关系,我们不在乎是 Service Pack 3、4 还是 5”。如果主版本号或次版本号有所增长,系统同样可以处理。我们查找了所有要检查版本信息的应用程序,发现它们将分别检查每一组件。它们在运行中会说“噢,主版本号是 5,我只要求 4,不错。次版本号是 .0,很好,但 Service Pack 是 0,我需要的是 3”。很明显,Windows 2000 还没有推出 SP3,所以这种检查版本信息的方法是错误的。
检查 Windows NT 的版本号时需要注意以下细节:检查版本号和服务包信息的方法有三种。第一种是获取 GetVersionEx 的返回值,然后检查“szCSDVersion”字符串。实际的服务包号嵌入在该字符串的某一位置。分析这一串字符实在是比较繁琐,而且您必须始终牢记它是否进行了本地化,这是很难做到的。这不是一种最好的方式。如果系统运行的是 Windows NT 4.0,您只需检查以下注册键值,其中有一个数字是服务包号。提取这一数字,并进行一个“等于”或“大于”比较即可:
HCLM\System\CurrentControlSet\Control\Windows\CSDVersion
如果您运行的是 Windows 2000 或更高版本,则仍可以使用 GetVersionEx,但需要将其传递到 OSVERSIONINFOEX 结构,而不是正在使用的OSVERSIONINFO 结构。考虑到起始处操作员成员的大小,Windows 2000 会将其视为一个较大的结构,并且给出一个新的字段(服务包、主版本、次版本),作为进行比较时所用的整数。如果您还是使用 VerifyVersionInfo,您会发现这是最方便的一种方式。
DLL 版本检查
在检查 Windows 版本的同时,我们还发现了另一个与版本相关的问题,即用户不检查 DLL 的版本。无论 DLL 是系统目录中的 Microsoft DLL,还是您自己的 DLL,在将其复制到系统前,都必须进行版本检查。检查的目的是防止将旧版的 DLL 复制到新版的上面。
必须确认在用户自己的 DLL 中已添加了版本信息,以便进行检查。进行这一操作非常重要,它能够避免出现各种麻烦。不要试图更改系统目录中的 DLL,甚至不要考虑对系统 DLL 进行升级或降级,或覆盖同一 DLL。系统 DLL 是由 Windows 2000 在 CD 或服务包中提交的、位于系统目录中的 DLL。
如果您打算编制新的 Windows 2000 应用程序,则需要使用 Windows Installer,它能够为您检查 DLL 的版本。只要说明需要将某一特定的 DLL 置于特定的目录中,即可发现 Windows Installer 在为您进行版本检查工作。您不需要再处理这一小块代码。
DLL Hell
如果没有正确地检查 DLL 版本,结果毋庸质疑会发生 DLL Hell。我肯定不需要向您解释什么是 DLL Hell,您一定在这上面花费了不少时间。因为我最初曾参与开发 ctl3d.dll,对这些东西就象熟悉我的邻居一样。
我们花费了几年的时间,努力去突破那些影响 DLL 正常工作的障碍,我们最终认定应用程序开发商无法保持后向兼容性。每个人都在朝这方面努力,这一目标很伟大,但却无法实现。实际上,DLL 不可能保持后向兼容性。结果就是:某一应用程序如果要正常结束,必须取决于某一 DLL 的某个特定版本,而另一应用程序则取决于该 DLL 的另一版本,因为这两个应用程序在这一共享组件(即共享 DLL)方面发生了冲突,导致无法共存于同一系统中。
到目前为止,我们仍然认为对于 Windows 应用程序来说,DLL 共享功能是一个非常良好和重要的组成部分。我们同样认为 DLL 能为您提供的功能还是非常有价值的。问题是如果不能测试 DLL 的版本,而跨应用程序对 DLL 进行全局共享会带来非常多的问题。
并行 DLL
在 Windows 2000 中,我们开始实施某些称之为并行 DLL 的内容。我们希望您开发的应用程序也开始向并行版本策略靠拢。在 Windows 2000 中,我们采取了一些预防性措施,以减少 DLL Hell。我们要考虑的第一位的事情是无论安装了何种应用程序,都要保证系统处于受保护状态,保持其完整性。随后我们将讨论 Windows 文件保护。
我们要做的另一件事情是实现组件的并行,同时希望应用程序供应商也这样做。我们针对的是微软的组件;至于您自己的组件,我们也建议您这样做。我们将采用并行版本功能,而对于您自己目前正在进行全局共享的组件,我们也希望您能采用并行版本功能。
系统稳定性
Windows NT 小组正在努力进行的另一项工作是确保系统保持长时间的稳定。微软允许通过分发服务包的形式将各种功能吸纳到 Windows NT 中,这是一个问题。安装了 Windows NT 的客户和公司在选取服务包时已非常警惕,因为这些东西绝不仅仅是在更正某些问题。通常情况下它会作出某些更正,然后说“噢,我们在这里添加了这个特性,在那里添加了那个功能”,这些东西使得系统无法达到所预期的稳定性。
按照常规策略,服务包只能包含对错误的更正,不能有其他内容。如果我们认为操作系统中需要新增某些重要的特性和功能,我们将推出 Windows 2000 的 .x 版本。您会得到类似 Windows NT 5.1、5.2 等此类内容,另外还有针对 Windows NT 的三个不同版本发布的服务包。我们将继续努力保持每一平台功能的完整性,其中甚至涉及到 QFE、错误检查和 hot fix。每次有了新的功能集或新特性,都会发布新版操作系统。
我们在微软所进行的最后一项工作是确保了解随同产品发布了哪些组件,我们强烈建议您遵照执行。我们正在尽最大可能地减少不同产品中发布的组件的数量。如果某一特定组件需要与另一特定组件协同工作,我们会尽量将这两个组件一同发布。对于所有这些能够重新分发的组件,我们将定出发布的结构顺序。
并行 DLL
如果需要将组件由全局共享组件或 DLL 更改为新的并行 DLL,需要对 DLL 进行某些改动。这种对 DLL 自身的改动是必须的。您必须得声明:“我所设计的组件将允许同时运行多个版本。”
为了使某一组件成为真正的并行组件,首先需要对 DLL 进行重命名,并且更改可能存在于 OCX 控件、COM 对象中的所有 GUID。这种重命名的工作只需进行一次,就能保证您获得一个并行运行的新 DLL,该 DLL 将不再是全局共享。
DLL 被重命名后,应用程序会将其安装到自主管理的目录中,而不会安装到系统目录。这样,应用程序开发人员就可以说:“我已对带有这一 DLL 的特定版本的产品进行了全面测试,而且我还确认在我再次进行测试之前,这一 DLL 不会进行升级。”这就给了做为开发员的您足够的信心:任何人无法使用共享组件扰乱您的应用程序,导致系统崩溃并将我们带回到 DLL Hell。
如果您是作为用户使用这些组件,您可以在自己的目录(而不是系统目录)中注册一个相对路径。这样会加载一个落在系统某处的本地版本,而不是全局副本。
我们对 LoadLibrary 功能进行了修改,从而确保:如果应用程序以相对路径注册了一个组件,我们也始终以相对路径完成加载,而不管这一组件是位于系统目录中,还是运行在别的什么地方。由此可以确保您获得用以测试应用程序的那一份副本。
隔离的应用程序
我们还修改了 LoadLibrary 代码,以便支持 DLL 重定向。由此管理员可以将 DLL 的加载过程重定向到某一位置,并由本地目录加载 DLL。经过这一处理后,您的 DLL 就可以处于隔离状态。假设某一大单位的某个人要测试他们能否采用您的应用程序,他们安装了另一应用程序,然后测试这两者能否协同工作。他们发现结果是不能,管理员就开始查找这两个应用程序在何处,在哪一组件上发生了冲突。找到这一组件后,管理员从同时使用这两个程序的雇员的角度进行了考虑,提取 DLL(或包含对象的 OCX),并将其置于应用程序所在的目录。然后管理员创建了一个名为 foo.exe 的文件,其后又加上 .local。如果调用 LoadLibrary,LoadLibrary 发现这里有一个 foo.exe.local 文件,它会首先加载应用程序目录中的文件,而不会考虑应用程序用于 LoadLibrary 调用本身的特定路径。这种方式有助于人们区分需要同一组件的不同版本的多个应用程序,使所有这些应用程序运行于同一系统中。
Windows 文件保护
为了确保系统的稳定性和平台的可靠性,第一步就是保障系统不会遇到任何 DLL Hell 问题。我们希望无论发生了什么事情,系统仍然能够运行,能够引导,即用户可以对系统的稳定性有充分的信心。
有了 Windows 文件保护 (WFP),如果应用程序试图更改某一系统文件,Windows 2000 会将其恢复原状。对部分功能,应用程序会安装并说:“瞧,我需要这个 DLL 的新版本…”去实现某一功能,或根本这就是一个错误的应用程序,不会正确地执行版本检查功能。Windows 2000 将检查这一点,会发现文件已改动。如果 Windows 2000 发现这是一个系统文件,并声明“我不允许改动这一文件”,它会将文件恢复原状。
如果要升级那些已被系统锁定的文件,只能采用 Windows NT 小组所发放的几种文件替换机制:服务包、QFE 或 hot fix。它们能够实现对系统文件的替换,而其他应用程序却不能。
举个例子,mfc42.dll 是我们锁定的一个文件。通过语言组自身将不能再升级该 DLL,只有 Windows NT 小组能够更改系统中的这一文件。如果 C 程序设计人员需要升级他们的 DLL(而且假定他们希望在 Windows 2000 上市之后而下一版的 Windows NT 发布之前进行升级),只能采用并行的组件版本功能。
大多数 *.sys、*.dll、*.exe 和 *.ocx 文件以及几个字体文件在保护之列。
如此还存在几个兼容性问题。首先,防病毒程序必须认识并正确处理 Windows 文件保护功能,再在此基础上进行应用程序的备份和恢复;不能简单地对这些文件进行复制、备份和恢复。因为您没有系统所支持的文件替换机制,如果您这样做,将取消 Windows 文件保护功能。
为了防止人们进行这类操作,我们在系统中添加了几个 API。
WFP API
第一个 API 是 SFCGetNextProtectedFile。可用这一 API 可以获得所有受保护或能保护的文件的清单。您可以以一个空值开始重复调用这一 API,以获得受保护文件的列表。
BOOL WINAPI SfcGetNextProtectedFile (IN HANDLE RpcHandle,IN PPROTECTED_FILE_DATA ProtFileData );//// 此功能将列出受保护文件//void ListProtectedFiles(HWND hWnd) { HWND hwndList; PROTECTED_FILE_DATA pfd; int iCount; char szFileName[260]; int iLen; RECT rt; hwndList = GetWindow(hWnd,GW_CHILD); if ( hwndList == NULL ) { GetClientRect(hWnd, &rt); // 第一次创建“列表”控件 hwndList = CreateWindow("LISTBOX", NULL, WS_CHILD | WS_VISIBLE | LBS_STANDARD | LBS_NOINTEGRALHEIGHT | LBS_USETABSTOPS, 0,20,rt.right,rt.bottom-40, hWnd, NULL, hInst, NULL); } else SendMessage(hwndList, LB_RESETCONTENT, 0, 0); ZeroMemory(&pfd,sizeof(PROTECTED_FILE_DATA)); iCount = 0; while ( g_pfnSfcGetNextProtectedFile(NULL, &pfd) != 0 ) { // 为此“ANSI 应用程序”将 WCHAR 转换到 ANSI iLen = WideCharToMultiByte(CP_ACP,NULL,pfd.FileName, wcslen(pfd.FileName), szFileName,260,NULL,NULL); szFileName[iLen] = '\0'; SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)szFileName); iCount++; } }
另一个更为直接的 API 是 SfcIsFileProtected。该 API 能更为便捷地为绝大多数应用程序直接调用,并回答以下问题:“看看这一文件,它是否受到保护?”但是请记住,它需要指向这一文件的完整路径。您不能只是简单地指定 NTS.sys,而是需要给出到达 NTS.sys 所处位置的路径。如果您将这一文件名传递给 API,它会说:“是的,这个文件已受到保护”,或“这不是一个受保护的文件”。在您进行任何备份或恢复操作时都需要使用该 API。如果您希望进行任何安装设置,或可能会更新某一系统文件,您都可以调用这一 API。如果您希望将某一目标文件置于系统目录中,在进行这一操作之前,您需要调用这一 API,以避免取消 Windows 文件保护功能。下一版的 Windows Installer(与 Windows 2000 一同发布)将在复制文件之前进行检查,因此不会意外地启动 WFP。
BOOL WINAPI SfcIsFileProtected (IN HANDLE RpcHandle,IN LPCWSTR ProtFileName);//// 此函数使用“文件”打开对话框,以便从用户获取文件名并检查其是否受保护。void CheckFileForProtection(HWND hWnd) { OPENFILENAME OpenFileName; CHAR szFile[MAX_PATH] = "\0"; CHAR szSystem32[MAX_PATH]; strcpy( szFile, ""); ZeroMemory(&OpenFileName, sizeof(OPENFILENAME)); // 填充 OPENFILENAME 结构以支持模板和挂接。 OpenFileName.lStructSize = sizeof(OPENFILENAME); OpenFileName.hwndOwner = hWnd; OpenFileName.hInstance = hInst; OpenFileName.lpstrFile = szFile; OpenFileName.nMaxFile = sizeof(szFile); OpenFileName.lpstrTitle = "Select a File"; OpenFileName.Flags = OFN_FILEMUSTEXIST; if (g_pfnSHGetFolderPath != NULL ) g_pfnSHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, NULL, szSystem32); else szSystem32[0] = '\0'; OpenFileName.lpstrInitialDir = szSystem32; // 调用公共对话函数。 if (GetOpenFileName(&OpenFileName)) { // 检查文件 WCHAR wzFileName[260]; int iLen; iLen = MultiByteToWideChar(CP_ACP,NULL,szFile, strlen(szFile), wzFileName, 260); wzFileName[iLen] = '\0'; if (g_pfnSfcIsFileProtected(NULL, wzFileName) == TRUE ) { MessageBox(hWnd,"Is Protected", szFile, MB_OK); } else MessageBox(hWnd,"Is NOT Protected", szFile, MB_OK); }
组件检查
我们发现导致无法在 Windows 2000 上安装应用程序的另一个原因是组件检查功能。显然,我们操作系统的每一版本都是由多个不同的组件组成。这些组件包括 TAPI、MAPI、Microsoft DirectX(R) 等等。我们发现应用程序会对什么组件处于什么位置,以及是否存在某一组件作出自己的假设。应用程序会因为甲组件存在而假定乙组件也存在,因为某一组件的版本 2 存在而认定另一组件的版本 3 也必然存在。如果您需要用到某一组件,则一定要检查系统中是否存在这一组件,以及它是否位于正确的级别。
除此之外,开发人员还会假设 Windows NT 中没有 DirectX。有时候某一语句为真,而且应用程序在编制时也认为它可能为真。然后在对其进行检查时,因假设 Windows 2000 没有 DirectX,它就会声明:“噢,我无法运行。”但是 Windows 2000 带有 DirectX,这一假设是不正确的。
我们遇到的另一问题是硬编码问题,应用程序如果假定组件所处的位置是错误的,则根本无法对路径进行硬编码。
另一个例子是,Windows 2000 现在包含 TAPI(最新版本为 TAPI 3.0)和 DirectX(最新版本为 7),而不是默认情况下的 MAPI。过去总是认为如果操作系统是 Windows NT,其中应该包含 MAPI。但现在的情况不同了。如果您使用某一组件,则一定要检查这一组件是否存在,而不能根据平台或组件等作出任何假设。
在错误的位置安装文件
我们在安装方面所发现的另一问题是人们放置文件的位置出错。如果您对别人置于某处的某些文件进行升级,那没关系;只要将它们置于用户所希望的位置即可。但有些文件在第一次安装时,需要将其置于“Program Files”目录下。
注意 不一定是 C:\Program Files。有时候是这一目录,有时候则不是。比如在我的机器上就不是。我一般都将 C 分区留给 Windows 98,而将 Windows NT 分区安装到其他分区。
我们希望您尽可能不要将文件置于 Windows 目录,或 System32 等任一子目录。虽然这样做本身没有什么坏处,过去我们也常常希望您如此,因此我们无法完全禁止这一做法,但我们现在希望系统中的所有文件更加有组织一些。我们已经发现,用户对 System32 目录下成百上千的文件深感头痛,对这些文件一无所知,因此也无法删除他们。您每每听到人们这样说:“每年,您都要清理系统,并从头开始重装 Windows”。卸载和清理程序已成为软件市场上最流行的应用程序。我们希望系统保持有序状态,并使操作过程更加简化。
可能的话,我们甚至希望您将某些共享组件也写到别处;Microsoft Visual Basic(R) 在这方面就是一个比较好的例子。许多应用程序都会用到这一组件。旧版 Visual Basic 总是安装在系统目录中,而现在我们将它置于 Program Files 的一个指定文件夹内。
如果要查找文件应该放置的位置,可以调用一个名为 SHGetFolderPath 。SHGetFolderPath 是 Windows 2000 的一个组成部分。它在第二版的 Windows 98 中也已经存在。如果您发现系统中没有这一 API,您可以对S HFolder.dll 进行分发。该 DLL 将解开这一名为 SHGetFolderPath 的新 API 的外壳。这一 API 了解每一特殊文件夹在系统中所处的位置。您可以在 SDK 中找到该 API。它是一个非常长的清单,其中列出了您能够考虑选用的每一个文件夹。
需要注意,旧版的平台只支持以下四个 CSLID。如果您要查找某一特定目录,发现该目录不存在,SHGetFolderPath 能够为您创建该目录,条件是您已指定了这一操作,但在 Windows 95 等后向平台中,这一过程只能对以下四个 CSIDL 有效:
CSIDL_PERSONAL
CSIDL_APPLICATIONDATA
CSIDL_MYPICTURES
CSIDL_LOCAL_APPLICATIONDATA
以下代码示例显示了 SHGetFolderPath 的应用方式;它是一个非常简单易用的 API。有一点需要注意:如果希望代码在所有平台上都能运行(包括 Windows 2000 等自带 SHGetFolderPath 的平台和 Windows 95 等不带 SHGetFolderPath 的平台),应用程序必须与 SHFolder.dll 中的实施过程进行动态链接。
// SHGetFolderPath can work everywhere SHFOLDER is installed. HMODULE hModSHFolder = LoadLibrary("shfolder.dll"); if ( hModSHFolder != NULL ) { (*(FARPROC*)&g_pfnSHGetFolderPath = GetProcAddress(hModSHFolder,"SHGetFolderPathA")); } else g_pfnSHGetFolderPath = NULL; } if (g_pfnSHGetFolderPath != NULL ) g_pfnSHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, NULL, szSystem32); else szSystem32[0] = '\0'; OpenFileName.lpstrInitialDir = szSystem32;
安全性问题
最后,作为设置和安装的最后一个问题,我们将讨论随 Windows 2000 出现的几个安全性问题:
- 高级用户应能安装整个系统范围的应用程序。我们发现有些应用程序坚持只能由管理员进行这类安装。对于锁定的机器,企业仍然希望许多不同用户,或一天中有两三个用户能够进行共享;或者是在特定的某一天,所有雇员都能够使用这一系统。他们只是不希望任何人都能以管理员权限对系统任意更改,为所欲为。许多客户要求高级用户(不仅是管理员)也能够完成应用程序的安装。
- 另一条,所有人都可以安装供自己(而不是系统中的任何人)使用的应用程序。如果我希望玩某个游戏,我就应该能安装该游戏,而且不让系统中的其他任何人使用这一游戏。但请记住,非高级用户不能向 “Program Files” 目录写东西;该用户无法对 HKEY_LOCAL_MACHINE 进行写操作。在安装过程中,您应能打开 HKEY_LOCAL_MACHINE,查看您对它有没有写权限,然后让它作出如下声明:“您不能对该处进行写操作;是否希望这一应用程序只供个人使用?”
- 另一个安全性问题是默认的服务器权限。如果您试图安装服务器应用程序或服务,而使用的是非特权服务帐户(意即,您没有使用本地系统的帐户,也没有使用作为本地管理员组的成员所建立的帐户),则该帐户基本上不可能拥有实际运行服务所需的特权。在 Windows 2000 中,非特权用户(实际上是非特权服务帐户)所拥有的权限与 Windows NT 4.0 中不同。前者拥有的权限较少。例如,非特权用户无法在 Windows 系统目录的任何地方写入东西。如果您拥有一个正在运行的服务器,该服务器试图进行某些操作,或试图安装某些东西,但它可能没有具备适当的权限。如果您正在运行服务器应用程序,请确认它已登录了正确的帐户,并拥有正常运行所需的所有权限。
Windows 2000 兼容性问题
我要讨论的下一个问题被我称之为 Windows 2000 兼容性问题,这里指的是我们对 Windows 平台进行的某些更改,其目的是推动平台不断进步,并实现我们为用户提供更为可靠的平台的目标。这些改动将影响某些应用程序在 Windows 2000 上运行的方式。
设置前台窗口
我们以一个相当简单的操作开始:设置前台窗口。实际上,这一变动始于 Windows 98。不能指望只是简单地由应用程序调用 SetForegroundWindow ,然后您的窗口就能自动地变更为前台窗口。所有这些都是为了防止在任何人希望跳到最前台时出现令人烦恼的选项。您正在兴致勃勃地键入,突然弹出了一个窗口,要请求某种操作。您可能在键入过程中毫无意识地对某些根本不了解的东西作出了肯定的答复。
为了防止出现这种情况,对应用程序何时能成为前台应用程序制订了规则。其实在 Windows 98 中这些规则已经存在:
- 如果您的进程已经是前台进程,则可以取前台窗口。
- 如果您的进程刚刚由前台进程启动,则可以取前台窗口。
- 如果您的进程收到最后一次输入,则会变为前台窗口。
- 如果此时没有前台窗口,则可以取前台窗口。
- 如果正在对前台进程进行调试,则每个人都可以取前台窗口。
- 如果发生了前台超时锁定(某一段时间内前台锁定没有任何操作,而且不会作出响应),那么别人可以取得前台窗口。
- 如果当前任何系统菜单是活动的,则您的应用程序将无法取得前台。这主要是为了防止出现以下令人烦恼的情况:您正在使用“开始”菜单,并沿着它的分支菜单正要启动某个应用程序,突然这些菜单全部消失。 Windows 2000 中新增的这一规则可以防止出现这种情况。
超级隐藏文件
Windows 2000 中增加的另一个新特性称为“超级隐藏文件”(Super Hidden Files)。这里,系统会将几个文件同时标记“系统”和“隐藏”属性。文件仍位于原位置,我们还可以应用这些文件;只是当您进入 Windows 资源管理器之后,这些文件不会显示出来。即使选中“显示隐藏文件”,也无法看到它们。在文件夹的属性列表中有一个新的复选框,该复选框将允许用户看见这些文件,但普通用户不会选中该选项,所以无法看到这些文件。
而且,系统将不再显示 Windows 环境中那些几乎毫无用处的文件,其中绝大多数是基于 MS-DOS(R) 的旧文件以及类似文件。大多数情况下,这对普通用户没有影响;他们所看到的将是一个更简洁的系统。
对于 32 位应用程序来说,这其实不是兼容性问题。应用程序能够在常规的“打开文件”对话框中看到文件,并顺利地打开文件,命令行也依然有效。如果您采用能够查看超级隐藏文件的 Dir /ASH 命令,将会看到所有文件。
唯一存在兼容性问题的是 16 位应用程序,这类程序会碰到一点麻烦。这主要是由调用 MS-DOS 的 INT21 引起。如果您要求系统查找隐藏文件,MS-DOS 的 INT21 只会查找出隐藏的服务文件。
部分被隐藏的文件:
- MS-DOS 系统文件,如 io.dos
- Office 快速查找文件
NetBIOS
NetBIOS 一直是 Windows NT 的一部分。从 Windows 2000 开始,这一情况有所变化。它不是默认配置,用户可能对系统进行设置,使之不加载并不出现 NetBIOS。如果您的应用程序在没有 NetBIOS 的系统中调用使用 NetBIOS 的 API,则这些应用程序将无法继续正常工作,同时将返回错误。例如,如果采用诸如 NetServerEnum 的调用,而运行的系统中没有 NetBIOS,则将返回错误信息。您必须检查所有使用 NetBIOS 调用的地方,确定它们是否发生在没有 NetBIOS 的机器中,并进行正确处理。或者,可以将其替换成非 NetBIOS 调用。请确保您的用户知道您的系统始终需要 NetBIOS,并在安装程序或版本发布说明中明确告知。
需要新的网络 .inf 文件
如果您拥有任一网络设备(如网络驱动程序、传输驱动程序以及某些网络文件打印提供程序等),则您需要确认系统中存在该设备所需的新的网络 .inf 文件,以便支持 Windows 2000 即插即用。无论使用网络设备的系统是从头开始安装,还是由 Windows NT 4.0 升级到 Windows 2000,都要用到这些新文件。因这一格式与 Windows 98 兼容,所以您以前可能用过。无论如何,您都需要立刻将这些文件提供给用户,以便在系统升级到 Windows 2000 后,网络设备仍然能够得到支持。
物理驱动器号
如果您的应用程序需要以低级方式访问硬盘驱动器和卷(如病毒扫描程序),则将需要查找物理驱动器号,这时必须更改查找该号码的方式。过去,您可能使用符号链,该符号链返回的内容类似于:
\Device\HarddiskX\PartitionY
在其中的某个地方您会找到“Harddisk”以及后面的 X,并看出它是硬盘 2 或硬盘 3。而现在,符号链返回的是:
\Device\HarddiskVolumeZ
物理驱动器号将不再出现在这一符号链的任何地方。您需要用一对可用的 IOCTL 来代替。第一个是:
IOCTL_STORAGE_GET_DEVICE_NUMBER
该 IOCTL 只对单个物理驱动器号有效。例如,如果驱动器是 C 驱动器,甚至在一个驱动器上有多个分区,该 IOCTL 都将生效。但针对多卷集合,则需要使用:
IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
该 IOCTL 对于 Windows NT 4.0 同样生效;从符号链中发现物理驱动器号总是有点危险的事情:在可能是多驱动器集的信息中,它们会只告诉您第一个驱动器。
访问磁带驱动器
如果您的应用程序要用到磁带驱动器,则您必须更改访问该磁带驱动器的方式。新的“层次结构存储管理”(Hierarchical Storage Management) 应用了称之为“可移动存储管理器”(Removable Storage Manager) 的工具,其主要操作过程如下:进入服务器并确认某个文件已很长时间未被访问。它会说:“让我们将它转到磁带上,如果某人需要该文件,我们可以将其找回,并让它脱离磁带”。用户将等待稍微长的时间,但可以获取该文件。这样,您可以使用一个似乎空间大得多的小驱动器。
因为“可移动存储管理器”正在服务器上频繁运行,而您的应用程序也在试图访问磁带驱动器,所以,应用程序会发现磁带驱动器总是处于忙碌状态,应用程序将无法控制磁带驱动器。在此讨论如何处理这一问题的篇幅不足。建议您在 Microsoft.com 上访问“Windows NT 5.0 存储应用程序开发过程中应考虑的问题”(Development Considerations for Storage Applications in Windows NT 5.0)。在该文中,您可以大致了解如何处理新的磁带驱动器。在实施这种处理之前,请花点时间浏览 SDK 中的 “可移动存储管理器程序员参考”(Removable Storage Manager Programmer's Reference),以便学习如何编写能与可移动存储管理器共享磁带驱动器的应用程序。
挂接显示驱动程序
如果您的应用程序试图“楔入”显示设备中(例如,您编写了一个显示驱动程序,它会先获得所有调用,然后再将它们移交给原始显示驱动程序),则需要改变操作方式。您已经见过进行这种操作的应用程序(远程控制应用程序即是一个例子),在这些例子中应用程序会取得显示驱动程序命令,并通过线路发送一个调用,然后在本地执行一个调用。如果要在 Windows 2000 中进行这种操作,则必须使用新的"显示驱动程序管理层"(Display Driver Management Level, DDML) 将该输出镜像到远程设备。这将启动多个显示驱动程序,而这正是远程控制应用程序正在进行的操作。这部分文档资料包含在 Windows 2000 Beta 3 版的 DDK 中。
写保护的内核模式
微软还采取了另一项措施增强平台的可靠性:任何运行于内核模式的程序都将在内存中实际拥有写保护区。如果您的设备驱动程序中使用了某些代码段或字符串段落,并且您在已列为只读区域的地方写入了某些临时内容(如注释等),这在 Windows 2000 中将是行不通的。我们不允许内核模式中的任何内容妨碍该处应该具备的保护功能,因为这会导致系统崩溃。
我们发现许多设备驱动程序没有遵守 Windows 2000 的这一规则。通过检查设备驱动程序,系统将判断如果设备驱动程序的设计目标是用于 Windows NT 4.0 而不是 Windows 2000,
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=3207