今天咱們就聊聊windows中的進程和線程
2016-09-30
在討論windows下的進程和線程時,我們先回顧下通用操作系統的進程和線程。之所以稱之為通用是因為一貫的本科或者其他教材都是這么說的:
1、進程是系統分配資源的最小單位。
2、線程是處理器調度的最小單位。
3、一個進程可以包含很多線程,且這些線程共享進程內的所有資源。
然后又有大致三種線程模型:進程模型、用戶級線程、內核級線程,三種模型如圖所示
把線程模型按嚴格意義上的划分就是這樣,但是事實上操作系統在真正運行過程中使用的要遠比這些復雜的多,但是這些就想事網絡中的OSI模型一樣,雖然在使用的時候有不少問題,但是作為學習仍然是很經典的一種提法。
那么為什么上面三種模型都不能滿足需求呢?
先說進程模型,進程模型下,沒有線程的概念,即不論是資源的分配還是CPU的調度都是基於進程的。這種情況下完成一個很小的任務也需要創建一個進程,但是進程的生命周期從創建到調度再到銷毀需要消耗比較多的資源。為了更加充分的利用資源,這里就提出了線程的概念,在這種情況下,用戶級線程和內核級線程可以說各有優劣。
首先說用戶級線程,CPU的調度和資源的分配仍然是按照進程來走,線程只存在於用戶空間,內核無法感知。這樣用戶就可以根據需要自己定義線程調度算法,線程的切換因為不需要模式的切換,故比較高效。其缺點就是對應用程序的要求比較高,雖然現在高級語言都有線程池的概念,但是這種情況下一個致命缺點就是如果進程內一個線程被阻塞,那么整個進程就會被阻塞,這很顯然無法充分利用多處理器的優勢;而如果一個線程異常,那么整個進程也就完蛋了。這就像一顆老鼠屎,壞了一鍋粥的思想。而內核級線程就不同了,其CPu根據線程進行調度,這樣用戶應用程序就可以專心做自己的事情,不用考慮調度算法。但是這樣用戶就無法自定義調度算法。這種情況下線程的管理全部由內核接管。線程的創建、調度、回收都有內核處理。創建和回收暫且不說,線程的調度由於是內核管理,其必然會涉及到用戶模式到內核模式的切換,而在此被調度就需要從內核模式切換回用戶模式。也就是說一個線程從被運行->就緒->運行,至少需要兩次模式切換,這樣雖然相對於進程來講,可以避免在不同進程切換時更多的上下文的保存(同一進程中的線程切換就可以避免),但是要不就說時代在發展,社會在進步呢,工程師總要一點點榨取CPU以便獲取最大的性能,有句話說沒有最好只有更好,所以即使是這種開銷也是無法忍受的。
windows中的進程 和線程
windows中嚴格意義上來說算是內核級的線程模型。在windows中進程和線程有明確的界限,進程就是進程,只是分配資源的單位,有自己獨立的數據結構。而線程就是線程,作為CPU調度的基本單位,也有自己的數據結構。這點和LInux有着本質的區別, 但是今天我們不討論具體的數據結構!
windows中的進程涉及到兩個結構EPROCESS和KPROCESS兩個結構EPROCESS是執行體層的對象而KPROCESS是內核層的對象。相對應的ETHREAD是執行體對象,而KTHREAD是內核層對象。關於執行體層和內核層,這點參考《windows內核原理與實現》相關章節。
熟悉這些結構的都應該清楚,內核層對象EPROCESS和ETHREAD都是相應執行體對象的首個內嵌結構,即實際上進程和線程的內核層對象和執行體層對象在內核中的地址是相同的。
內核部分實現的基本是和進程或者線程本身相關的屬性,而執行體層更多的注重於管理。
下面還是從線程的創建開始一步步觀察下windows創建線程是如何被一步步建立起來的,並看看線程包含了哪些東西
創建一個線程首先從用戶程序調用用戶函數CreateThread開始,該函數直接調用了函數CreateRemoteThread,CreateRemoteThread函數主要完成以下工作:
a)創建用戶空間堆棧
b)初始化CONTEXT結構體
c)初始化OBJECT_ATTRIBUTES結構體,此結構體在創建線程對象的時候使用。
d)調用NtCreateThread,進入內核空間。
需要注意這里Context結構作為參數傳遞給NtCreateThread,創建系統線程時,該參數為空。用戶線程則在初始化系統堆棧的時候復制Context內容到堆棧。
看NtCreateThread
NTSTATUS NtCreateThread( __out PHANDLE ThreadHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in HANDLE ProcessHandle, __out PCLIENT_ID ClientId, __in PCONTEXT ThreadContext, __in PINITIAL_TEB InitialTeb, __in BOOLEAN CreateSuspended ) /*++ Routine Description: This system service API creates and initializes a thread object. Arguments: ThreadHandle - Returns the handle for the new thread. DesiredAccess - Supplies the desired access modes to the new thread. ObjectAttributes - Supplies the object attributes of the new thread. ProcessHandle - Supplies a handle to the process that the thread is being created within. ClientId - Returns the CLIENT_ID of the new thread. ThreadContext - Supplies an initial context for the new thread. InitialTeb - Supplies the initial contents for the thread's TEB. CreateSuspended - Supplies a value that controls whether or not a thread is created in a suspended state. --*/ { NTSTATUS Status; INITIAL_TEB CapturedInitialTeb; PAGED_CODE(); // // Probe all arguments // try { if (KeGetPreviousMode () != KernelMode) { ProbeForWriteHandle (ThreadHandle); if (ARGUMENT_PRESENT (ClientId)) { ProbeForWriteSmallStructure (ClientId, sizeof (CLIENT_ID), sizeof (ULONG)); } if (ARGUMENT_PRESENT (ThreadContext) ) { ProbeForReadSmallStructure (ThreadContext, sizeof (CONTEXT), CONTEXT_ALIGN); } else { return STATUS_INVALID_PARAMETER; } ProbeForReadSmallStructure (InitialTeb, sizeof (InitialTeb->OldInitialTeb), sizeof (ULONG)); } CapturedInitialTeb.OldInitialTeb = InitialTeb->OldInitialTeb; if (CapturedInitialTeb.OldInitialTeb.OldStackBase == NULL && CapturedInitialTeb.OldInitialTeb.OldStackLimit == NULL) { // // Since the structure size here is less than 64k we don't need to reprobe // CapturedInitialTeb = *InitialTeb; } } except (ExSystemExceptionFilter ()) { return GetExceptionCode (); } Status = PspCreateThread (ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle, NULL, ClientId, ThreadContext, &CapturedInitialTeb, CreateSuspended, NULL, NULL); return Status; }
該函數在做有些常規檢查后就直接調用了另個函數PspCreateThread
NTSTATUS PspCreateThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN HANDLE ProcessHandle, IN PEPROCESS ProcessPointer, OUT PCLIENT_ID ClientId OPTIONAL, IN PCONTEXT ThreadContext OPTIONAL,//用戶空間線程運行上下文 IN PINITIAL_TEB InitialTeb OPTIONAL, IN BOOLEAN CreateSuspended, IN PKSTART_ROUTINE StartRoutine OPTIONAL, IN PVOID StartContext ) /*++ Routine Description: This routine creates and initializes a thread object. It implements the foundation for NtCreateThread and for PsCreateSystemThread. Arguments: ThreadHandle - Returns the handle for the new thread. DesiredAccess - Supplies the desired access modes to the new thread. ObjectAttributes - Supplies the object attributes of the new thread. ProcessHandle - Supplies a handle to the process that the thread is being created within. ClientId - Returns the CLIENT_ID of the new thread. ThreadContext - Supplies a pointer to a context frame that represents the initial user-mode context for a user-mode thread. The absence of this parameter indicates that a system thread is being created. InitialTeb - Supplies the contents of certain fields for the new threads TEB. This parameter is only examined if both a trap and exception frame were specified. CreateSuspended - Supplies a value that controls whether or not a user-mode thread is created in a suspended state. StartRoutine - Supplies the address of the system thread start routine. StartContext - Supplies context for a system thread start routine. --*/ { HANDLE_TABLE_ENTRY CidEntry; NTSTATUS Status; PETHREAD Thread; PETHREAD CurrentThread; PEPROCESS Process; PTEB Teb; KPROCESSOR_MODE PreviousMode; HANDLE LocalThreadHandle; BOOLEAN AccessCheck; BOOLEAN MemoryAllocated; PSECURITY_DESCRIPTOR SecurityDescriptor; SECURITY_SUBJECT_CONTEXT SubjectContext; NTSTATUS accesst; LARGE_INTEGER CreateTime; ULONG OldActiveThreads; PEJOB Job; AUX_ACCESS_DATA AuxData; PACCESS_STATE AccessState; ACCESS_STATE LocalAccessState; PAGED_CODE(); CurrentThread = PsGetCurrentThread (); if (StartRoutine != NULL) { PreviousMode = KernelMode; } else { PreviousMode = KeGetPreviousModeByThread (&CurrentThread->Tcb); } Teb = NULL; Thread = NULL; Process = NULL; if (ProcessHandle != NULL) { // // Process object reference count is biased by one for each thread. // This accounts for the pointer given to the kernel that remains // in effect until the thread terminates (and becomes signaled) // Status = ObReferenceObjectByHandle (ProcessHandle, PROCESS_CREATE_THREAD, PsProcessType, PreviousMode, &Process, NULL); } else { if (StartRoutine != NULL) { ObReferenceObject (ProcessPointer); Process = ProcessPointer; Status = STATUS_SUCCESS; } else { Status = STATUS_INVALID_HANDLE; } } if (!NT_SUCCESS (Status)) { return Status; } // // If the previous mode is user and the target process is the system // process, then the operation cannot be performed. // if ((PreviousMode != KernelMode) && (Process == PsInitialSystemProcess)) { ObDereferenceObject (Process); return STATUS_INVALID_HANDLE; } //創建ethread對象 Status = ObCreateObject (PreviousMode, PsThreadType, ObjectAttributes, PreviousMode, NULL, sizeof(ETHREAD), 0, 0, &Thread); if (!NT_SUCCESS (Status)) { ObDereferenceObject (Process); return Status; } RtlZeroMemory (Thread, sizeof (ETHREAD)); // // Initialize rundown protection for cross thread TEB refs etc. // ExInitializeRundownProtection (&Thread->RundownProtect); // // Assign this thread to the process so that from now on // we don't have to dereference in error paths. // Thread->ThreadsProcess = Process; Thread->Cid.UniqueProcess = Process->UniqueProcessId; CidEntry.Object = Thread; CidEntry.GrantedAccess = 0; //從句柄表中分配一個CidEntry Thread->Cid.UniqueThread = ExCreateHandle (PspCidTable, &CidEntry); if (Thread->Cid.UniqueThread == NULL) { ObDereferenceObject (Thread); return (STATUS_INSUFFICIENT_RESOURCES); } // // Initialize Mm // Thread->ReadClusterSize = MmReadClusterSize; // // Initialize LPC // KeInitializeSemaphore (&Thread->LpcReplySemaphore, 0L, 1L); InitializeListHead (&Thread->LpcReplyChain); // // Initialize Io // InitializeListHead (&Thread->IrpList); // // Initialize Registry // InitializeListHead (&Thread->PostBlockList); // // Initialize the thread lock // PspInitializeThreadLock (Thread); KeInitializeSpinLock (&Thread->ActiveTimerListLock); InitializeListHead (&Thread->ActiveTimerListHead); if (!ExAcquireRundownProtection (&Process->RundownProtect)) { ObDereferenceObject (Thread); return STATUS_PROCESS_IS_TERMINATING; } //如果ThreadContext不為null表明這是一個用戶線程,否則是系統線程 if (ARGUMENT_PRESENT (ThreadContext)) { // // User-mode thread. Create TEB etc // Status = MmCreateTeb (Process, InitialTeb, &Thread->Cid, &Teb); if (!NT_SUCCESS (Status)) { ExReleaseRundownProtection (&Process->RundownProtect); ObDereferenceObject (Thread); return Status; } try { // // Initialize kernel thread object for user mode thread. // Thread->StartAddress = (PVOID)CONTEXT_TO_PROGRAM_COUNTER(ThreadContext); #if defined(_AMD64_) Thread->Win32StartAddress = (PVOID)ThreadContext->Rdx; #elif defined(_X86_) Thread->Win32StartAddress = (PVOID)ThreadContext->Eax; #else #error "no target architecture" #endif } except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); } if (NT_SUCCESS (Status)) { Status = KeInitThread (&Thread->Tcb, NULL, PspUserThreadStartup, (PKSTART_ROUTINE)NULL, Thread->StartAddress, ThreadContext, Teb, &Process->Pcb); } } else { Teb = NULL; // // Set the system thread bit thats kept for all time // PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_SYSTEM); // // Initialize kernel thread object for kernel mode thread. // Thread->StartAddress = (PKSTART_ROUTINE) StartRoutine; Status = KeInitThread (&Thread->Tcb, NULL, PspSystemThreadStartup, StartRoutine, StartContext, NULL, NULL, &Process->Pcb); } if (!NT_SUCCESS (Status)) { if (Teb != NULL) { MmDeleteTeb(Process, Teb); } ExReleaseRundownProtection (&Process->RundownProtect); ObDereferenceObject (Thread); return Status; } PspLockProcessExclusive (Process, CurrentThread); // // Process is exiting or has had delete process called // We check the calling threads termination status so we // abort any thread creates while ExitProcess is being called -- // but the call is blocked only if the new thread would be created // in the terminating thread's process. // if ((Process->Flags&PS_PROCESS_FLAGS_PROCESS_DELETE) != 0 || (((CurrentThread->CrossThreadFlags&PS_CROSS_THREAD_FLAGS_TERMINATED) != 0) && (ThreadContext != NULL) && (THREAD_TO_PROCESS(CurrentThread) == Process))) { PspUnlockProcessExclusive (Process, CurrentThread); KeUninitThread (&Thread->Tcb); if (Teb != NULL) { MmDeleteTeb(Process, Teb); } ExReleaseRundownProtection (&Process->RundownProtect); ObDereferenceObject(Thread); return STATUS_PROCESS_IS_TERMINATING; } OldActiveThreads = Process->ActiveThreads++; InsertTailList (&Process->ThreadListHead, &Thread->ThreadListEntry); KeStartThread (&Thread->Tcb); PspUnlockProcessExclusive (Process, CurrentThread); ExReleaseRundownProtection (&Process->RundownProtect); // // Failures that occur after this point cause the thread to // go through PspExitThread // if (OldActiveThreads == 0) { PERFINFO_PROCESS_CREATE (Process); if (PspCreateProcessNotifyRoutineCount != 0) { ULONG i; PEX_CALLBACK_ROUTINE_BLOCK CallBack; PCREATE_PROCESS_NOTIFY_ROUTINE Rtn; for (i=0; i<PSP_MAX_CREATE_PROCESS_NOTIFY; i++) { CallBack = ExReferenceCallBackBlock (&PspCreateProcessNotifyRoutine[i]); if (CallBack != NULL) { Rtn = (PCREATE_PROCESS_NOTIFY_ROUTINE) ExGetCallBackBlockRoutine (CallBack); Rtn (Process->InheritedFromUniqueProcessId, Process->UniqueProcessId, TRUE); ExDereferenceCallBackBlock (&PspCreateProcessNotifyRoutine[i], CallBack); } } } } // // If the process has a job with a completion port, // AND if the process is really considered to be in the Job, AND // the process has not reported, report in // // This should really be done in add process to job, but can't // in this path because the process's ID isn't assigned until this point // in time // Job = Process->Job; if (Job != NULL && Job->CompletionPort && !(Process->JobStatus & (PS_JOB_STATUS_NOT_REALLY_ACTIVE|PS_JOB_STATUS_NEW_PROCESS_REPORTED))) { PS_SET_BITS (&Process->JobStatus, PS_JOB_STATUS_NEW_PROCESS_REPORTED); KeEnterCriticalRegionThread (&CurrentThread->Tcb); ExAcquireResourceSharedLite (&Job->JobLock, TRUE); if (Job->CompletionPort != NULL) { IoSetIoCompletion (Job->CompletionPort, Job->CompletionKey, (PVOID)Process->UniqueProcessId, STATUS_SUCCESS, JOB_OBJECT_MSG_NEW_PROCESS, FALSE); } ExReleaseResourceLite (&Job->JobLock); KeLeaveCriticalRegionThread (&CurrentThread->Tcb); } PERFINFO_THREAD_CREATE(Thread, InitialTeb); // // Notify registered callout routines of thread creation. // if (PspCreateThreadNotifyRoutineCount != 0) { ULONG i; PEX_CALLBACK_ROUTINE_BLOCK CallBack; PCREATE_THREAD_NOTIFY_ROUTINE Rtn; for (i = 0; i < PSP_MAX_CREATE_THREAD_NOTIFY; i++) { CallBack = ExReferenceCallBackBlock (&PspCreateThreadNotifyRoutine[i]); if (CallBack != NULL) { Rtn = (PCREATE_THREAD_NOTIFY_ROUTINE) ExGetCallBackBlockRoutine (CallBack); Rtn (Thread->Cid.UniqueProcess, Thread->Cid.UniqueThread, TRUE); ExDereferenceCallBackBlock (&PspCreateThreadNotifyRoutine[i], CallBack); } } } // // Reference count of thread is biased once for itself and once for the handle if we create it. // ObReferenceObjectEx (Thread, 2); if (CreateSuspended) { try { KeSuspendThread (&Thread->Tcb); } except ((GetExceptionCode () == STATUS_SUSPEND_COUNT_EXCEEDED)? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { } // // If deletion was started after we suspended then wake up the thread // if (Thread->CrossThreadFlags&PS_CROSS_THREAD_FLAGS_TERMINATED) { KeForceResumeThread (&Thread->Tcb); } } AccessState = NULL; if (!PsUseImpersonationToken) { AccessState = &LocalAccessState; Status = SeCreateAccessStateEx (NULL, ARGUMENT_PRESENT (ThreadContext)?PsGetCurrentProcessByThread (CurrentThread) : Process, AccessState, &AuxData, DesiredAccess, &PsThreadType->TypeInfo.GenericMapping); if (!NT_SUCCESS (Status)) { PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_DEADTHREAD); if (CreateSuspended) { (VOID) KeResumeThread (&Thread->Tcb); } KeReadyThread (&Thread->Tcb); ObDereferenceObjectEx (Thread, 2); return Status; } } Status = ObInsertObject (Thread, AccessState, DesiredAccess, 0, NULL, &LocalThreadHandle); if (AccessState != NULL) { SeDeleteAccessState (AccessState); } if (!NT_SUCCESS (Status)) { // // The insert failed. Terminate the thread. // // // This trick is used so that Dbgk doesn't report // events for dead threads // PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_DEADTHREAD); if (CreateSuspended) { KeResumeThread (&Thread->Tcb); } } else { try { *ThreadHandle = LocalThreadHandle; if (ARGUMENT_PRESENT (ClientId)) { *ClientId = Thread->Cid; } } except(EXCEPTION_EXECUTE_HANDLER) { PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_DEADTHREAD); if (CreateSuspended) { (VOID) KeResumeThread (&Thread->Tcb); } KeReadyThread (&Thread->Tcb); ObDereferenceObject (Thread); ObCloseHandle (LocalThreadHandle, PreviousMode); return GetExceptionCode(); } } KeQuerySystemTime(&CreateTime); ASSERT ((CreateTime.HighPart & 0xf0000000) == 0); PS_SET_THREAD_CREATE_TIME(Thread, CreateTime); if ((Thread->CrossThreadFlags&PS_CROSS_THREAD_FLAGS_DEADTHREAD) == 0) { Status = ObGetObjectSecurity (Thread, &SecurityDescriptor, &MemoryAllocated); if (!NT_SUCCESS (Status)) { // // This trick us used so that Dbgk doesn't report // events for dead threads // PS_SET_BITS (&Thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_DEADTHREAD); if (CreateSuspended) { KeResumeThread(&Thread->Tcb); } KeReadyThread (&Thread->Tcb); ObDereferenceObject (Thread); ObCloseHandle (LocalThreadHandle, PreviousMode); return Status; } // // Compute the subject security context // SubjectContext.ProcessAuditId = Process; SubjectContext.PrimaryToken = PsReferencePrimaryToken(Process); SubjectContext.ClientToken = NULL; AccessCheck = SeAccessCheck (SecurityDescriptor, &SubjectContext, FALSE, MAXIMUM_ALLOWED, 0, NULL, &PsThreadType->TypeInfo.GenericMapping, PreviousMode, &Thread->GrantedAccess, &accesst); PsDereferencePrimaryTokenEx (Process, SubjectContext.PrimaryToken); ObReleaseObjectSecurity (SecurityDescriptor, MemoryAllocated); if (!AccessCheck) { Thread->GrantedAccess = 0; } Thread->GrantedAccess |= (THREAD_TERMINATE | THREAD_SET_INFORMATION | THREAD_QUERY_INFORMATION); } else { Thread->GrantedAccess = THREAD_ALL_ACCESS; } KeReadyThread (&Thread->Tcb); ObDereferenceObject (Thread); return Status; }
首先說下該函數的幾個參數,
ThreadHandle是一個輸出參數,創建成功會包含創建線程的句柄。
DesiredAccess包含了對新線程的訪問權限。
OBjectAttributes是可選參數,代表了線程的屬性。
ProcessHandle指向一個進程的句柄,創建好的線程將運行在該進程的環境中。
ProcessPointer指向所屬進程的Eprocess對象,該參數在創建系統線程時指向PsInitialSystemProcess對象,在創建用戶線程的時候為null。
ClientId返回新線程的ClientID結構。
ThreadCOntext提供了新線程的執行環境,它代表了用戶線程的初始環境,如果為null則表示為系統線程。
InitialTeb參數為新線程的Teb結構提供初始值,系統線程沒有用戶空間堆棧也沒有Teb,所以如果創建的是系統線程,則該參數為null
CreateSuspended參數指明新線程創建后是否被掛起,如果為true,則創建新線程后不會立刻運行,而必須等到顯示調用NtResumeThread函數讓線程運行。
StartRoutine參數指定了系統線程啟動函數地址,所以用戶線程情況下,該參數為null。
StartContext參數指定了系統線程啟動函數執行環境,用戶線程下同樣為null。
下面分析下具體的代碼:
1、創建並初始化EThread對象
之前有提到,Ethread對象時線程的執行體對象,且第一個字段就是Kthread對象,windows采用的內核線程模型,每個線程都有一個這樣的結構。而初始化主要是設置線程的所屬進程,以及設置CID中進程ID,從全局句柄表PspCidTable分配一個可用的項來表示此線程,並返回線程ID,然后就是初始化線程的一些鏈表。
2、判斷參數ThreadContext是否為空。
正如前面所提到的,這里若非空就表示創建的是用戶線程,那么就需要創建一個Teb,這里調用了MmCreateTeb函數。此函數后面再說。然后就是一個try,嘗試為用戶線程初始化內核線程對象Ethread,實際上是初始化了Thread的startaddress為ThreadContext中的RiP,並將ThreadContext中的eax設置到線程的win32StartAddress域,eax保存的是用戶指定的線程執行函數。然后調用KeInitThread函數對線程進行初始化,這里主要就是根據進程的屬性對線程做設置,包括優先級,親和性,serviceTable等,注意這里設置的內核層thread對象Kthread.
3、如果上面參數為空
就表示這是一個內核線程,那么設置TeB=NULL.設置thread->startAddress=參數startRoutine,最后仍然是調用KeInitThread函數初始化線程
4、接下來就設置進程的活動線程數加一,調用InsertTailList把線程假如到進程的線程鏈表中,這里是站在執行體層的角度,操作的是EProcess和Ethread。
5、判斷CreateSuspended參數,為true則把進程掛起。
6、最后經過一系列的設置,如線程的創建時間,引用計數等就調動keReadyThread設置線程就緒態,然后就准備被調度執行了。
其實上面說的還不是很詳細,不少細節都忽略了,但是大體上幾個重要步驟基本就是這樣。理解了這些,windows線程也就基本了解了!!
感覺還是有很多不足的地方,后面再慢慢補充吧,不想寫了!!