Windows内核编程学习笔记

来源:互联网 发布:jquery数组清空 编辑:程序博客网 时间:2024/05/22 15:34

1.内核对象:内核对象的数据结构只能由内核访问,应用程序不能在内存中定位这些数据结构和直接改变它们的内容(内核对象通过调用相应的Win32函数产生,并返回一个标识该对象的句柄,注:该句柄与进程相关。每一个内核对象实际上是由内核分配的一块内存,而只能由内核访问。这块内存就是一块数据结构,它包含的成员是关于该对象的信息。内核对象由内核控制何时释放,而不是由调用它的进程,每一个内核对象的数据中都有一个进程引用计数,当某进行创建了一个内核对象时,该内核对象中的引用计数被置为1,之后要是有其它进程访问该内核对象时,引用计数加1,当所有访问内核对象的进程都释放,引用计数为0时,该内核对象由内核释放)

2.用户及图形接口对象:菜单、窗口、鼠标、光标、刷子、字体等对象。可以使用创建对象的函数来区分哪个是内核对象,哪个是用户及图形接口对象,在内核对象创建函数的参数中大多都有LPSECURITY_ATTRIBUTES类型的参数,相反用户对象图形接口对象无此类型参数。

3.内核对象共享:

(1)内核对象的共继承:一个子进程继承父进程中(可继承的)内核对象。如果父进程想指定某个子进程继承它的内核对象该怎么办呢?可以通过函数SetHandleInformation(HANDLE hObject,DWORD dwMask,DWORD dwFlags);来实现。

具体方法:

1.使子进程可以继承父进程的内核对象:SetHandleInformation(hObj,HANDLE_F

LAG_INHERIT,HANDLE_FLAG_INHERIT);//注:这种操作也可以称为打开内核对象

句柄继承标志。

2.子进程不能继承父进程的内核对象方法:SetHandleInformation(hObj,HANDLE

_FLAG_INHERIT,0);//注:这个操作也可以称为关闭内核对象继承标志

另注:判断某个句柄是否可继承方法:

DWORD dwFlags;

GetHandleInformation(hObj,&dwFlags);

BOOL fHandleIsInheritable = (0!=(dwFlags &HANDLE_FLAG_INHERITABLE));

其中函数GetHandleInformation(HANDLE hObj,LPDWORD lpdwFlags)函数是用来返回当前句柄标志到lpdwFlags指向的DWORD中。

(2)命名对象:使用下列函数可以创建一个命名的内核对象,

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpszName);

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,BOOL bManualReset,BOOL bInitialState,LPCTSTR lpszName);

HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,LONG lInitialCount,LONG lMaximumCount,LPCTSTR lpszName);

HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes,BOOL bManualReset,LPCTSTR lpszName);

HANDLE CreateFileMapping(HANDLE hFile,LPSECURITY_ATTRIBUTES lpFileMappingAttributes,DWORD flProtect,DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCTSTR lpszName);

当这些函数的最后一个参数lpszNameNULL时,系统将创建一个无名的内核对象。创建的无名对象,可以通过继承和复制来实现在进程间共享内核对象。lpszName参数:它的最大长度为MAX_PATH(260个字符),且不能包含左斜杠

小常识: 得到执行错误信息可以通过函数GetLastError();

例如:DWORD dwErrorCode = GetLastError();

那么如何使用“命名的内核对象来共享内核对象呢?”

方法及理论实质:通过具体例子说明,假设进程A创建一个内核对象:

HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,"UUXAMutex");之后进程B也同样:HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,"UUXAMutex");创建了一个与进程A相同的内核对象,那么结果进程B是否成功创建内核对象了呢?系统是这样处理这种情况的,首先系统检查是否存在一个名称为"UUXAMutex"的内核对象,经检查后呢,发现已经存在内核对象"UUXAMutex",之后检查内核对象类型,由于进程AB创建的内核对象类型都是“互斥量”,所以系统就认为进程BCreateMutex的调用成功了。系统在进程B的句柄表中找到一个空表项,然后初始化该表项指向已经存在的内核对象。

判断一个进程是否真正的创建了一个新的内核对象可以使用GetLastError来判断。(使用命名的内核对象,可以保证同一个程序只运行一次,如果没有命名,程序可以运行多次)

HANDLE hMutex = CreateMutex(NULL,FALSE,"SomeMutex");

if (GetLastError() == ERROR_ALREADY_EXISTS) {

}else{};

(3)复制对象句柄:通过函数BOOL DuplicateHandle(HANDLE hSourceProcessHandle,HANDLE hSourceHandle,HANDLE hTargetProcessHandle,LPHANDLE lpTargetHandle,DWORD dwDesireAccess,BOOL bInheritHandle,DWORD dwOptions);

参数:dwOptions可以是0或是DUPLICATE_SAME_ACCESSDUPLICATE_CLOSE_SOURCE

举例:如果进程A想让进程B得到自己能够访问的一个内核对象的访问权限,那么该如何做呢?如下所示:

HANDLE hObjProcessA = CreateMutex(NULL,FALSE,NULL);

HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessIdB);

HANDLE hObjProcessB;

DuplicateHandle(GetCurrentProcess(),hObjProcessA,hProcessB,&hObjProcessB,0,FALSE,DUPLICATE_SAME_ACCESS);

CloseHandle(hProcessB);

CloseHandle(hObjProcessA);


 Windows内核编程学习笔记之二“线程“

线程:

 

举一个例子说明一下线程,

有一个电子表格程序,在用户修改了单元格中数据后需要重新进行计算。因为一个复杂的电子表格进行数据运算需要花费很多时间,因此电子表格的重新计算功能应由另一个单独的线程完成,而且该线程的优先级要比主线程的要低。这样当用户敲击键盘时,系统会响应主线程,也就是说系统不会把CPU时间分配给重新计算功能线程。当用户敲击键盘结束时,主线程挂起,继续执行重新计算功能线程,也就是说重新计算功能得到CPU时间。只要用户再次敲击键盘,由于主线程有较高的优先级,就抢占了重新计算的CPU时间。

[@more@]

(1) 何时何地使用线程:在处理复杂并十分耗时的功能时,可以将其单独建立一个线程,这样GUI的主界面就会还可以响应用户,而执行其它功能。

(2) 线程的属性:

1. 线程的栈:每一个线程都会从进程的4GB地址空间中分配了自己的栈。局部变量是存放在线程的栈上的。

2. 线程的CONTEXT结构:每一个线程都有一组自己的CPU寄存器,叫做线程的上下文。CONTEXT结构反应了进程上次执行时CPU寄存器的状态。该结构在WINNT.h文件中定义。当线程使用CPU时,系统使用线程的CONTEXT结构来初始化CPU的寄存器。其中一个CPU寄存器是指令指针,指出线程要执行的下一条CPU指令地址。CPU寄存器中还有一个栈指针,用来存放线程的栈指针。

3. 线程的执行时间:可以使用BOOL GetThreadTimes(HANDLE hThread,LPFILETIME lpCreationTime,LPFILETIME lpExitTime,LPFILETIME lpKernelTime,LPFILETIME lpUserTime);函数来得到一个线程占用CPU的时间。GetThreadTimes函数中各个参数的含义:

CreationTime:线程创建时间。

ExitTime:线程退出时间。

KernelTime:线程用于执行操作系统代码时间。

UserTime:线程用于执行应用程序代码时间。

下面是使用这个函数来判断线程执行时所占用的CPU时间的方法:

__int64 FileTimeToQuadWord(PFILETIME pFileTime);{

__int64 qw;

qw = pFileTime->dwHighDateTime;

qw <<= 32;

qw = pFileTime->dwlowDateTime;

return(qw);

}

PFILETIME QuadWordToFileTime(__int64 qw,PFILETIME pFileTime);{

pFileTime->dwHighDateTime = (DWORD)(qw>>32);

pFileTime->dwLowDateTime = (DWORD)(qw & 0xFFFFFFFF);

return(pFileTime);

}

void Recalc(){

FILETIME ftKernelTimeStart,ftKernelTimeEnd;

FILETIME ftUserTimeStart,ftUserTimeEnd;

FILETIME ftDummy,ftTotalTimeElapsed;

__int64 qwKernelTimeElapsed,qwUserTimeElapsed,qwTotalTimeElapsed;

//得到线程的启动时间。

GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftKernelTimeStart,&ftUserTimeStart);

//

。。。线程在这里执行

//下面得到线程结束时间

GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftKernelTimeEnd,&ftUserTimeEnd);

qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) – FileTimeToQuadWord(&ftKernelTimeStart);

qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) – FileTimeToQuadWord(&ftUserTimeStart);

qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;

QuadWordToFileTime(qwTotalTimeElapsed,&ftTotalTimeElapsed);

}

(3)CreateThread函数

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa,DWORD cbStack,LPTHREAD_START_ROUTINE lpStartAddr,LPVOID lpvThreadParm,DWORD fdwCreate,LPDWORD lpIdThread);

每次调用CreateThread函数,系统都会做这些事:

1. 分配一个线程内核对象来标识和管理新创建的线程。函数CreateThread返回进程内核对象的句柄。

2. 把线程的退出码初始化为STILL_ACTIVE,把线程的挂起计算设置为1

3. 为新线程分配一个上下文结构。

4. 通过保留一块地址空间来准备线程的栈,向该区域提交2页的物理内存,把提交的内存的保护设置为PAGE_READWRITE,在第2页上设置为PAGE_GUARD

5. lpStartAddrlpvThread值被放在栈的顶部,使它们成为传递给StartOfThread的参数。

6. 把线程上下文结构中的栈指针寄存器指向第5步中栈顶的值,把指令指针寄存器指向内部的StartOfThread函数。

CreateThread函数的各个参数说明:

1. Lpsa:该参数是一个指向SECURITY_ATTRIBUTES结构的指针。

2. cbStack:指定线程栈的地址空间大小。每一个线程都有自己的栈。当CreateProcess启动一个应用程序时,它调用CreateThread来初始化进程的主线程。对于参数cbStackCreateThread使用包含在EXE文件中的值。可以使用链接器的/STACK开关来控制该值:

/STACK:[reserve][,commit]

参数reserve设置系统在地址空间中应为该线程保留的大小。缺省值为1Mb。参数commit设置最初应当提交给堆栈的物理内存大小。缺省值为1页。注:当调用CreateThread时,可以把cbStack值设为0,这时CreateThread会使用由链接器嵌入到EXE文件中的commit值为新线程生成一个堆栈。

3. lpStartAddr:指出新进程开始执行时,代码所在函数的地址。

4. fdwCreate:控制进程创建标志。它可以取两个值,一个是0,另一个是CREATE_SUSPENDED,为0时表示:线程立刻开始执行。为CREATE_SUSPENDED时表示:系统创建线程,创建线程的栈,初始化线程的上下文结构中的CPU寄存器,准备开始执行线程函数的第一条指令后挂起,并不开始执行。

5. lpIDThreadCreateProcess函数将在该参数中存放系统分配给线程的ID

(4)线程终止:张程编目方式有三种,它们是:

1.使用ExitThread函数。 (线程的线被释放)

2.在同一进程或另一个进程中的某个线程调用了TerminateThread函数。(线程的栈不被释放)

3.包含该线程的进程终止。

线程终止时所发生的一些事情:

一、线程所拥有的所有用户句柄被释放掉。

二、线程内核对象的状态变为有信号状态。

三、线程的退出码由STILL_ACTIVE变成由ExitThreadTerminateThread的传递的值。

四、如果这个线程是进程中的最后一个活动的线程的话,那么这个进程也终止。

五、线程内核对象引用计数被减1

(5)线程可以使用HANDLE GetCurrentProcess(VOID);函数来得到线程本身所在的进程的句柄。函数GetCurrentProcess得到的只是进程的伪句柄,如果使用CloseHandle函数来关闭用GetCurrnetProcess得到的句柄,CloseHandle会忽略这次操作。不过还是可以通过使用GetCurrentProcess函数传递进程句柄给想要使用进程句柄的函数,如:

 

SetPropertyClass(GetCurrentProcess(),HIGH_PRIORITY_CLASS);//把当前进程的优先级改为HIGH_PRIORITY_CLASS


(6)函数DWORD GetCurrentProcessID(VOID);返回系统唯一标识的进程ID

(7)当调用CreateThread后,新创建的线程句柄将返回给发出调用的线程,但是新创建的线程本身不知道自己的线程句柄是什么,它可以通过使用HANDLE GetCurrentThread(VOID);来得到自己的线程句柄。GetCurrentThread返回的句柄也是一个伪句柄,只能用于当前线程的上下文中。其它情况与GetCurrentProcess相近。

(8)函数 DWORD GetCurrentThreadID(VOID);返回线程ID

[@more@]

(9)将一个进程的伪句柄变成真句柄方法:

HANDLE hProcess;

DuplicateHandle(

GetCurrentProcess(),

GetCurrentProcess(),

GetCurrentProcess(),

&hProcess,

0,

FALSE,

DUPLICATE_SAME_ACCESS);

(10)系统如何调度线程:一个抢先的操作系统必须使用某种算法来决定何时调用哪个线程以及调度多长时间。

一、首先明确一点:系统是根据线程的优先级来调度线程的

二、优先级的范围从031

三、零页线程:优先级为0。该线程的任务是:在系统中其它的线程都不工作时,把系统中所有释放的页的内容设置为0。其它线程的优先级不可能为0

四、线程CPU的分配:当系统向每一个线程分配CPU时,它平等对待处于同一优先级中的线程。就是说,系统简单的把CPU分配给优先级为31的第一个线程。当该线程的时间片结束后,系统把CPU分配给优先级为31的下一个线程,当所有优先级为31的线程都轮了一次之后,系统又把CPU分配给优先级为31的第一个线程。如果对于一个CPU来说,总是有一个优先级为31的线程,那么所有优先级低于31的线程就没有机会分配到CPU,它们将永远不会被执行。这种现象被称为饥饿。当一些线程占用了所有CPU时间,使其它线程就永远不会执行,这时就会发生饥饿。当没有优先级为31的线程时,系统会把CPU分配给优先级为30的线程。当没有优先级为3130的线程时,系统会把CPU分配给优先级为29的线程,如此类推。

五、线程常常没有必要运行,如:进程的主线程调用了GetMessage,而系统发现没有消息,就会挂起线程,撤回该线程剩余的时间片,立即把CPU分配给下一个线程。GetMessage得不到任何消息时,进程的线程就会挂起,CPU不会分配给它。而当有消息被放进线程的队列时,系统就知道这个线程不应该再被挂起了,当高级别的线程不需要CPU时,系统就会把CPU分配给该线程。

 

六、高级别优先级的线程抢低级别的线程CPU

例如:当系统中有一个优先级别为5的线程正在运行,此时系统发现有一个高级别的线程准备运行,系统就会当即挂起低级别的线程,把一个完整的CPU分配给高级别的线程。不管低级别的线程正在做什么,系统都会这样子做。

七、进程的优先级别:Win32支持四种不同的优先级类,它们是:

 

CreateProcess标识

 

级别

 

空闲(IDLE)

IDLE_PRIORITY_CLASS

4

普通(Normal)

NORMAL_PRIORITY_CLASS

8

(HIGH)

HIGH_PRIORITY_CLASS

13

实时(REAL)

REAL_PRIORITY_CLASS

24

对应于各种优先级的在何时使用?如下表格所示:

IDLE_PRIORITY_CLASS

监控应用程序,如:屏幕保护程序。

NORMAL_PRIORITY_CLASS

用户运行的大多数应用程序都属于普通优先级

HIGH_PRIORITY_CLASS

Explorer使用的就是高优先级。大多数时候Explorer的线程被挂起,等待用户按下键盘或鼠标来唤醒。当它的线程被挂起时,系统不会向它们分配CPU,这样低优先级的线程就能运行。一旦用户按下键,系统就会唤醒Explorer的线程。如果有其它低优先级线程在运行,系统就会立即抢先这些线程,让Explorer的线程来运行。实际上就算低优先级的线程陷入了无循环,系统也会让Explorer得到CPU

 

REAL_PRIORITY_CLASS

这个优先级基本上不用,像键盘、鼠标、后台磁盘刷新和Ctrl+Alt+Del等线程的优先级都要低于实时优先级。

该优先级适用于与硬件相关的开发。

 

 

 

八、改变进程的优先级:可以使用BOOL SetPriorityClass(HANDLE hProcess,DWORD fdwPriority);参数说明:hProcess是待改变优先级的进程句柄,fdwPriority是指定的优先级。

九、获得进程优先级:DWORD GetPriorityClass(HANDLE hProcess);

十、命令行中的start命令:以特定的优先级启动应用程序。

例子:打开DOS窗口,输入如下:“C:>start /LOW calc.exe

这个输入的意思是:以低优先级启动计算器。此外还可以以HIGHNORMALREALTIME等共四种优先级启动程序。

 

十一、线程的相对优先级:当线程被创建时,它的优先级是所在进程的优先级。

设置线程优先级的函数BOOL SetPriorityClass(HANDLE hThread,int nPriority);

hThread是线程的句柄,nPriority取下面表格中的值:

标识

描述

THREAD_PRIORITY_LOWEST

该线程的优先级比所属进程的优先级小2

THREAD_PRIORITY_BELOW_NORMAL

该线程的优先级比所属进程的优先级小1

THREAD_PRIORITY_NORMAL

该线程的优先级与所属进程的优先级一样大

THREAD_PRIORITY_ABOVE_NORMAL

该线程的优先级比所属进程的优先级大1

THREAD_PRIORITY_HIGHEST

该线程的优先级比所属进程的优先级大2

THREAD_PRIORITY_IDLE

如果进程的优先级是实时,那么设置该标识后,该线程优先级为16,进程的优先级不是实时情况,设置该标识后,该线程的优先级为1

THREAD_PRIORITY_TIME_CRITICAL

如果进程的优先级为实时,那么线程设置该标识后,线程的优先级为31,如果所属进程的优先级不是实时,那么设置该标识后,线程的优先级为16

 

 

下面表格列出了线程的基本优先级

 

线程相对优先级

对应于进程的优先级

空闲

普通

实时

Time Critical

15

15

15

31

Highest

6

10

15

26

Above Normal

5

9

14

25

Normal

4

8

13

24

Below Normal

3

7

12

23

Lowest

2

6

11

22

Idle

1

1

1

16

 

注意:Win32没有函数返回线程的优先级。

十二、线程优先级的自动提升:可以使用下面的函数来控制是否使用线程的自动提升。

BOOL SetProcessPriorityBoost(HANDLE hProcess,BOOL DisablePriorityBoost);告诉系统恢复或废除一个进程中所有线程的优先级自动提升。

BOOL SetThreadPriorityBoost(HANDLE hThread,BOOL DisablePriorityBoost);告诉系统统恢复或废除对单个线程的提升。

下面的函数用于判断优先级是恢复还是废除状态:

BOOL GetProcessPriorityBoost(HANDLE hProcess,PBOOL pDisablePriorityBoost);

BOOL GetThreadPriorityBoost(HANDLE hThread,PBOOL pDisablePriorityBoost);

十三、线程的挂起和恢复:一线程可以把自己挂起但是不可以把自己恢复。一个线程可以被挂起多次,在WINNT.H文件中MAXIMUM_SUSPEND_COUNT127次。线程被挂起几次,就要被恢复几次,之后才可以被分配CPU,否则不可以。

挂起线程方法:有两种方法可以挂起线程,一、在线程创建时指定CREATE_SUSPENDED标识,二、使用DWORD SuspendThread(HANDLE hThread);

恢复线程方法:DWORD ResumeThread(HANDLE hThread);该方法一定是别的线程调用,也就是说一个挂起的线程不能自己调用ResumeThread函数来恢复自己。ResumeThread函数调用成功返回线程以前的挂起次数,如果失败则返回0xFFFFFFFF

十四、VC++工具PSTAT.Exe可以查看系统中装入的所有进程及进程中包含的所有线程。

十五、线程、进程和C运行时库

1. C的标准运行时库(1970年发明)不支持多线程。

2. 在多线程环境下会发生问题的一些C运行时变量及函数:errno_closeerrnostrtok_wcstokstrerror_strerrortmpnamtmpfileasctime_wasctimegmtime_ecvt_fcvt等。

3. 要想让使用C标准运行时库的CC++程序能正常工作,对于每一个使用C运行时库函数的线程都需要创建和关联一个数据结构。实现方法:利用函数_beginthreadex来代替Win32CreateThread函数来创建线程。(详细内容参见《Windows内核编程第三版》第85页至第87页,书作者:Jeffrey Richer)

0 0
原创粉丝点击