問:在應用程序嘗試訪問文件時,我收到了拒絕訪問錯誤,原因是其他應用程序正在使用該文件。以前,我使用 Sysinternals (microsoft.com/technet/sysinternals) 的工具來確定是哪個應用程序,但我希望能夠通過編程的方式從我的應用程序中發現這一點。是否能夠通過編程的方式確定哪些進程當前正在使用某個特定文件?
問:在應用程序嘗試訪問文件時,我收到了拒絕訪問錯誤,原因是其他應用程序正在使用該文件。以前,我使用 Sysinternals (microsoft.com/technet/sysinternals) 的工具來確定是哪個應用程序,但我希望能夠通過編程的方式從我的應用程序中發現這一點。是否能夠通過編程的方式確定哪些進程當前正在使用某個特定文件?
答:已經有幾個人問過我這個問題了,但每次都無法回答,因為迄今為止,我還沒有找到答案。您可以通過內核模式驅動程序獲得有關當前由任意進程打開的文件的信息,但是迄今為止還沒有已記錄的用戶模式 Win32® 或 Microsoft® .NET Framework API 可以提供該信息。但是,隨著 Windows Vista™ 的發布,通過一些技巧,可以從用戶模式應用程序中確定此信息。但 API 文檔還沒有給這個方法進行適當地命名。相反,想得稍微遠一點,您會驚奇地發現 Restart Manager API。
答:已經有幾個人問過我這個問題了,但每次都無法回答,因為迄今為止,我還沒有找到答案。您可以通過內核模式驅動程序獲得有關當前由任意進程打開的文件的信息,但是迄今為止還沒有已記錄的用戶模式 Win32® 或 Microsoft® .NET Framework API 可以提供該信息。但是,隨著 Windows Vista™ 的發布,通過一些技巧,可以從用戶模式應用程序中確定此信息。但 API 文檔還沒有給這個方法進行適當地命名。相反,想得稍微遠一點,您會驚奇地發現 Restart Manager API。
Restart Manager API 是 Windows Vista 的新增功能,其目的是為了減少軟件安裝過程中需要系統重新啟動的次數。軟件安裝和升級需要系統重新啟動的主要原因是,正在運行的進程占用著安裝程序需要訪問的文件。Restart Manager API 通過允許安裝程序有秩序地關閉 Restart Manager 發現的占用安裝程序需要訪問的資源的應用程序來解決這一沖突。在此過程中,Restart Manager API 允許安裝程序查詢占用著前面安裝程序需要的資源的進程。然而,最妙的是,任何應用程序都可被視為“安裝程序”。因此,任何應用程序都可以使用這些 Restart Manager API 來查詢正在占用特定資源的其他進程。因此,我可以編寫一個方法(將參數作為到沖突文件的路徑),該方法可以使用 Restart Manager API 收集使用該文件的進程列表。
.NET Framework 3.0 不包含 Restart Manager API 的托管等效項,因此我需要通過神奇的 P/Invoke 提供自己的等效項。我的目的是需要訪問四個函數,從 rstrtmgr.dll 可以訪問所有這四個函數,每個函數的 P/Invoke 定義都顯示在圖 1 中(注意,除了這四個函數之外,Restart Manager API 還包含很多其他函數,但只需這四個函數就能實現我的目的了)。
Figure 1 主要 Restart Manager API 的 P/Invoke 聲明
[DllImport(“rstrtmgr.dll”, CharSet = CharSet.Unicode)] static extern int RmStartSession( out uint pSessionHandle, int dwSessionFlags, string strSessionKey); [DllImport(“rstrtmgr.dll”)] static extern int RmEndSession(uint pSessionHandle); [DllImport(“rstrtmgr.dll”, CharSet = CharSet.Unicode)] static extern int RmRegisterResources(uint pSessionHandle, UInt32 nFiles, string[] rgsFilenames, UInt32 nApplications, [In] RM_UNIQUE_PROCESS[] rgApplications, UInt32 nServices, string[] rgsServiceNames); [DllImport(“rstrtmgr.dll”)] static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons); private const int RmRebootReasonNone = 0; private const int CCH_RM_MAX_APP_NAME = 255; private const int CCH_RM_MAX_SVC_NAME = 63; [StructLayout(LayoutKind.Sequential)] struct RM_UNIQUE_PROCESS { public int dwProcessId; public FILETIME ProcessStartTime; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct RM_PROCESS_INFO { public RM_UNIQUE_PROCESS Process; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] public string strAppName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] public string strServiceShortName; public RM_APP_TYPE ApplicationType; public uint AppStatus; public uint TSSessionId; [MarshalAs(UnmanagedType.Bool)] public bool bRestartable; } enum RM_APP_TYPE { RmUnknownApp = 0, RmMainWindow = 1, RmOtherWindow = 2, RmService = 3, RmExplorer = 4, RmConsole = 5, RmCritical = 1000 }
RmStartSession 函數用於創建一個新的 Restart Manager 會話,RmEndSession 函數用於關閉先前啟動的會話。RmStartSession 向創建的會話返回一個句柄,然後將此會話提供給其他需要引用該會話的函數(包括 RmEndSession)。
我需要的另外兩個函數主要用於處理資源。RmRegisterResources 函數用於指定我的安裝程序需要訪問的文件、應用程序和服務列表,而 RmGetList 函數返回有關正在使用這些資源的其他進程的信息。RmGetList 以 RM_PROCESS_INFO 結構的數組的形式返回該信息,每個數組都包含沖突進程的進程 ID。
有了這些互操作簽名和結構之後,我就可以編寫自己的方法了,如圖 2 所示。我的 GetProcessesUsingFiles 方法接受文件路徑列表並返回 System.Diagnostic.Process 對象列表(表示使用特定路徑中文件的任何進程)。要啟動此過程,該方法會調用 RmStartSession 方法,讓 Restart Manager 知道我們已經做好准備;作為 RmStartSession 的最後一個參數提供的字符串密鑰必須唯一,因此我使用 GUID。
Figure 2 使用 Restart Manager 檢測正在使用的文件
public static IList<Process> GetProcessesUsingFiles( IList<string> filePaths) { uint sessionHandle; List<Process> processes = new List<Process>(); // Create a restart manager session int rv = RmStartSession(out sessionHandle, 0, Guid.NewGuid().ToString(“N”)); if (rv != 0) throw new Win32Exception(); try { // Let the restart manager know what files we’re interested in string[] pathStrings = new string[filePaths.Count]; filePaths.CopyTo(pathStrings, 0); rv = RmRegisterResources(sessionHandle, (uint)pathStrings.Length, pathStrings, 0, null, 0, null); if (rv != 0) throw new Win32Exception(); // Ask the restart manager what other applications // are using those files const int ERROR_MORE_DATA = 234; uint pnProcInfoNeeded = 0, pnProcInfo = 0, lpdwRebootReasons = RmRebootReasonNone; rv = RmGetList(sessionHandle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons); if (rv == ERROR_MORE_DATA) { // Create an array to store the process results RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; pnProcInfo = (uint)processInfo.Length; // Get the list rv = RmGetList(sessionHandle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); if (rv == 0) { // Enumerate all of the results and add them to the // list to be returned for(int i; i<pnProcInfo; i++) { try { processes.Add(Process.GetProcessById( processInfo[i].Process.dwProcessId)); } // in case the process is no longer running catch (ArgumentException) { } } } else throw new Win32Exception(); } else if (rv != 0) throw new Win32Exception(); } // Close the resource manager finally { RmEndSession(sessionHandle); } return processes; }
如果會話啟動成功,我將把用戶提供的文件路徑傳遞到 RmRegisterResources 函數(因為我只關心文件,處理應用程序和服務的參數將被忽略)。注冊資源後,我會調用 RmGetList 函數。函數會接受多個參數,其中包括會話句柄、存儲受影響進程的 out 參數、詳細說明我提供的數組中的元素數的參數、存儲返回的進程信息的數組以及指示為何需要重新啟動的參數(在本示例中,不需要該參數,因此指定了 RmRebootReasonNone)。在調用 RmGetList 時,因為尚不知道會涉及多少進程,所以我將 0 傳遞給 pnProcInfo 參數。如果沒有受影響的進程,RmGetList 將返回 0。如果返回值為 234 或 ERROR_MORE_DATA,則我提供的數組的大小不足以存儲受影響進程的信息,這實際上意味著有受影響的進程(因為我傳遞的數組長度為 0)。現在既然已經知道了受影響的進程數,我會再次調用 RmGetList,這次使用大小合適的數組。假設這次返回值為 0,我會通過返回的 RM_PROCESS_INFO 結構進行循環,使用 Process.GetProcessById 方法和 Process.dwProcessId 從每個結構創建一個可以返回給調用方的列表<進程>集合。
調用 RmGetList 兩次的方法確實有不足之處,但是在希望使用 RmGetList 的模型中,此方法是必需的。在兩次調用中,使用文件的進程數可能發生了改變。所以,即使在第二次調用時,提供的數組的大小也可能不足以處理需要返回的全部信息。在這種情況下,我選擇引發異常,但您可以修改嘗試調用循環中 RmGetList 的方法,直至數組大小合適或達到一定的重試計數。
有了此方法之後,枚舉訪問特定文件的所有進程就成了一個簡單的任務,如以下應用程序所示。這是一個很有用的編譯工具,應將它保留在您的桌面上;如果您在訪問文件時遇到問題,只需將該文件拖放到此應用程序中,它將告訴您哪些應用程序/進程正在使用該文件:
class Program { static void Main(string[] args) { IList<Process> processes = InUseDetection.GetProcessesUsingFiles(args); Console.WriteLine(processes.Count + " processes found:"); foreach (Process p in processes) { Console.WriteLine(p.ProcessName + ": " + p.Id); } Console.ReadLine(); } }
得到這些進程實例後,您可以對找到的要使用您的文件的這些進程進行任何操作(包括收集它們的相關信息,甚至取消這些進程)。但是在關閉這些進程之前,請一定要三思;因為這正是 Restart Manager API 存在的根本原因,它在有秩序地關閉使用您要訪問的資源的其他應用程序的同時,允許它們保存其狀態,因此這些應用程序重新啟動時不會丟失任何數據。Restart Manager 可以關閉應用程序和服務。但是當前,Restart Manager 只能關閉與其會話位於同一個用戶會話的應用程序(因此,例如您運行的應用程序無法使用 Restart Manager 關閉由通過終端服務登錄的另一個用戶運行的應用程序)。最後,請注意此方法將只返回有關保留沖突文件的活動文件句柄的信息;如果某個應用程序打開了一個文件,從中捕獲了一些數據,關閉該文件然後顯示該數據,在您看來好像是應用程序打開了該文件,但該過程沒有顯示出來(當然,此行為是正確的)。
有關使用 Restart Manager API 的詳細信息,請參閱 Daniel Moth 的有關該主題的屏幕演示(位於 9 頻道,網址為 channel9.msdn.com/Showpost.aspx?postid=251492)。
問:我編寫了一個泛型方法,並且知道可以基於為其提供的不同的類型參數對該方法進行多次編譯。我如何確定是否可以用我正在使用的參數重新使用此方法,或者,實際上是否可以為不同的調用重新編譯此方法?
問:我編寫了一個泛型方法,並且知道可以基於為其提供的不同的類型參數對該方法進行多次編譯。我如何確定是否可以用我正在使用的參數重新使用此方法,或者,實際上是否可以為不同的調用重新編譯此方法?
答:簡而言之,在類型參數都是引用類型時,為泛型方法生成的本機代碼可以共享;而只要有類型參數是值類型,就不能共享(但是,這裡有一些微妙之處,有關更多信息,請參閱 blogs.msdn.com/259224.aspx)。但是,如果使用 Visual Studio® 2005,您可以非常方便地確定在什麼時候可以重新使用為您的托管方法生成的本機代碼。只需在方法中設置一個斷點並在運行時打開“寄存器調試”窗口。顯示的寄存器中將有一個是 EIP 寄存器,該寄存器用於存儲要執行的下一條指令的內存地址。每次遇到您的方法中的斷點時,都會檢查 EIP 的值。如果該方法只編譯一次,則在每次記錄 EIP 值時,該值都不會發生變化。而如果該值發生了任何變化,您會知道存在此泛型方法的多個實例。
答:簡而言之,在類型參數都是引用類型時,為泛型方法生成的本機代碼可以共享;而只要有類型參數是值類型,就不能共享(但是,這裡有一些微妙之處,有關更多信息,請參閱 blogs.msdn.com/259224.aspx)。但是,如果使用 Visual Studio® 2005,您可以非常方便地確定在什麼時候可以重新使用為您的托管方法生成的本機代碼。只需在方法中設置一個斷點並在運行時打開“寄存器調試”窗口。顯示的寄存器中將有一個是 EIP 寄存器,該寄存器用於存儲要執行的下一條指令的內存地址。每次遇到您的方法中的斷點時,都會檢查 EIP 的值。如果該方法只編譯一次,則在每次記錄 EIP 值時,該值都不會發生變化。而如果該值發生了任何變化,您會知道存在此泛型方法的多個實例。
圖 3 所示的程序對此進行了演示。我在突出顯示行設置了一個斷點並在 Visual Studio 中運行該應用程序。斷點出現了六次,生成了以下 EIP 值(每次運行可能會有所不同,但是要討論的各個值之間的關系不會改變):
0073123A 00731297 00731297 00731312 00731297 0073123A
Figure 3 測試編譯的泛型方法
class Program { static void Main() { Test.WriteType<int>(); Test.WriteType<string>(); Test.WriteType<object>(); Test.WriteType<float>(); Test.WriteType<Test>(); Test.WriteType<int>(); } class Test { public static void WriteType<T>() { Console.WriteLine(typeof(T).ToString()); } } }
您可以看到,第 2、3 和 5 項的值總是相同,因為它們對應於引用類型參數。第 1 和 6 項相等並與其他的值不同,這是因為使用了相同的值類型。而第 4 項是唯一的,因為它是唯一使用浮點值類型的調用。
當然,此技術只適用於不支持代碼丟棄(釋放 JIT 編譯的代碼的功能)的 JIT 編輯器實現。因此,舉例來說,這對於在 .NET Compact Framework 上運行的代碼是無效的。