Windows核心编程阅读笔记(1~7章)

第一章

1、获得前一个错误的错误码:

DWORD WINAPI GetLastError(void);

可以使用VS带的工具查看错误码的含义:工具->错误查找

Windows核心编程阅读笔记(1~7章)_第1张图片     

2、在DeBug模式下(F5)可以在Watch窗口中使用$err,hr来显示错误的具体含义



-------------------------------------------------------------------

第二章

1、使用通用的数据类型(TCHAR或PTSTR)来表示文本字符和字符串

2、使用明确的数据类型BYTE或PBYTE来表示字节、字节指针和数据缓冲区

3、使用TEXT或_T宏表示字面常量字符和字符串

4、使用malloc(nCharacter*sizeof(TCHAR)),而不是使用malloc(nCHaracter)

5、使用MultiByteToWideChar和WideCharToMultiByte函数(P26)

6、使用安全的函数处理字符串(_s)如memcpy_s、memmove_s等

7、使用IsTextUnicode来判断字符串是否是UNICODE


------------------------------------------------------------------

第三章、内核对象

在Windows操作系统中我们常常接触的有三种对象类型:

1、Windows内核对象 (事件对象,文件对象,进程对象,I/O完成端口对象,互斥量对象,进程对象,线程对象等等):

         由执行体(Excutive)对象管理器(Object Manager)管理,内核对象结构体保存在系统内存空间(0x80000000-0xFFFFFFFF),句柄值与进程相关。

2、Windows GDI对象 (画笔对象,画刷对象等):

        由Windows子系统管理,句柄值在系统,会话范围 (system-wide /session-wide) 有效。 

3、Windows USER对象 (窗口对象,菜单对象等) :

      由Windows子系统管理,句柄值在系统,会话范围 (system-wide /session-wide) 有效。  

 

一、内核对象

             

           1、每个内核对象都是一个内存块,由操作系统内核分配,并只由操作系统内核访问,该内存块是一个数据结构,其成员维护者与对象相关的信息。

           2、每个内核对象都一个计数器用于记录有多少进程正在使用该内核对象,当计数为0时,操作系统内核将销毁该内核对象。

           3、创建内核对象后,返回一个句柄,该句柄作为进程内核对象表的索引使用,索引句柄是与进程相关的,它只能提供给本进程的所有线程使用,不能在其他进程内使用。

          4、判断对象是否是内核对象:内核对象的创建函数一般都含有一个PSECURITY_ATTRIBUTE参数,而用户对象和GDI对象创建函数不具备该参数。如函数CreateThread、CreateFile、CreateFileMapping、CreateMutex等。

          5、关闭内核对象:BOOL CloseHandle(HANDLE hobject):函数首先检查主调进程的句柄表,确认句柄的有效性,然后系统将获得内核对象的数据结构地址,并将计数器减1,如果计数为0时,操作系统内核将销毁该内核对象。关闭后hobject= NULL(谨记)


二、跨进程边界共享内核对象

 有三种不同机制来允许进程共享内核对象:使用对象句柄继承;为对象命名;复制对象句柄。

A. 对象句柄继承

	//为父进程初始化一个SECURITY_ATTRIBUTES结构体
	SECURITY_ATTRIBUTES sa;
	sa.nLength = sizeof(sa);
	sa.lpSecurityDescriptor = NULL;
	//设置句柄表为可继承的
	sa.bInheritHandle = TRUE;

	HANDLE hFile = CreateFile(TEXT("d:\\win32.txt"),GENERIC_READ|GENERIC_WRITE,0,&sa,OPEN_EXISTING,0,NULL);

	STARTUPINFO si = { sizeof(si) }; 
	PROCESS_INFORMATION pi; 
	//将宽字常量字符串复制到一个临时缓冲区
	TCHAR szCommandLine[] = TEXT("NOTEPAD"); 
	//其中第五个参数为TRUE,也就是从父进程继承句柄表
	CreateProcess(NULL, szCommandLine, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);

B. 对象命名

  1. HANDLE CreateMutex( 
        PSECURITY_ATTRIBUTES psa, 
        BOOL bInitialOwner, 
        PCTSTR pszName); //使用pszName为内核对象命名
  2.  HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj"));
        if (GetLastError() == ERROR_ALREADY_EXISTS) {
       	 // Opened a handle to an existing object.//存在内核对象
       	 // sa.lpSecurityDescriptor and the second parameter
        	// (FALSE) are ignored.
        } else {
       	 // Created a brand new object.
        	// sa.lpSecurityDescriptor and the second parameter
        	// (FALSE) are used to construct the object.
        }
    

    CreateXXX()和OpenXXX()的区别:

    1)、OpenXXX()的第三个参数不能为NULL,只能是一个以0为终止符的字符串

    2)、对象不存在时,CreateXXX()会创建一个,而OpenXXX()只是简单的以调用失败告终

  3. 可以使用命名对象来防止运行一个应用程序的多个实例。P50

    BOOL CPhone_contact_newApp::InitInstance()
    {
    	CreateMutex(NULL,TRUE,AfxGetAppName()); //为内核对象命名
    	if(GetLastError()==ERROR_ALREADY_EXISTS) 
    		return   FALSE; 
    	//.... ... 	
    }

C.  复制对象句柄

 

 ------------------------------------------------------------------

第四章、进程

一、进程

    1、进程是一个运行的程序的一个实例,它由两部分组成:

  • 一个内核对象
  • 一个地址空间

   2、进程实例句柄:

   int APIENTRY _tWinMain(HINSTANCE hInstance,//进程实例句柄
                     HINSTANCE  hPrevInstance,//遗留问题,永远为NULL
                     LPTSTR  lpCmdLine,
                     int  nCmdShow);

       32位Windows中,HMODULE与HINSTANCE完全是一回事。

       进程实例句柄实际是一个内存基地址。可以通过下方法获得主调进程的可执行文件的基地址

       HMODULE hAddr = ::GetModuleHandle(NULL);

   3、进程的命令行

   4、进程的环境块变量:

       1)LPTCH WINAPI GetEnvironmentStrings(void),用完后要FreeEnvironmentStrings

       2)控制台程序,从main参数TCHAR *env[]获得。

       3)子进程可以获得父进程的环境块的一个副本,该副本由子进程专用。

       4)DWORD WINAPI GetEnvironmentVariable:获得某环境变量的值(可用于判断是否存在)P78

       5) BOOL WINAPI SetEnvironmentVariable:添加、修改或删除一个环境变量

   5、进程的关联性

       子进程继承父进程的关联性

   6、进程的错误模式

       UINT SetErrorMode(UINTfuErrorMode)

       子进程继承父进程的错误模式

   7、进程当前所在的驱动器和目录

       DWORD WINAPI GetCurrentDirectory               使用MAX_PATH

       BOOL WINAPI SetCurrentDirectory

        进程中的一个线程更改了当前驱动器或目录,则进程的所有线程来说,这些信息都被改变

二、创建进程

       调用CreateProcess函数创建新的进程时,系统首先创建一个新的进程内核对象,并为新进程创建一个虚拟地址空间,将可执行文件和所有DLL的代码及数据加载到该地址空间。之后系统为新进程创建一个主线程来执行启动例程

       

三、终止进程的方法

       1、主线程的入口点函数返回(推荐)

       2、进程中的一个线程调用ExitProcess函数(避免使用)

       3、另一个进程中的线程调用TerminateProcess函数(避免使用)

 

四、子进程

       在CreateProcess返回后立即关闭到子进程的主线程的内核句柄是一种好的编程习惯:

        PROCESS_INFORMATION pi;
	BOOL fSuccess = ::CreateProcess(...,&pi);
	if (fSuccess){
		::CloseHandle(pi.hThread);
	} 

      假定子进程的主线程生成了另一个线程,主线程终止,此时系统就可以从内存对象中释放子进程的主进程对象,前提是父进程没有打开到这个线程的对象的句柄。如果父线程打开的到子进程的主线程对象的一个句柄,系统就不会释放该主线程的内核对象,除非父进程关闭这个句柄。

  •   等待子进程的结果              
	PROCESS_INFORMATION pi;
	DWORD dwExitCode;
	BOOL fSuccess = ::CreateProcess(...,&pi);
	if (fSuccess){
		::CloseHandle(pi.hThread);

		::WaitForSingleObject(pi.hProcess,INFINITE);//等待子进程返回结果
		::GetExitCodeProcess(pi.hProcess,&dwExitCode);
		::CloseHandle(pi.hProcess);
	}

  •    创建独立运行的子进程:

               

父进程调用CloseHandle来关闭子进程及子进程的主线程的句柄            

	PROCESS_INFORMATION pi;
	BOOL fSuccess = ::CreateProcess(...,&pi);
	if (fSuccess){
		::CloseHandle(pi.hThread);
		::CloseHandle(pi.hProcess);
	}

MFC型的父进程获得控制台型的子进程的输出:(使用匿名管道)

                SECURITY_ATTRIBUTES   sa; 
		HANDLE   hRead,hWrite;      
		sa.nLength   =   sizeof(SECURITY_ATTRIBUTES); 
		sa.lpSecurityDescriptor   =   NULL; 
		sa.bInheritHandle   =   TRUE; 
		if   (!CreatePipe(&hRead,&hWrite,&sa,0))   { //创建匿名管道
			AfxMessageBox( TEXT("Error   On   CreatePipe() ")); 
			return; 
		}   
		STARTUPINFO   si; 
		PROCESS_INFORMATION   pi;   
		si.cb   =   sizeof(STARTUPINFO); 
		GetStartupInfo(&si);   
		si.hStdError   =   hWrite; 
		si.hStdOutput   =   hWrite; 
		//si.wShowWindow   =   SW_HIDE; 
		si.dwFlags   =     STARTF_USESTDHANDLES; 
		if   (!::CreateProcess(TEXT("lip-vireo.exe"),cmdStr.GetBuffer()  , NULL,NULL,TRUE,0,NULL,NULL,&si,π))   { 
			MessageBox( TEXT("Error   on   CreateProcess() ")); 
			return; 
		} 
		CloseHandle(hWrite); 

		char buffer[4096]   =   {0}; 
		DWORD   bytesRead;   
		CString str;
		while   (true)   { 
			if   (ReadFile(hRead,buffer,4096,&bytesRead,NULL)   ==   NULL) 
				break; 
			buffer[bytesRead] = 0;
			str = (TCHAR*)(_bstr_t(buffer));
			AfxMessageBox(str);
			Sleep(200);   
		}

	}

---------------------------------------------------------------------------------------------------

第五章、作业

      Windows提供了一个作业(job)内核对象,它允许将进程组合在一起并创建一个“沙箱”(即施加限制)来限制进程能做什么,相当于进程的容器。

      如果一个进程已与一个作业关联,就无法将其或其子进程从进程中移除。

 1、判断一个进程是否已和一个现有的作业关联:

	BOOL WINAPI IsProcessInJob(
  	__in      HANDLE ProcessHandle, 
  	__in_opt  HANDLE JobHandle, //为NULL则表示所有作业
 	__out     PBOOL Result
);

 2、创建新的作业内核对象:

	HANDLE WINAPI CreateJobObject(
  	__in_opt  LPSECURITY_ATTRIBUTES lpJobAttributes,
  	__in_opt  LPCTSTR lpName
);

 3、关闭一个作业内核对象:

CloseHandle

关闭一个作业对象,不会迫使其所有进程终止运行,作业对象只是被加了一个删除标记,只有在作业中的所有进程都已终止后才会自动销毁。

关闭作业的句柄会导致所有的进程都不可再访问此作业,即使是它依然存在。

 4、对作业中的进程施加限制:

限制类型:

  •  基本限额和扩展基本限额,用于防止作业中的进程独占系统资源
  •  基本的UI限制,用于防止作业中的进程更改用户界面
  • 安全限额,用于防止作业中的进程访问安全资源(文件、注册表子项等)

添加限制的函数:

BOOL WINAPI SetInformationJobObject(
  __in  HANDLE hJob,    //要限制的作业
  __in  JOBOBJECTINFOCLASS JobObjectInfoClass,    //限制类型
  __in  LPVOID lpJobObjectInfo,          //具体限制设置
  __in  DWORD cbJobObjectInfoLength   //lpJobObjectInfo结构大小
);

 5、将进程放入作业中

BOOL bResult = ::CreateProcess(NULL,szCmdLine,NULL,NULL,FALSE,
			CREATE_SUSPENDED | CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi);
//进程创建时还不属于作业,为了防止其脱离沙箱的限制立即执行,要将其SUSPENDED后加入作业:
::AssignProcessToJobObj(hjob,pi.hProcess);
::ResumeThread(pi.hProcess);

1、一旦一个进程已经属于了一个作业的一部分,他就不能再移动到另一个作业中

2、当一个作业的进程产生了子进程,子进程自动成为父进程所属的作业的一部分。这种行为是可以改变的。P131

 6、终止作业中的所有线程

“杀死”作业内部的所有进程:

BOOL WINAPI TerminateJobObject(
  __in  HANDLE hJob,
  __in  UINT uExitCode //nExitCode
);

 7、查询作业的统计信息和限制

BOOL WINAPI QueryInformationJobObject

 8、作业通知

创建一个I/O完成端口内核对象,并将我们的作业与完成端口关联,然后有一个或者多个工作线程等待作业通知到达完成端口,以便对它们进行处理。

一旦创建了I/O端口,就可以调用SetInformationJobObject来将它与一个作业关联起来。

JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
	joacp.CompletionKey = 1;
	joacp.CompletionPort = hIOCP;
::SetInformationJobObject(hJob,JobObjectAssociateCompletionPortInformation,
&joacp,sizeof(joacp));
线程通过调用GetQueuedCompletionStatus来监视完成端口。P136
---------------------------------------------------------------------

第六章、线程基础

1、线程组成:

  • 线程内核对象,操作系统用它操作线程以及存放统计信息
  • 线程栈,用来维护线程执行时所需的所有函数参数和局部变量

2、创建线程:

   HANDLE WINAPI CreateThread(
      __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,  //安全属性,通常为NULL
      __in       SIZE_T dwStackSize,   //线程栈大小
      __in       LPTHREAD_START_ROUTINE lpStartAddress, //线程函数地址
      __in_opt   LPVOID lpParameter, //传递给线程函数的参数
      __in       DWORD dwCreationFlags, //控制创建,0或CREATE_SUSPENDED
      __out_opt  LPDWORD lpThreadId  //存储线程ID
   );

系统从进程的地址空间中分配内存给线程栈使用。新线程在与负责创建的那个线程在相同的进程上下文中运行,因此新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他的线程栈

应该使用_beginthreadex函数来代替CreateThread

3、终止线程

      1)、线程函数返回(推荐)

       2)、线程调用ExitThread函数杀死自己(避免使用)

       3)、另一个进程或同一进程中的线程调用TerminateThread函数(避免使用)

       4)、包含线程的进程终止运行(避免使用)

4、线程内幕

Windows核心编程阅读笔记(1~7章)_第2张图片

线程的上下文反映了当线程上一次执行时,线程的CPU寄存器的状态。线程的CPU寄存器全部保存在一个CONTEXT结构,CONTEXT结构本身保存在线程内核对象中。

5、C/C++运行库注意事项

      在创建线程时,一定不要使用操作系统提供的CreateThread函数,相反的而是调用C/C++运行库函数_beginthreadex

      关于_beginthreadex

  • 每个线程都有自己的专用的_tiddata(一个数据块)内存块,它们从C/C++运行库的堆上分配。
  • 传给_beginthreadex的线程函数和参数以及线程ID、句柄等都保存在_tiddata内存块中。
  •  _beginthreadex函数在内部调用CreateThread函数,CreateThread被调用时传给它的线程函数是_threadstartex,附加参数是_tiddata的地址
  • 创建成功返回线程的句柄,创建失败返回0

关于_threadstartex

  •   _threadstartex将_tiddata内存块与新线程关联在一起
  • 新线程首先执行RtlUserThreadStart,然后在跳转到_threadstartex
  • _threadstartex的唯一参数是新线程的_tiddata内存块的地址
  •  _threadstartex会调用_endthreadex来安全带的退出

6、获得线程或进程的句柄

      HANDLE  GetCurrentProcess();

      HANDLE  GetCurrentThread();

      这两个函数返回主调进程内核对象或线程对象的一个伪句柄(pseudohandle),它们在主调进程句柄表中新建句柄,且调用者两个函数不会影响线程或进程内核对象的使用计数。

    将伪句柄转换为真正的句柄:DuplicateHandle函数,该函数递增了指定内核对象的使用计数,所以在用完复制的对象句柄后要CloseHandle.

7、用户模式下的线程和内核模式下的线程:(《C++多核高级编程》)

          在用户模式下,进程或线程是执行程序或链接库中的指令,它们不对操作系统内核进行任何调用。在内核模式下,进程或线程是在进行系统调用,例如访问资源或抛出异常。同时,在内核模式下,进程或线程可以访问在内核空间中定义的对象。

          用户级线程驻留在用户空间或模式。运行时库管理这些线程,它也位于用户空间。它们对于操作系统是不可见的,因此无法被调度到处理器内核。每个线程并不具有自身的线程上下文。因此,就线程的同时执行而言,任意给定时刻每个进程只能够有一个线程在运行,而且只有一个处理器内核会被分配给该进程。对于一个进程,可能有成千上万个用户级线程,但是它们对系统资源没有影响。运行时库调度并分派这些线程。如同下图中看到的那样,库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种"多对一"的线程映射

Windows核心编程阅读笔记(1~7章)_第3张图片

         内核级线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。这被称作"一对一"线程映射,如下图所示。操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要考虑到数量巨大的用户级线程。您必须了解每个进程允许的线程的最大数目是多少。操作系统为每个线程创建上下文。进程的每个线程在资源可用时都可以被指派到处理器内核。

Windows核心编程阅读笔记(1~7章)_第4张图片
------------------------------------------------------------------

第七章线程调度、优先级和关联性

        上下文切换:Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文中的值载入CPU寄存器的操作。

1、线程的挂起和恢复

  • 线程内核对象中有一个挂起计数,计数器大于零则表示线程被挂起。
  • CreateThread或CreateProcess时,系统创建线程内核对象,并将挂起计速器置为1,在线程初始化好之后,函数查看是否有CREATE_SUSPENDED标志传入,如果没有则将计速器置为0.
  • 可以使用函数ResumeThread函数,使线程可调度,函数返回线程前一个挂起的计数。线程可以被挂起多次,挂起几次,就要利用该函数恢复几次才可调度。

                          DWORD WINAPI ResumeThread(__in  HANDLE hThread);

  •  可以使用SuspendThread函数挂起线程。任何线程都可以使用该函数挂起另一个线程。线程可以挂起自己,但是无法自己恢复。

                        DWORD WINAPI SuspendThread( __in  HANDLE hThread);

              只有在确切的知道目标线程是哪个(或它在做什么),而且采用完备的措施避免出现因挂起线程而引起问题或者死锁的时候,调用SuspendThread才是安全的。

         OpenThread:找到与线程ID匹配的线程内核对象,并将内核对象的使用计数递增1,然后返回对象的句柄。

2、睡眠

          VOID WINAPI Sleep(__in  DWORD dwMilliseconds);

  •   调用Sleep将使线程自动放弃属于它的时间片剩余部分,设置的时间只是近似值。
  •  可以传递0给Sleep,告诉系统主调线程放弃时间片的剩余部分,强制系统调用其他线程,但是系统还可能会重新调度刚调用Sleep的那个线程

3、切换到另一个线程

       BOOL WINAPI SwitchToThread(void);

      调用该函数时,系统查看是否存在正急需CPU时间的饥饿线程,没有则立即返回,存在则调度该线程。与Sleep传入0不同的是该函数允许执行低优先级的线程。

4、线程的执行时间   

BOOL WINAPI GetThreadTimes(
  __in   HANDLE hThread,
  __out  LPFILETIME lpCreationTime,	//创建时间
  __out  LPFILETIME lpExitTime,        //退出时间
  __out  LPFILETIME lpKernelTime,     //内核时间
  __out  LPFILETIME lpUserTime        //用户时间:线程执行应用程序代码所用的时间
);

5、在实际上下文中谈CONTEXT结构

  •  系统使用CONTEXT结构记住线程的状态,线程在下一次获得CPU运行时,可以从上次停止处继续。
  •   CONTEXT结构取决于CPU
  •  上下文分为:用户模式和内核模式
  •  使用GetThreadContext来查看线程内核对象,并获得CPU寄存器状态的集合,调用该函数前需要先调用SuspendThread函数并初始化ContextFlags位。GetThreadContext只能返回线程的用户模式上下文。
        CONTEXTcontext;
        context.ContextFlags = CONTEXT_CONTROL|CONTEXT_INTEGER;//指定需要那种寄存器,
        //CONTEXT_FULL为所有寄存器
        ::GetThreadContext(::GetCurrentThread(),&context);

  • 可以使用SetThreadContext来改变结构中的成员,使用前必须先调用SuspendThread函数并初始化ContextFlags位

6、线程的优先级

  •  每个线程被赋予0~31(高)的优先级数。
  • 饥饿:当较高优先级的线程占用了CPU时间,导致较低优先级的线程无法运行的情况
  •   较高优先级的线程总是会抢占较低优先级的线程,无论较低优先级的线程是否在执行
  •  系统会在启动时创建一个页面清零线程(整个系统中唯一一个优先级为0的线程),负责在没有其他进程执行的时候将系统内的所有闲置页面清零

7、优先级编程

      系统通过线程的相对优先级加上线程所属的进程的优先级来确定线程的优先级值(基本优先级值)。

  •  改变进程的优先级:

BOOL WINAPI SetPriorityClass(

  __in  HANDLE hProcess,     //进程句柄

  __in  DWORD dwPriorityClass//优先级标识,如NORMAL_PRIORITY_CLASS

);

  •  获得进程优先级:DWORD WINAPI GetPriorityClass(__in HANDLE hProcess);
  •  设置线程的相对优先级:

BOOL WINAPI SetThreadPriority(

  __in  HANDLE hThread,

  __in  int nPriority

);

  • 获得线程的相对优先级:int WINAPI GetThreadPriority(__in  HANDLE hThread);

      CreateThread总是创建优先级为normal的线程,要创建其他优先级线程,需先挂起线程:
      DWORD dwThreadID;
      HANDLE hThread=::CreateThread(NULL,0,ThreadFuc,NULL,CREATE_SUSPENDED,&dwThreadID);
      SetThreadPriority(hThread,THREAD_BASE_PRIORITY_IDLE);
      ResumeThread(hThread);
      CloseHandle(hThread);

  • 动态提升线程的优先级值

            系统只提升优先级值在0~15的线程,该范围称为动态优先级范围

             禁止系统对线程进行优先级的动态提升:

             SetProcessPriorityBoost      //禁止提升进程内的所有线程的优先级

              SetThreadPriorityBoost        //禁止提升线程的优先级

         当系统检测到有的线程已经处在饥饿状态3~4秒,它会动态提升线程优先级到15,并允许线程运行两个时间片,运行完后恢复到基本优先级值。









你可能感兴趣的:(编程,windows,null,作业,attributes,winapi)