第一章 勿在浮沙筑高台
摘要:win32、console、进程、线程、优先级
Windows SDK程序开发流程(32位):
Windows程序调用的函数可分为C Runtimes和Windows API两大部分。CRuntimes支持动态连接,LIBC.LIB、MSVCRT.LIB;Windows API由操作系统本身(主要是Windows三大模块GDI32.DLL和USER32.DLL和KERNEL32.DLL提供);
所有的Windows程序都必须包含WINDOWS.H头文件。WINDOWS.H只照顾三大模块所提供的API函数。
所有的GUI系统,都是以消息为基础的事件驱动系统。
Windows程序的本体与操作系统之间的关系:
Makefile,使某个文件和某个文件相比,比较其产生日期,只要右边的任一文件比左边的文件新,就执行下一行所指定的动作。例如:
Generic.res : generic.rc generic.h
Rc generic.rc
程序的进入点WinMain:当Windows的shell侦测到使用者欲执行一个Windows程序,于是调用加载器把该程序加载,然后调用C startup code,后者再调用WinMain,开始执行程序,WinMain的四个参数由操纵系统传递进来。
程序必须在产生窗口之前利用API函数RegisterClass设定属性:
CreateWindow只产生窗口,并不显示窗口,必须利用ShowWindow将之显示在屏幕上。
一般的编程习惯,RegisterClass包装在InitApplication函数中,CreateWindow则包装在InitInstance函数中。
消息循环:
While(GetMessage(&msg,…) {
TranslateMessage(&msg); //转换键盘消息
DispatchMessage(&msg); //把消息传给窗口函数去处理不用指定函数名称
}
DispatchMessage经过USER模块的协助,才能把消息交到窗口函数手中。
窗口函数:
通常使用switch/case方式判断消息种类。它是被Windows系统所调用的,是一种call back函数。函数虽然由你设计,但是永远不会也不该被你调用,它们是为Windows系统准备的,它能开放出一个接口给操作系统调用,窗口函数的形式,相当一致:
LRESULT CALLBACK WndProc(HWND hWnd,UINTmessage,WPARAM wParam,LPARAM lParam)
不论什么消息,都必须被处理,所以switch/case指令中的default:处必须调用DefWindowProc,这是Windows内部预设的消息处理函数,wParam和lParam的意义,因消息的不同而异。
消息映射(Message Map)的雏形:
首先,定义一个MEGMAP_ENTRY结构和一个dim宏:
Struct MSGMAP_ENTRY {
UINT nMessage;
LONG(*pfn)(HWND,UINT,WPARAM,LPARAM);//指向处理消息的函数指针
}
#define dim(x) (sizeof(x)/sizeof(x[0]))
接着,组织两个数组_messageEntries[]和_commandEntries[],把程序中欲处理的消息以及消息处理例程的关联性建立起来:
//消息与处理例程之对照表格
Struct MSGMAP_ENTRY _messageEntries[]=
{
WM_CREATE,OnCreate, //消息 消息处理例程
WM_PAINT,OnPaint,
WM_SIZE,OnSize,
WM_COMMAND,OnCommad,
WM_SETFOCUS,OnSetFocus,
WM_CLOSE,OnClose,
WM_DESTROY,OnDestroy,
};
//Command-ID与处理例程之对照表格
Struct MSGMAP_ENTRY _commandEntries=
{
IDM_ABOUT,OnAbout, //WM_COMMAND 命令处理例程
IDM_FILEOPEN,OnFileOpen,
IDM_SAVEAS, OnSaveAs,
};
于是窗口函数可以这样设计:
LRESULT CALLBACK WndProc(HWND hwnd,UINTmessage,WPARAM wParam,LPARAM lParam)
{
IntI;
For(i=0;i<dim()messageEntries);i++){
If(message==messageEntries[i].nMessage)
Return((*messageEntries[i].pfn)(hwnd,message,wParam,lParam));
}
Return(DefWindowProc(hWnd,message,wParam,lParam));
}
//OnCommand专门处理WM_COMMAD
LONG OnCommand(HWND hWnd,UINTmessage,WPARAM wParam,LPARAM lParam)
{
IntI;
For(i=0;i<dim(_commandEntries);i++){
If(LOWORD(wParam)==commandEntries[i].nMessage)
Return((*commandEntries[i].pfn)(hWnd,message,wParam,lParam));
}
Return(DefWindowProc(hWnd,message,wParam,lParam));
}
LONG OnCreate(HWND hWnd,UINT wMsg,UINTwParam,LONG lParam)
{
…………….
}
LONG OnAbout(HWND hWnd,UINT wMsg,UINTwParam,LONG lParam)
{
………
}
这样,WndProc和OnCommand永远不必改变,每当有新的要处理的消息,只要在_messageEntries[]和_commandEntries[]两个数组中加上新元素,并针对新消息撰写新的处理例程即可。
对话框的运作:
1、 modal对话框 令其父窗口除能,只到对话框结束。
2、 modeless对话框 父窗口与对话框共同运行。
为了做出对话框,需要做两方面准备:
1、 对话框模版(dialogtemplate)。RC文件中定义
2、 对话框函数(dialogprocedure)。对话框中的各个控制组件也都各有自己的窗口函数,它们与其管理者(父窗口)沟通,所有的控制组件传来的消息都是WM_COMMAND,再由其参数分辨哪一种控制组件以及哪一种通告(notification)。
Modal对话框的激活与结束,靠的是DialogBox和EndDialog两个API函数。
对话框的诞生、运作、结束:
模块定义文件(.DEF)
Windows程序需要一个模块定义文件,将模块名称、程序节区的内存特性、模块堆积(heap)大小、堆栈(stack)大小、所有callback函数名称….等等记录下来。
资源描述档(.RC)
RC文件是一个以文字描述资源的地方。常用的资源有九项之多,分别是ICON、CURSOR、BITMAP、FONT、DIALOG、MENU、ACCELERATOR、STRING、VERSIONINFO。还可能有新的资源不断加入。这些文字描述需经过RC编译器,才产生可使用的二进制代码。
Windows程序的生死:
1、 程序初始化过程中调用CreateWindow,为程序建立了一个窗口。CreateWindow产生窗口之后会送出WM_CREATE直接给窗口函数,后者可以做些初始化动作。
2、 程序运行中,不断用GetMessage从消息队列中抓取消息,如果这个消息是WM_QUIT,GetMessage会传回0而结束while循环,进而结束整个程序。
3、 DispatchMessage透过Windows USER模块的协助与监督,把消息分派至窗口函数。消息在该处被判别并处理。
4、 程序不断进行2和3。
5、 当使用者按下系统菜单中的close命令时,系统送出WM_CLOSE。通常程序的窗口函数不拦截此消息,于是DefWindowProc处理它。
6、 DefWindowProc收到WM_CLOSE后,调用DestroyWindow把窗口清除。DestroyWindow本身又会送出WM_DESTROY。
7、 PostQuitMessage没什么其它动作,就只送出WM_QUIT消息,准备让消息循环中的Getmessage取得,结束消息循环。
为什么结束一个程序复杂如斯?因为操作系统与应用程序职责不同,二者是互相合作的关系,所以必须各做各的份内事,并互以消息通知对方。
空闲时间的处理:OnIdle
所谓的空闲时间(idle time),是指系统中没有任何消息等待处理的时间。代码设计如下:
while(TRUE){
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE){
if(msg,message==WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else{
OnIdle();
}
}
Peekmessage和GetMessage的性质不同。操作系统第一次没抓到消息,第二次抓取时,又没抓到:
GetMessage会过门不入,于是操作系统再去照顾他人。
PeekMessage会取回控制权,使程序得以执行一段时间,于是消息循环进入OnIdle函数中。
Console程序:
Console程序可以调用部分的、不牵扯到图形使用者接口(GUI)的Win32API,甚至可以在console程序中使用部分的MFC类别(同样必须是与GUI没有关联的),例如处理数组、串行等数据结构的collection class(CArray、CList、CMap)、与文件有关的CFile、CStdioFile。
Console程序与DOS程序的差别:
制造方式:如果程序是以main为进入点,调用Cruntime函数和不牵扯GUI的Win32API函数,那么就是一个console程序,console窗口将成为其标准输入和输出装置。而过去在DOS环境下开发的程序,称为DOS程序,它也是以main为程序进入点,可以调用C runtime函数,但是不能调用win32 API函数。
可执行文档的格式:DOS程序是所谓的MZ格式。Console程序的格式则和所有的Win32程序一样,是所谓的PE格式,意思是它可以被拿到任何Win32平台上执行。
进程与执行线程(Process and Thread):
线程实际上是CPU的排程单位。
核心对象:
是系统的一种资源,系统对象一但产生,任何应用程序都可以开启并使用该对象,系统给予核心对象一个计数值作为管理之用。核心对象包括:
核心对象 | 产生方法 |
event 用于执行线程的同步化 | createEvent |
Mutex 用于执行线程的同步化 | CreateMutex |
Semaphore 用于执行线程的同步化 | CreateSemaphore |
File | CreateFile |
File-mapping 用于内存映射文件 | CreateFileMapping |
Process | CreateProcess |
Thread | CreateThread |
每使用一次,其对应的计数值就加1。核心对象的结束方式相当一致,调用CloseHandle即可。
一个进程的诞生与死亡:
1、 shell调用CreateProcess激活App.exe。
2、 系统产生一个进程核心对象,计数值为1.
3、 系统为此进程建立一个4GB地址空间。
4、 加载器将必要的码加载到上述地址空间中,包括App.exe的资料、程序以及所需的动态链接函数库(DLLs)。加载器如何知道要加载那些DLLs呢?它们被记录在可执行文件(PE文件格式)的idata section中。
5、 系统为此进程建立一个执行线程,称为主执行线程(primary thread)。执行线程才是CPU时间的分配对象。
6、 系统调用C runtime函数库的Startupcode。
7、 Startup code调用App程序的WinMain函数。
8、 App程序开始运作。
9、 使用者关闭App主窗口,使WinMain中的消息循环结束掉,于是WinMain结束。
10、回到Startup code。
11、回到系统,系统调用ExitProcess结束进程。
可以说,透过这种方式执行起来的所有Windows程序,都是shell的子进程。本来,母进程与子进程之间可以有某些关系存在,但shell在调用CreateProcess时已经把母子之间的脐带关系剪断了,因此它们事实上是独立实例。
产生子进程:
BOOL WINAPICreateProcess(
__in LPCTSTRlpApplicationName, //可执行文档的名字
__in_out LPTSTRlpCommandLine, //传递的命令参数
__in LPSECURITY_ATTRIBUTESlpProcessAttributes,//进程的安全属性
__in LPSECURITY_ATTRIBUTESlpThreadAttributes,//线程的安全属性
__in BOOLbInheritHandles,
__in DWORDdwCreationFlags,
__in LPVOIDlpEnvironment,
__in LPCTSTRlpCurrentDirectory,
__in LPSTARTUPINFOlpStartupInfo,//设定窗口的标题、位置、大小
__out LPPROCESS_INFORMATIONlpProcessInformation//保存创建的核心对象&线程对象的句柄
);
贴上一段代码示例:
#include <iostream>
#include<windows.h>
using namespacestd;
int main()
{
STARTUPINFO si; //一些必备参数设置
memset(&si,0,sizeof(STARTUPINFO));
si.cb =sizeof(STARTUPINFO);
si.dwFlags =STARTF_USESHOWWINDOW;
si.wShowWindow=SW_SHOW;
PROCESS_INFORMATION pi;//必备参数设置结束
if(!CreateProcess(L"c:\\windows\\system32\\notepad.exe",NULL,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi))
{
cout<<"CreateFail!"<<endl;
exit(1);
}
else
{
cout<<"Success!"<<endl;
}
return 0;
}
进程结束:
void ExitProcess(UINT fuExitCode);
结束另一个进程的生命:
BOOL TerminateProcess(HANDLEhProcess,UINT fuExitCode);
剪断脐带:
PROCESS _INFORMATION ProcInfo;
BOOL fSucess;
fSuccess=CreateProcess(…..,&ProcInfo);
if(fSucess){
CloseHandle(ProcInfo.hThread);
CloseHandle(ProcInfo.hProcess);
}
执行线程的诞生与死亡:
程序代码的执行,是执行线程的工作。当一个进程建立起来,主执行线程也产生。所以每一个Windows程序一开始就有了一个执行线程。我们可以调用CreateThread产生额外的执行线程。
HANDLE WINAPI CreateThread(
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全属性
__in SIZE_T dwStackSize,//堆栈大小
__in LPTHREAD_START_ROUTINE lpStartAddress,//执行线程函数
__in LPVOID lpParameter,//函数参数
__in DWORD dwCreationFlags,//0表示立即执行,CREATE_SUSPENDED表示暂停执行
__out LPDWORD lpThreadId//执行线程的ID
);
执行线程的结束有两种情况:一是执行线程函数正常退出,那么执行线程也就自然而然终结,这时系统会调用ExitThread(也可自己调用)做清理工作;二是别的执行线程强制以TerminateThread将它终结。
_beginthreadex取代CreateThread:
这不是一个标准的ANSIC runtime函数,不再由win32类别包装,_beginthreadex在内部调用了CreateThread,在调用之前_beginthreadex做了很多的工作,从而使得它比CreateThread更安全。
示例代码:
#include <windows.h>
#include <process.h>
#include <stdio.h>
unsigned _stdcall myfunc(void *p);
void main()
{
UINT thd;
HANDLE tid;
tid=(HANDLE)_beginthreadex(NULL,0,myfunc,0,0,&thd);
if(tid!=NULL)
{
CloseHandle(tid);
}
Sleep(10);
}
unsigned _stdcall myfunc(void *p)
{
printf("threadstart!\n");
return 0;
}
执行线程优先权(Priority):
Win32 有所谓的优先权的概念,较高优先级的线程必然获得较多的 CPU 时间。优先权以一个数值表示,值为 0 ~ 31。
进程的优先级类型(Priority class)决定了进程的重要性,Win32 提供了 4 种优先级类型:
- HIGH_PRIORITY_CLASS — 权值 13
任务管理器就是使用 HIGH_PRIORITY_CLASS,即使系统很忙碌的情况下,它常常会对操作有反应 - IDLE_PRIORITY_CLASS — 权值 4
有时候我们期望程序在 CPU 空闲下来的时候才执行,这个时候使用 IDLE_PRIORITY_CLASS 就很合适 - NORMAL_PRIORITY_CLASS — 权值 7 or 8
一般的应用程序都是使用这个类型 - REALTIME_PRIORITY_CLASS — 权值 24
进程设置为此优先权类型,那么它将优于内核进程和设备驱动进程,不要对 GUI 程序或者典型的服务器程序使用此优先级类型
Windows 的相关 API 为 SetPriorityClass() 和GetPriorityClass()。
线程的优先级等级(Priority level)决定了同一个进程中的各个线程的相对重要性,实际上线程的优先级等级是对进程的优先级类型的一个修改,Win32 提供了 7 种优先级等级:
- THREAD_PRIORITY_HIGHEST — 权值 +2
- THREAD_PRIORITY_ABOVE_NORMAL — 权值 +1
- THREAD_PRIORITY_NORMAL — 权值 +0
- THREAD_PRIORITY_BELOW_NORMAL — 权值 -1
- THREAD_PRIORITY_LOWEST — 权值 -2
- THREAD_PRIORITY_IDEL — 权值设置为 1
- THREAD_PRIORITY_TIME_CRITICAL — 权值设置为 15
Windows 的相关 API 为 SetThreadPriority() 和GetThreadPriority()。如果某一个进程的优先级类别为 HIGH_PRIORITY_CLASS,其中一个线程的优先级等级为 THREAD_PRIORITY_LOWEST 那么它的优先级权值就是13 – 2 = 11。
上面的两个因素决定了线程的优先权,另外还有一个决定线程优先权的因素:动态提升(Dynamic boost)。
首先,我们可以调整系统的设置,使得 CPU 更加倾向于程序还是后台服务。
除了调整系统设置,键盘消息、鼠标消息、计时器消息都可能引发动态提升,例如,线程获得键盘输入时,就获得了一个 +5 的优先级调整值。
最后,当等待状态得到满足时,例如,Wait… 返回时,此线程的优先权就会获得动态提升。
补充一个程序示例:
#include<windows.h>
#include<iostream>
using namespace std;
DWORD WINAPI FUN1(LPVOID lpParameter);
DWORD WINAPI FUN2(LPVOID lpParameter);
int index=0;
int tickets=100;
HANDLE hMutex1,hMutex2,hMutex3;//必须是全局变量
void main() //主进程结束,子线程也会结束 CPU为线程分配时间片
{
HANDLE thread1;
HANDLE thread2;
//HANDLE thread3;
thread1=CreateThread(NULL,0,FUN1,0,0,NULL);
thread2=CreateThread(NULL,0,FUN2,0,0,NULL);
CloseHandle(thread1);//需要有
CloseHandle(thread2);
hMutex1=CreateMutex(NULL,FALSE,NULL);//默认的安全属性、主线程不拥有该对象、互斥对象的名称
Sleep(3000);
hMutex2=CreateMutex(NULL,true,L"main");//默认的安全属性、
hMutex3=CreateMutex(NULL,true,L"main");///利用互斥对象 还可以保证只有一个程序的实例运行
if(hMutex3)
{
if(ERROR_ALREADY_EXISTS==GetLastError())
{
cout<<"only one instance can run!"<<endl;
return;
}
}
ReleaseMutex(thread2);
/*while(index++<100)
cout<<"main thread is running"<<endl;*/
Sleep(3000);
}
DWORD WINAPI FUN1(LPVOID lpParameter)
{
cout<<"thread1 is running"<<endl;*/
while(TRUE)
{
WaitForSingleObject(hMutex1,INFINITE);//必须主动请求钥匙(互斥对象)使互斥对象无信号
if(tickets>0)
cout<<"thread1 sell ticket:"<<tickets--<<endl;
else
break;
ReleaseMutex(hMutex1);//释放钥匙(互斥对象)是互斥对象有信号 如果没有该语句 则 线程执行完之后会自动释放
}
return 0;
}
DWORD WINAPI FUN2(LPVOID lpParameter)
{
cout<<"thread2 is running"<<endl;*/
while(TRUE)
{
WaitForSingleObject(hMutex1,INFINITE);
if(tickets>0)
cout<<"thread2 sell ticket:"<<tickets--<<endl;
else
break;
ReleaseMutex(hMutex1);//互斥对象是谁拥有 谁释放
}
return 0;
}