閱讀901 返回首頁    go 阿裏雲 go 技術社區[雲棲]


核心編程隨筆5

Note 0:
Windows提供了一個作業(job)內核對象,它允許你將進程組合在一起並創建一個"沙箱"來限製進程能夠做什麼.最好將作業對象想象成一個進程容器.但是,即使作業中隻包含一個進程,也是非常有用的,因為這樣可以對進程施加平時不能施加的限製.
Note 1:
以下的StartRestrictedProcess函數將一個進程放入一個作業中,以限製此進程具體能夠做哪些事情,如下所示:
void StartRestrictedProcess() {
// Check if we are not already associated with a job.
// If this is the case, there is no way to switch to
// another job.
BOOL bInJob = FALSE;
IsProcessInJob(GetCurrentProcess(), NULL, &bInJob);
if (bInJob) {
MessageBox(NULL, TEXT("Process already in a job"),
TEXT(""), MB_ICONINFORMATION | MB_OK);
return;
}
// Create a job kernel object.
HANDLE hjob = CreateJobObject(NULL,
TEXT("Wintellect_RestrictedProcessJob"));
// Place some restrictions on processes in the job.
// First, set some basic restrictions.
JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 };
// The process always runs in the idle priority class.
jobli.PriorityClass = IDLE_PRIORITY_CLASS;
// The job cannot use more than 1 second of CPU time.
jobli.PerJobUserTimeLimit.QuadPart = 10000; // 1 sec in 100-ns intervals
// These are the only 2 restrictions I want placed on the job (process).
jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS
| JOB_OBJECT_LIMIT_JOB_TIME;
SetInformationJobObject(hjob, JobObjectBasicLimitInformation, &jobli,
sizeof(jobli));
// Second, set some UI restrictions.
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
jobuir.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; // A fancy zero
// The process can't log off the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
// The process can't access USER objects (such as other windows)
// in the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;
SetInformationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir,
sizeof(jobuir));
// Spawn the process that is to be in the job.
// Note: You must first spawn the process and then place the process in
// the job. This means that the process’ thread must be initially
// suspended so that it can’t execute any code outside of the job's
// restrictions.
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szCmdLine[8];
_tcscpy_s(szCmdLine, _countof(szCmdLine), TEXT("CMD"));
BOOL bResult =
CreateProcess(
NULL, szCmdLine, NULL, NULL, FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// Place the process in the job.
// Note: If this process spawns any children, the children are
// automatically part of the same job.
AssignProcessToJobObject(hjob, pi.hProcess);
// Now we can allow the child process' thread to execute code.
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
// Wait for the process to terminate or
// for all the job's allotted CPU time to be used.
HANDLE h[2];
h[0] = pi.hProcess;
h[1] = hjob;
DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);
switch (dw – WAIT_OBJECT_0) {
case 0:
// The process has terminated...
break;
case 1:
// All of the job's allotted CPU time was used...
break;
}
FILETIME CreationTime;
FILETIME ExitTime;
FILETIME KernelTime;
FILETIME UserTime;
TCHAR szInfo[MAX_PATH];
GetProcessTimes(pi.hProcess, &CreationTime, &ExitTime,
&KernelTime, &UserTime);
StringCchPrintf(szInfo, _countof(szInfo), TEXT("Kernel = %u | User = %u\n"),
KernelTime.dwLowDateTime / 10000, UserTime.dwLowDateTime / 10000);
MessageBox(GetActiveWindow(), szInfo, TEXT("Restricted Process times"),
MB_ICONINFORMATION | MB_OK);
// Clean up properly.
CloseHandle(pi.hProcess);
CloseHandle(hjob);
}
Note 2:
IsProcessInJob函數可以驗證當前進程是否在一個現有的作業控製之下運行:
BOOL IsProcessInJob(
HANDLE hProcess,
HANDLE hJob,
PBOOL pbInJob);
Note 3:
默認情況下,在Windows Vista中通過Windows資源管理器來啟動一個應用程序時,進程會自動同一個專用的作業關聯,此作業的名稱使用了"PCA"字符串前綴.作業中的一個進程退出時,我們是可以接收到一個通知的.所以,一旦通過Windows資源管理器啟動的一個曆史遺留的程序出現問題,就會觸發Program Compatibility Assistant(程序兼容性助手).
Windows Vista提供這個功能的目的是檢測兼容性問題.所以,如果你已經為應用程序定義了一個清單(manifest),Windows資源管理器就不會將你的進程同"PCA"前綴的作業關聯,它會假定你已經解決了任何可能的兼容性問題.
但是,在需要調試應用程序的時候,如果調試器是從Windows資源管理器啟動的,即使有一個清單(mainifest),應用程序也會從調試器繼承帶有"PCA"前綴的作業.一個簡單的解決方案是從命令行而不是Windows資源管理器中啟動調試器.在這種情況下,不會發生與作業的關聯.
Note 4:
CreateJobObject函數可以用來創建一個新的作業內核對象:
HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);
OpenJobObject函數可以用來打開一個作業內核對象:
HANDLE OpenJobObject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

Note 5:
關閉作業的句柄會導致所有進程都不可訪問此作業,即使這個作業仍然存在.如以下代碼所示:
// Create a named job object.
HANDLE hJob = CreateJobObject(NULL, TEXT("Jeff"));
// Put our own process in the job.
AssignProcessToJobObject(hJob, GetCurrentProcess());
// Closing the job does not kill our process or the job.
// But the name ("Jeff") is immediately disassociated with the job.
CloseHandle(hJob);
// Try to open the existing job.
hJob = OpenJobObject(JOB_OBJECT_ALL_ACCESS, FALSE, TEXT("Jeff"));
// OpenJobObject fails and returns NULL here because the name ("Jeff")
// was disassociated from the job when CloseHandle was called.
// There is no way to get a handle to this job now.
Note 6:
創建好一個作業之後,接著一般需要限製作業中的進程能做的事情;換言之,現在要設置一個"沙箱".可以向作業應用以下幾種類型的限製:
基本限製和擴展基本限製,防止作業中的進程獨占係統資源.
基本的UI限製,防止作業內的進程更改用戶界麵.
安全限製,防止作業內的進程訪問安全資源(文件、注冊表子項等).
Note 7:
SetInformationJobObject函數可以向作業應用限製:
BOOL SetInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pJobObjectInformation,
DWORD cbJobObjectInformationSize);

限製類型              
   限製                   第二個參數的值                                 第三個參數的結構
類型
基本限製               JobObjectBasicLimitInformation         JOBOBJECT_BASIC_LIMIT_INFORMATION
擴展後的基本限製 JobObjectExtendedLimitInformation JOBOBJECT_EXTENDED_LIMIT_INFORMATION
基本的UI限製        JobObjectBasicUIRestrictions             JOBOBJECT_BASIC_UI_RESTRICTIONS
安全限製              JobObjectSecurityLimitInformation    JOBOBJECT_SECURITY_LIMIT_INFORMATION
Note 8:
針對作業對象基本用戶界麵限製的位標誌
標誌                                                              描述
JOB_OBJECT_UILIMIT_EXITWINDOWS            阻止進程通過ExitWindowsEx函數注銷、關機、重啟或斷開係統電源.
JOB_OBJECT_UILIMIT_READCLIPBOARD        阻止進程讀取剪貼板中的內容
JOB_OBJECT_UILIMIT_WRITECLIPBOARD        阻止進程清除剪貼板中的內容
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS   阻止進程通過SystemParametersInfo 函數更改係統參數
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS        阻止進程通過ChangeDisplaySettings函數更改顯示設置
JOB_OBJECT_UILIMIT_GLOBALATOMS          為作業指定其專有的全局atom表,並規定作業中的進程隻能訪問作業的表
JOB_OBJECT_UILIMIT_DESKTOP                    阻止進程使用CreateDesktop或SwitchDesktop函數來創建或切換桌麵
JOB_OBJECT_UILIMIT_HANDLES                   阻止作業中的進程使用同一個作業外部的進程所創建的USER對象(比如HWND)
Note 9:
有時需要讓作業內部的一個進程同作業外部的一個進程通信.一個簡單的辦法是使用窗口消息.但是,如果作業中的進程不能訪問UI句柄,那麼作業內部的進程就不能向作業外部的進程創建的一個窗口發送或張貼窗口消息.幸運的是,可以用另一個函數來解決這個問題,如下所示:
BOOL UserHandleGrantAccess(
HANDLE hUserObj,
HANDLE hJob,
BOOL bGrant);
Note 10:
QueryInformationJobObject函數能查詢對作業施加的限製:
BOOL QueryInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pvJobObjectInformation,
DWORD cbJobObjectInformationSize,
PDWORD pdwReturnSize);
向此函數傳遞作業的句柄(這類似於SetInformationJobObject函數)——這是一個枚舉類型,它指出了你希望哪些限製信息,要由函數初始化的數據結構的地址,以及包含該數據結構的數據塊的大小.最後一個參數是pdwReturnSize,它指向由此函數來填充的一個DWORD,指出緩衝區中已填充了多少個字節.如果對這個信息不在意,可以(通常也會)為此參數傳遞一個NULL值.

作業中的進程可以調用QueryInformationJobObject獲得其所屬作業的相關信息(為作業句柄參數傳遞NULL值).這是很有用的一個技術,因為它使進程能看到自己被施加了哪些限製.不過,如果為作業句柄參數傳遞NULL值,SetInformationJobObject函數調用會失敗——目的是防止進程刪除施加於自己身上的限製.
Note 11:
AssignProcessToJobObject函數可以將進程顯式地放入我新建的作業中:
BOOL AssignProcessToJobObject(
HANDLE hJob,
HANDLE hProcess);
這個函數隻允許將尚未分配給任何作業的一個進程分配給一個作業,你可以使用IsProcessInJob函數對此進行檢查.
Note 12:
一旦進程已經屬於作業的一部分,它就不能再移動到另一個作業中,也不能成為所謂"無作業"的.還要注意,當作業中的一個進程生成了另一個進程的時候,新進程將自動成為父進程所屬於的作業的一部分.但可以通過以下方式改變這種行為:
1.打開JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成員的JOB_OBJECT_LIMIT_BREAKAWAY_OK標誌,告訴係統新生成的進程可以在作業外部執行.為此,必須在調用CreateProcess函數時指定新的CREATE_BREAKAWAY_FROM_JOB標誌.如果這樣做了,但作業並沒有打開JOB_OBJECT_LIMIT_BREAKAWAY_OK限製標誌,CreateProcess調用就會失敗.如果希望由新生成的進程來控製作業,這就是非常有用的一個機製.
2.打開JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成員的JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK標誌.此標誌也告訴係統新生成的子進程不是作業的一部分.但是,現在就沒有必要向CreateProcess函數傳遞任何額外的標誌.事實上,此標誌會強製新進程脫離當前作業.如果進程在設計之初對作業對象一無所知,這個標誌就相當有用.

在調用了AssignProcessToJobObject之後,新進程就成為受限製的作業的一部分.然後,調用ResumeThread,使進程的線程可以在作業的限製下執行代碼.
Note 13:
Visual Studio沒有一個簡單的辦法來停止正在進行的一次生成,因為它必須知道哪些進程是從它生成的第一個進程生成的.(這非常難.在Microsoft Systems Journal 1998年6月期的Win 32 Q&A專欄討論
過Developer Studio是如何做到這一點的,可以通過以下網址找到這篇文章:https://www.microsoft.com/msj/0698/win320698.aspx.)也許Visual Studio未來的版本會轉而使用作業,因為這樣一來,代碼的編寫會變得更容易,而且可以用作業來做更多的事情.
Note 14:
TerminateJobObject函數可以殺死作業內部的所有進程:
BOOL TerminateJobObject(
HANDLE hJob,
UINT uExitCode);
這類似於為作業內的每一個進程調用TerminateProcess,將所有退出代碼設為uExitCode.
Note 15:
GetProcessIoCounters函數可以獲得沒有放入作業的那些進程的信息,如下所示:
BOOL GetProcessIoCounters(
HANDLE hProcess,
PIO_COUNTERS pIoCounters);

Note 16:
任何時候都可以調用QueryInformationJobObject,獲得作業中當前正在運行的所有進程的進程ID集.為此,必須首先估算一下作業中有多少個進程,然後,分配一個足夠大的內存塊來容納由這些進程ID構成的一個數組,另加一個JOBOBJECT_BASIC_PROCESS_ID_LIST結構的大小:
typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST {
DWORD NumberOfAssignedProcesses;
DWORD NumberOfProcessIdsInList;
DWORD ProcessIdList[1];
} JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;
所以,為了獲得作業中當前的進程ID集,必須執行以下類似的代碼:
void EnumProcessIdsInJob(HANDLE hjob) {
// I assume that there will never be more
// than 10 processes in this job.
#define MAX_PROCESS_IDS 10
// Calculate the number of bytes needed for structure & process IDs.
DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) +
(MAX_PROCESS_IDS – 1) * sizeof(DWORD);
// Allocate the block of memory.
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil =
(PJOBOBJECT_BASIC_PROCESS_ID_LIST)_alloca(cb);
// Tell the function the maximum number of processes
// that we allocated space for.
pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS;
// Request the current set of process IDs.
QueryInformationJobObject(hjob, JobObjectBasicProcessIdList,
pjobpil, cb, &cb);
// Enumerate the process IDs.
for (DWORD x = 0; x < pjobpil->NumberOfProcessIdsInList; x++) {
// Use pjobpil->ProcessIdList[x]...
}
// Since _alloca was used to allocate the memory,
// we don’t need to free it here.
}
這就是使用這些函數所能獲得的所有信息.
Note 17:
操作係統保存著很多與作業相關的信息.這是通過性能計數器來實現的,可以使用Performance Data Helper函數庫(PDH.dll)中的函數來獲取這些信息.還可以使用Reliability and Performance Monitor("可靠性和性能監視器",在"管理工具"中打開)來查看作業信息.但是,這樣隻能看到全局命名的作業對象.不過,利用Sysinternals的Process Explorer(https://www.microsoft.com/technet/sysinternals/ProcessesAndThreads/ProcessExplorer.mspx),可以很好地觀察作業.默認情況下
作業限製下的所有進程都用棕色來突出顯示.
Note 18:
作業中的進程如果尚未用完已分配的CPU時間,作業對象就是nonsignaled(無信號)的.一旦用完所有已分配的CPU時間,Windows就會強行殺死作業中的所有進程,作業對象的狀態會變成signaled(有信號).通過調用WaitForSingleObject(或者一個類似的函數),可以輕鬆捕捉到這個事件.順便提一句,可以調用SetInformationJobObject並授予作業更多的CPU時間,將作業對象重置為原來的nonsignaled狀態.
Note 19:
Microsoft選擇在已分配的CPU時間到期時,才將作業的狀態變成signaled.在許多作業中,都會有一個父進程一直在運行,直至其所有子進程全部結束.所以,我們可以隻等待父進程的句柄,借此得知整個作業何時結束.
Note 20:
如何獲得一些"高級"的通知(比如進程創建/終止運行).要獲得這些額外的通知,必須在自己的應用程序中建立更多的基礎結構.具體來講,你必須創建一個I/O完成端口(completion port)內核對象,並將自己的作業對象與完成端口關聯.然後,必須有一個或者多個線程在完成端口上等待並處理作業通知.
Note 21:
一旦創建了I/O完成端口,就可以調用SetInformationJobObject將它與一個作業關聯起來,如下所示:
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
joacp.CompletionKey = 1; // Any value to uniquely identify this job
joacp.CompletionPort = hIOCP; // Handle of completion port that
// receives notifications
SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation,
&joacp, sizeof(jaocp));
執行上述代碼後,係統將監視作業,隻要有事件發生,就會把它們post到I/O完成端口(順便提一句,你可以調用QueryInformationJobObject來獲取completion key和完成端口句柄,但很少有必要這樣做).線程通過調用GetQueuedCompletionStatus來監視完成端口:
BOOL GetQueuedCompletionStatus(
HANDLE hIOCP,
PDWORD pNumBytesTransferred,
PULONG_PTR pCompletionKey,
POVERLAPPED *pOverlapped,
DWORD dwMilliseconds);
Note 22:
在默認情況下,作業對象是這樣配置的:當作業已分配的CPU時間到期時,它的所有進程都會自動終止,而且不會post JOB_OBJECT_MSG_END_OF_JOB_TIME這個通知.如果你想阻止作業對象殺死進程,隻是簡單地通知你CPU時間到期,就必須像下麵這樣執行代碼:
// Create a JOBOBJECT_END_OF_JOB_TIME_INFORMATION structure
// and initialize its only member.
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB;
// Tell the job object what we want it to do when the job time is
// exceeded.
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation,
&joeojti, sizeof(joeojti));
針對EndOfJobTimeAction,你惟一能指定的另一個值就是JOB_OBJECT_TERMINATE_AT_END_OF_JOB.這是創建作業時的默認值.
Note 23:
使用psapi.h中的函數(比如GetModuleFileNameEx和GetProcessImageFileName),可以根據進程ID來獲得進程的完整路徑名稱.但是,當作業收到通知,知道一個新進程在它的限製下創建時,前一個函數調用會失敗.因為在這個時候,地址空間尚未完全初始化:模塊尚未與它建立映射.GetProcessImageFileName則很有意思,因為即使在那種極端情況下,它也能獲取完整路徑名.但是,路徑名的格式近似於內核模式(而不是用戶模式)中看到的格式.例如,是\Device\HarddiskVolume1\Windows\System32\notepad.exe,而不是C:\Windows\System32\notepad.exe.這就是你應該依賴於新的QueryFullProcessImageName函數的
原因.該函數在任何情況下都會返回你預期的完整路徑名.

最後更新:2017-04-03 15:21:56

  上一篇:go 網絡子係統11_arp子係統初始化
  下一篇:go 核心編程隨筆4