硬件篇

编程实现测试CPU的速度
CPU的速度随温度和电压的变化而变化,如何随时查看CPU的速度?下面我们通过编程实现。在这个过程中,要用到汇编语言的知识。

  第一步:生成一个基于对话框的工程CPUSpeed。其他选项我们可以都取其默认值。

  第二步:在对话框上添加一个按钮,名称为"测试CPU速度",双击此按钮生成此按钮的处理函数,OnButton1。

  第三步:在CPUSpeedDlg.cpp文件中定义类Ctime,在OnButton1中添加处理代码,最后文件CPUSpeedDlg.cpp变成如下:

// CPUSpeedDlg.cpp : implementation file
// 
#include "stdafx.h"
#include "CPUSpeed.h"
#include "CPUSpeedDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///
// CAboutDlg dialog used for App About
inline unsigned __int64 theCycleCount(void)
{

 _asm _emit 0x0F
 _asm _emit 0x31

 /// this causes a compiler warning as there is no return statement
 /// however the _emits return a __int64 value
}

class CTimer
{
 unsigned __int64 m_start;

 public:

  unsigned __int64 m_overhead;

 CTimer(void)
 {
  m_overhead = 0;
  Start(); /// we get the start cycle
  m_overhead = Stop();
  // then we get the stop cycle catching the overhead time
 }

 void Start(void)
 {
  m_start = theCycleCount();
 }

 unsigned __int64 Stop(void)
 {
  /// with the stop cycle we remove the overhead's time
  return theCycleCount()-m_start-m_overhead;
 }
};

void CCPUSpeedDlg::OnButton1() 
{
 // TODO: Add your control notification handler code here
 CString strRawClockFrequency;
 
 CTimer timer;

 long tickslong;
 long tickslongextra;
 timer.Start(); 
 Sleep(1000); 
 unsigned cpuspeed100 = (unsigned)(timer.Stop()/10000);

 tickslong = cpuspeed100/100;
 tickslongextra = cpuspeed100-(tickslong*100);
 strRawClockFrequency.Format("%d.%d MHZ estimate ", tickslong,tickslongextra );
 AfxMessageBox("CPU速度为"+strRawClockFrequency);
 }
 class CAboutDlg : public CDialog
 {
 ……以下为编程环境生成时自动生成的代码。

  好了,现在点击按钮"测试CPU速度"就可以弹出对话框告诉我们CPU的速度了。

程序中使用自定义的鼠标

?建立工程与一个资源档

用Image Editor编辑一个鼠游标

  (Fild | New | Resource File)

新建一个 CURSOR_1 的 CURSOR, 设定好它的 Hot Spot

  (Cursor | Set Hot Spot)

  存档时注意要和建立的Project存在同一个目录在本例我们先假定为 MyCursor.res

二. 程序部分

  定义一个常数crMyCursor, 这个常数您必须设成大於零的任何整数, 以 LoadCursor() 函数将自订的鼠标资源 load 进来, 以下为源代码:

// unit.pas

unit Unit1;

interface

uses

SysUtils, WinTypes, WinProcs, Messages, Classes,

Graphics, Controls, Forms, Dialogs;

const

crMyCursor = 1; (* 宣告一个常数 *)

type

TForm1 = class(TForm)

procedure FormCreate(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

{$R mycursor.res}//这行$R不可少, 否则自订的鼠游标就出不来

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);

begin

//将鼠标资源 load 进来

Screen.Cursors[crMyCursor] := LoadCursor

(hInstance,CURSOR_1);

Cursor := crMyCursor;//指定 form1 的 cursor 自订鼠标

Button1.Cursor := crMyCursor;//指定Button1的cursor为自订?

end;

end.

 进程与线程

Windows多线程多任务设计初步
[前言:]
当前流行的Windows操作系统,它能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线程,线程提供了多任务处理的能力。用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义。现在的应用软件无一不是多线程多任务处理,单线城的软件是不可想象的。因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的。本文针对多线程技术在应用中经常遇到的问题,如线程间的通信、同步等,对它们分别进行探讨。

  一、 理解线程

  要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应Visual C++中的CwinThread类的对象。单独一个执行程序运行时,缺省的运行包含的一个主线程,主线程以函数地址的形式,如main或WinMain函数,提供程序的启动点,当主线程终止时,进程也随之终止,但根据需要,应用程序又可以分解成许多独立执行的线程,每个线程并行的运行在同一进程中。

  一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。

  线程被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进城终止。工作者线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CwinThread类派生来创建,对它来说最重要的是如何实现工作线程任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同时每个线程可以拥有自己的变量。

二、 线程的管理和操作

  1. 线程的启动

  创建一个用户界面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声明和实现这个CwinThread派生类。

  第二步是根据需要重载该派生类的一些成员函数如:ExitInstance();InitInstance();OnIdle();PreTranslateMessage()等函数,最后启动该用户界面线程,调用AfxBeginThread()函数的一个版本:CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );其中第一个参数为指向定义的用户界面线程类指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线程创建时的附加标志,缺省为正常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。

  对于工作线程来说,启动一个线程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Fun1(),接着定义一个指向CwinThread对象的指针变量*pThread,调用AfxBeginThread(Fun1,param,priority)函数,返回值付给pThread变量的同时一并启动该线程来执行上面的Fun1()函数,其中Fun1是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数Fun1的任意32位值,priority则是定义该线程的优先级别,它是预定义的常数,读者可参考MSDN。

  2.线程的优先级

  以下的CwinThread类的成员函数用于线程优先级的操作:

int GetThreadPriority();
BOOL SetThradPriority()(int nPriority);

上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。至于优先级设置所需的常数,自己参考MSDN就可以了,要注意的是要想设置线程的优先级,这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置,CwinThread类没有提供相应的函数,但是可以通过Win32 SDK函数GetPriorityClass()和SetPriorityClass()来实现。

   3.线程的悬挂、恢复

  CwinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行;ResumeThread()用来恢复线程的执行。如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的ResumeThread()来恢复线程的运行。

   4.结束线程

  终止线程有三种途径,线程可以在自身内部调用AfxEndThread()来终止自身的运行;可以在线程的外部调用BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈;第三种方法是改变全局变量,使线程的执行函数返回,则该线程终止。下面以第三种方法为例,给出部分代码:


//CtestView message handlers
/Set to True to end thread
Bool bend=FALSE;//定义的全局变量,用于控制线程的运行
//The Thread Function
UINT ThreadFunction(LPVOID pParam)//线程函数
{
while(!bend)
{Beep(100,100);
Sleep(1000);
}
return 0;
}
CwinThread *pThread;
HWND hWnd;
/
Void CtestView::OninitialUpdate()

hWnd=GetSafeHwnd();
pThread=AfxBeginThread(ThradFunction,hWnd);//启动线程
pThread->m_bAutoDelete=FALSE;//线程为手动删除
Cview::OnInitialUpdate();
}

Void CtestView::OnDestroy()
{ bend=TRUE;//改变变量,线程结束
WaitForSingleObject(pThread->m_hThread,INFINITE);//等待线程结束
delete pThread;//删除线程
Cview::OnDestroy();
}

三、 线程之间的通信

  通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种方法实现这种通信任务:使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们主要介绍后两种方法。

   1. 利用用户定义的消息通信

  在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非常简单。首先用户要定义一个用户消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的时候,在一个线程中调用

::PostMessage((HWND)param,WM_USERMSG,0,0)

CwinThread::PostThradMessage()

来向另外一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果是在线程结束时显示一个对话框,提示线程结束:

UINT ThreadFunction(LPVOID pParam)
{
while(!bend)

Beep(100,100);
Sleep(1000);
}
::PostMessage(hWnd,WM_USERMSG,0,0);
return 0;
}
WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam,LPARAM lParam)
LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam)
{
AfxMessageBox("Thread ended.");
Retrun 0;
}

上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定的处理等消息,让它在后台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己实现一个消息循环。GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空,就获取这个消息,判断这个消息的内容并进行相应的处理。

   2.用事件对象实现通信

  在线程之间传递信号进行通信比较复杂的方法是使用事件对象,用MFC的Cevent类的对象来表示。事件对象处于两种状态之一:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。上述例子代码修改如下:


Cevent threadStart,threadEnd;

UINT ThreadFunction(LPVOID pParam)
{
::WaitForSingleObject(threadStart.m_hObject,INFINITE);
AfxMessageBox("Thread start.");
while(!bend)
{
Beep(100,100);
Sleep(1000);
Int result=::WaitforSingleObject(threadEnd.m_hObject,0);
//等待threadEnd事件有信号,无信号时线程在这里悬停
If(result==Wait_OBJECT_0)
Bend=TRUE;
}
::PostMessage(hWnd,WM_USERMSG,0,0);
return 0;
}
/
Void CtestView::OninitialUpdate()

hWnd=GetSafeHwnd();
threadStart.SetEvent();//threadStart事件有信号
pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程
pThread->m_bAutoDelete=FALSE;
Cview::OnInitialUpdate();
}

Void CtestView::OnDestroy()
{ threadEnd.SetEvent();
WaitForSingleObject(pThread->m_hThread,INFINITE);
delete pThread;
Cview::OnDestroy();
}

运行这个程序,当关闭程序时,才显示提示框,显示"Thread ended"

四、 线程之间的同步

  前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。

   1. 临界区

  临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它们对应的函数分别为WriteThread()和ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:

#include "afxmt.h"
int array[10],destarray[10];
CCriticalSection Section;

UINT WriteThread(LPVOID param)
{Section.Lock();
for(int x=0;x<10;x++)
array[x]=x;
Section.Unlock();
}
UINT ReadThread(LPVOID param)
{
Section.Lock();
For(int x=0;x<10;x++)
Destarray[x]=array[x];
Section.Unlock();
}

上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如果不使用同步,则不是这个结果,有兴趣的读者可以实验一下
  2. 互斥

  互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互斥与Cmutex类的对象相对应,使用互斥对象时,必须创建桓鯟SingleLock或CMultiLock对象,用于实际的访问控制,因为这里的例子只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock()函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:

#include "afxmt.h"
int array[10],destarray[10];
CMutex Section;

/
UINT WriteThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();
for(int x=0;x<10;x++)
array[x]=x;
singlelock.Unlock();
}
UINT ReadThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();

For(int x=0;x<10;x++)
Destarray[x]=array[x];
singlelock.Unlock();

}

  3 . 信号量

  信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦创建了一个信号量对象,就可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对象,然后用该对象的Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得以显示。

/
Csemaphore *semaphore;
Semaphore=new Csemaphore(2,2);
HWND hWnd=GetSafeHwnd();
AfxBeginThread(threadProc1,hWnd);
AfxBeginThread(threadProc2,hWnd);
AfxBeginThread(threadProc3,hWnd);
//
UINT ThreadProc1(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);
return 0;
}
UINT ThreadProc2(LPVOID param)
{CSingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);
return 0;
}
UINT ThreadProc3(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);
return 0;
}

  对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。本文讲述了线程中经常遇到的问题,希望对读者朋友有一定的帮助。

例程分析多线程编程

Windows系统平台经历了16位到32位的转变后,系统运行方式和任务管理方式有了很大的变化,在Windows 95和Windows NT中,每个Win32程序在独立的进程空间上运行,32位地址空间使我们从16位段式结构的64K段限制中摆脱出来,逻辑上达到了4G的线性地址空间,我们在设计程序时,不再需要考虑编译的段模式,同时还提高了大程序的运行效率。独立进程空间的另一个更大的优越性是大大提高了系统的稳定性,一个应用的异常错误不会影响其它的应用。与在MS-DOS和16位Windows操作系统中不同,32位Windows进程是没有活力的。这就是说,一个32位Windows进程并不执行什么指令,它只是占据着4GB的地址空间,此空间中有应用程序EXE文件的代码和数据。EXE需要的DLL也将它们的代码的数据装入到进程的地址空间。除了地址空间,进程还占有某些资源,比如文件、动态内存分配和线程。当进程终止时,在它生命期中创建的各种资源将被清除。

  如上所述,进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含几个线程,它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和椎。每个进程至少有一个线址程在执行其地址空间中的代码,如果没有线程执行进程地空间中的代码,如果没有线程执行进程地址空间中的代码,进程也就没有继续存在的理由,系统将自动清除进程及其地址空间。为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮转方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。创建一个32位Windows进程时,它的第一个线程称为主线程,由系统自动生成,然后可由这个主线程生成额外的线程,这些线程又可生成更多的线程。

  例如,在基于Internet网上的可视电话系统中,同时要进行语音采集、语音编译码、图像采集、图像编译码、语音和图像码流的传输,所有这些工作,都要并行处理。特别是语音信号,如果进行图像编解码时间过长,语音信号得不到服务,通话就有间断;如果图像或语音处理时间过长,而不能及时传输码流数据,通信同样也会中断。这样就要求我们实现一种并行编程,在只有一个CPU的机器上,也就是要将该CPU时间按时一定的优先准则分配给各个事件,定期处理各事件,而不会对某一事件处理过长。

  在32位Windows95或Windows NT下,我们可以用多线程的处理技术来实现这种并行处理。实际上,这种并行编程在很多场合下都是必须的。再例如,在File Manager拷贝文件时,它显示一个对话框中包含了一个Cancel按钮。如果在文件拷贝过程中,点中Cance l按钮,就会终止拷贝。在16位Winows中,实现这类功能需要在File Copy循环内部周期性地调用PeekMessage函数。如果正在读一个很大的动作;如果从软盘读文件,则要花费好几秒的时间。由于机器反应太迟钝,用户会频繁地点中这个按钮,以为系统不知道想终止这个操作。如果把File Copy指令放入另外一个线程,就不需要在代码中放一大堆PeekMessage函数,处理用户界面的线程将与它分开操作,点中Cancel按钮后会立即得到响应。同样的道理,在应用程序中创建一个单独线程来处理所有打印任务也是很有用的,用户可以在打印处理时继续使用应用程序。

  多线程的编程在Win32方式下和MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程,当线程执行完任务后,自动终止线程,当进程结束后,所有的线程都终止。所有活动的线程共享进程的资源,所以在编程时,需要考虑在多个线程访问同一资源时,产生冲突的问题,当一个线程正在访问一个进程对象,这时另一个线程要改变该对象,这时可能会产生错误的结果,所以程序员在编程时要解决这种冲突。

下面举例说明在Win32 基础上进行多线程编程的过程。

  1.使用函数说明

  Win32函数库里提供了多线程控制的操作函数,包括创建线程、终止线程、建立互斥区等。首先,在应用程序的主线程或者其它活动线程的适当的地方创建新的线程,创建线程的函数如下:

  HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,

  DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,

  LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );

  参数lpThreadAttributes 指定了线程的安全属性,在Windows 95中被忽略;

  参数dwStackSize 指定了线程的堆栈深度;

  参数lpStartAddress 指定了线程的起始地址,一般情况为下面原型的函数

  DWORD WINAPI ThreadFunc( LPVOID );

  参数 lpParameter指定了线程执行时传送给线程的32位参数,即上面函数的参数;

  参数dwCreationFlags指定了线程创建的特性;

  参数 lpThreadId 指向一个DWORD变量,可返回线程ID值。

  如果创建成功则返回线程的句柄,否则返回NULL。

  创建了新的线程后,线程开始启动执行,如果在dwCreationFlags中用了CREATE_SUSPENDED特性,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程,在这过程中可以调用函数

  BOOL SetThreadPriority( HANDLE hThread, int nPriority);

  设置线程的优先权。

  当线程的函数返回后,线程自动终止,如果要想在线程的执行过程中终止的话,可以调用函数

  VOID ExitThread( DWORD dwExitCode);

  如果在线程的外面终止线程的话,可以用下面的函数

  BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );

  但注意,该函数可能会引起系统不稳定,而且,线程所占用的资源也不释放,因此,一般情况下,建议不要使用该函数。

  如果要终止的线程是进程内的最后一个线程的话,在线程被终止后,相应的进程也终止。

  2. 无优先级例程,该例程实现在对话框上通过一线程实现时钟的显示和停止。步骤如下:

  第一步:建立一基于对话框的工程MultiProcess1。

  第二步:在对话框上建立两个按钮和一个编辑框,ID号分别为ID_START、ID_STOP和IDC_TIME,Caption分别为"启动"、"停止"。如下:

  第三步:在MultiProcess1Dlg.cpp中定义全局函数ThreadProc(),格式如下:

void ThreadProc()
 {
  CTime time;
  CString m_time;
  for(;;)
   {
    time=CTime::GetCurrentTime();
    m_time=time.Format("%H:%M:%S");
    ::SetDlgItemText(AfxGetMainWnd()->m_hWnd,IDC_TIME,m_time);
    Sleep(1000);
   }
  }

  第四步:在头文件MultiProcess1Dlg.h中定义变量如下:

   DWORD ThreadID;

   HANDLE hThread;

  第五步:双击"开始"按钮,生成消息映射函数OnStart(),编写其中的代码如下:

void CMultiProcess1Dlg::OnStart() 
{
 // TODO: Add your control notification handler code here
 hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,
   NULL,0,&ThreadID);
}

  此时即刻实现在对话框上点击"启动",启动时钟。接下来我们实现如何让时钟停下来。

  第六步:双击"停止"按钮,添加停止的消息映射函数OnStop(),编写代码如下:

void CMultiProcess1Dlg::OnStop() 
{
// TODO: Add your control notification handler code here
TerminateThread(hThread,1);
}

  注意:该函数可能会引起系统不稳定,而且,线程所占用的资源也不释放,因此,一般情况下,建议不要使用该函数。

  到现在,这个程序就完整了,看一下效果吧!

  最后需要说明的是,并不是设计多线程就是一个好的程序。目前大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统性能,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,但对于大部分时间被阻塞的线程(例如等待文件 I/O 操作),可以用一个单独的线程来完成,这样可把CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用时,需要慎重选择,具体情况具体处理,以使应用程序获得最佳的性能。

用VC++5.0实现多线程的调度和处理

一多任务,多进程和多线程
---- Windows95 和WindowsNT 操作系统支持多任务调度和处理,基于该功能所提供的多任务空间,程序员可以完全控制应用程序中每一个片段的运行,从而编写高效率的应用程序。
---- 所谓多任务通常包括这样两大类:多进程和多线程。进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。

---- 开发多线程应用程序可以利用32 位Windows 环境提供的Win32 API 接口函数,也可以利用VC++ 中提供的MFC 类库进行开发。多线程编程在这两种方式下原理是一样的,用户可以根据需要选择相应的工具。本文重点讲述用VC++5.0 提供的MFC 类库实现多线程调度与处理的方法以及由线程多任务所引发的同步多任务特征,最后详细解释一个实现多线程的例程。

二基于MFC 的多线程编程
---- 1 MFC 对多线程的支持
---- MFC 类库提供了多线程编程支持,对于用户编程实现来说更加方便。非常重要的一点就是,在多窗口线程情况下,MFC 直接提供了用户接口线程的设计。

---- MFC 区分两种类型的线程:辅助线程(Worker Thread)和用户界面线程(UserInterface Thread)。辅助线程没有消息机制,通常用来执行后台计算和维护任务。MFC 为用户界面线程提供消息机制,用来处理用户的输入,响应用户产生的事件和消息。但对于Win32 的API 来说,这两种线程并没有区别,它只需要线程的启动地址以便启动线程执行任务。用户界面线程的一个典型应用就是类CWinApp,大家对类CwinApp 都比较熟悉,它是CWinThread 类的派生类,应用程序的主线程是由它提供,并由它负责处理用户产生的事件和消息。类CwinThread 是用户接口线程的基本类。CWinThread 的对象用以维护特定线程的局部数据。因为处理线程局部数据依赖于类CWinThread,所以所有使用MFC 的线程都必须由MFC 来创建。例如,由run-time 函数_beginthreadex 创建的线程就不能使用任何MFC API。

---- 2 辅助线程和用户界面线程的创建和终止

---- 要创建一个线程,需要调用函数AfxBeginThread。该函数通过参数重载具有两种版本,分别对应辅助线程和用户? 线程。无论是辅助线程还是用户界面线程,都需要指定额外的参数以修改优先级,堆栈大小,创建标志和安全特性等。函数AfxBeginThread 返回指向CWinThread 类对象的指针。

---- 创建助手线程相对简单。只需要两步:实现控制函数和启动线程。它并不必须从CWinThread 派生一个类。简要说明如下:

---- 1. 实现控制函数。控制函数定义该线程。当进入该函数,线程启动;退出时,线程终止。该控制函数声明如下:

UINT MyControllingFunction( LPVOID pParam );
---- 该参数是一个单精度32 位值。该参数接收的值将在线程对象创建时传递给构造函数。控制函数将用某种方式解释该值。可以是数量值,或是指向包括多个参数的结构的指针,甚至可以被忽略。如果该参数是指结构,则不仅可以将数据从调用函数传给线程,也可以从线程回传给调用函数。如果使用这样的结构回传数据,当结果准备好的时候,线程要通知调用函数。当函数结束时,应返回一个UINT 类型的值值,指明结束的原因。通常,返回0 表明成功,其它值分别代表不同的错误。
---- 2. 启动线程。由函数AfxBeginThread 创建并初始化一个CWinThread 类的对象,启动并返回该线程的地址。则线程进入运行状态。

---- 3. 举例说明。下面用简单的代码说明怎样定义一个控制函数以及如何在程序的其它部分使用。

UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject- >IsKindOf(RUNTIME_CLASS(CMyObject)))
return -1; //非法参数
……//具体实现内容
return 0; //线程成功结束
}

//在程序中调用线程的函数
……
pNewObject = new CMyObject;
AfxBeginThread(MyThreadProc, pNewObject);
    ……
    创建用户界面线程有两种方法。
---- 第一种方法,首先从CWinTread 类派生一个类(注意必须要用宏DECLARE_DYNCREATE 和IMPLEMENT_DYNCREATE 对该类进行声明和实现);然后调用函数AfxBeginThread 创建CWinThread 派生类的对象进行初始化启动线程运行。除了调用函数AfxBeginThread 之外, 也可以采用第二种方法,即先通过构造函数创建类CWinThread 的一个对象,然后由程序员调用函数::CreateThread 来启动线程。通常类CWinThread 的对象在该线程的生存期结束时将自动终止,如果程序员希望自己来控制,则需要将m_bAutoDelete 设为FALSE。这样在线程终止之后类CWinThread 对象仍然存在,只是在这种情况下需要手动删除CWinThread 对象。
---- 通常线程函数结束之后,线程将自行终止。类CwinThread 将为我们完成结束线程的工作。如果在线程的执行过程中程序员希望强行终止线程的话,则需要在线程内部调用AfxEndThread(nExitCode)。其参数为线程结束码。这样将终止线程的运行,并释放线程所占用的资源。如果从另一个线程来终止该线程,则必须在两个线程之间设置通信方法。如果从线程外部来终止线程的话,还可以使用Win32 函数(CWinThread 类不提供该成员函数):BOOL TerminateThread(HANDLE hThread,DWORD dwExitcode)。但在实际程序设计中对该函数的使用一定要谨慎,因为一旦该命令发出,将立即终止该线程,并不释放线程所占用的资源,这样可能会引起系统不稳定。

---- 如果所终止的线程是进程内的最后一个线程,则在该线程终止之后进程也相应终止。

---- 3 进程和线程的优先级问题

---- 在Windows95 和WindowsNT 操作系统当中,任务是有优先级的,共有32 级,从0 到31,系统按照不同的优先级调度线程的运行。

---- 1) 0-15 级是普通优先级,线程的优先级可以动态变化。高优先级线程优先运行,只有高优先级线程不运行时,才调度低优先级线程运行。优先级相同的线程按照时间片轮流运行。2) 16-30 级是实时优先级,实时优先级与普通优先级的最大区别在于相同优先级进程的运行不按照时间片轮转,而是先运行的线程就先控制CPU,如果它不主动放弃控制,同级或低优先级的线程就无法运行。

---- 一个线程的优先级首先属于一个类,然后是其在该类中的相对位置。线程优先级的计算可以如下式表示:

---- 线程优先级= 进程类基本优先级+ 线程相对优先级

---- 进程类的基本优先级:

IDLE_PROCESS_CLASS
    NORMAL_PROCESS_CLASS
    HIGH_PROCESS_CLASS
    REAL_TIME_PROCESS_CLASS
线程的相对优先级:
    THREAD_PRIORITY_IDLE 
(最低优先级,仅在系统空闲时执行)
    THREAD_PRIORITY_LOWEST        
    THREAD_PRIORITY_BELOW_NORMAL 
THREAD_PRIORITY_NORMAL (缺省)
    THREAD_PRIORITY_ABOVE_NORMAL    
    THREAD_PRIORITY_HIGHEST        
    THREAD_PRIORITY_CRITICAL
(非常高的优先级)
---- 4 线程同步问题

---- 编写多线程应用程序的最重要的问题就是线程之间的资源同步访问。因为多个线程在共享资源时如果发生访问冲突通常会产生不正确的结果。例如,一个线程正在更新一个结构的内容的同时另一个线程正试图读取同一个结构。结果,我们将无法得知所读取的数据是什么状态:旧数据,新数据,还是二者的混合?

---- MFC 提供了一组同步和同步访问类来解决这个问题,包括:

---- 同步对象:CSyncObject, CSemaphore, CMutex, CcriticalSection 和CEvent ;同步访问对象:CMultiLock 和CSingleLock 。

---- 同步类用于当访问资源时保证资源的整体性。其中CsyncObject 是其它四个同步类的基类,不直接使用。信号同步类CSemaphore 通常用于当一个应用程序中同时有多个线程访问一个资源(例如,应用程序允许对同一个Document 有多个View)的情况;事件同步类CEvent 通常用于在应用程序访问资源之前应用程序必须等待(比如,在数据写进一个文件之前数据必须从通信端口得到)的情况;而对于互斥同步类CMutex 和临界区同步类CcriticalSection 都是用于保证一个资源一次只能有一个线程访问,二者的不同之处在于前者允许有多个应用程序使用该资源(例如,该资源在一个DLL 当中)而后者则不允许对同一个资源的访问超出进程的范畴,而且使用临界区的方式效率比较高。

---- 同步访问类用于获得对这些控制资源的访问。CMultiLock 和CSingleLock 的区别仅在于是需要控制访问多个还是单个资源对象。

---- 5 同步类的使用方法

---- 解决同步问题的一个简单的方法就是将同步类融入共享类当中,通常我们把这样的共享类称为线程安全类。下面举例来说明这些同步类的使用方法。比如,一个用以维护一个帐户的连接列表的应用程序。该应用程序允许3 个帐户在不同的窗口中检测,但一次只能更新一个帐户。当一个帐户更新之后,需要将更新的数据通过网络传给一个数据文档。

---- 该例中将使用3 种同步类。由于允许一次检测3 个帐户,使用CSemaphore 来限制对3 个视窗对象的访问。当更新一个帐目时,应用程序使用CCriticalSection 来保证一次只有一个帐目更新。在更新成功之后,发CEvent 信号,该信号释放一个等待接收信号事件的线程。该线程将新数据传给数据文档。

---- 要设计一个线程安全类,首先根据具体情况在类中加入同步类做为数据成员。在例子当中,可以将一个CSemaphore 类的数据成员加入视窗类中,一个CCriticalSection 类数据成员加入连接列表类,而一个CEvent 数据成员加入数据存储类中。

---- 然后,在使用共享资源的函数当中,将同步类与同步访问类的一个锁对象联系起来。即,在访问控制资源的成员函数中应该创建一个CSingleLock 或CMultiLock 的对象并调用该对象的Lock 函数。当访问结束之后,调用UnLock 函数,释放资源。

---- 用这种方式来设计线程安全类比较容易。在保证线程安全的同时,省去了维护同步代码的麻烦,这也正是OOP 的思想。但是使用线程安全类方法编程比不考虑线程安全要复杂,尤其体现在程序调试过程中。而且线程安全编程还会损失一部分效率,比如在单CPU 计算机中多个线程之间的切换会占用一部分资源。

三编程实例
---- 下面以VC++5.0 中一个简单的基于对话框的MFC 例程来说明实现多线程任务调度与处理的方法,下面加以详细解释。
---- 在该例程当中定义两个用户界面线程,一个显示线程(CDisplayThread) 和一个计数线程(CCounterThread)。这两个线程同时操作一个字符串变量m_strNumber,其中显示线程将该字符串在一个列表框中显示,而计数线程则将该字符串中的整数加1。在例程中,可以分别调整进程、计数线程和显示线程的优先级。例程中的同步机制使用CMutex 和CSingleLock 来保证两个线程不能同时访问该字符串。同步机制执行与否将明显影响程序的执行结果。在该例程中允许将将把两个线程暂时挂起,以查看运行结果。例程中还允许查看计数线程的运行。该例程中所处理的问题也是多线程编程中非常具有典型意义的问题。

---- 在该程序执行时主要有三个用于调整优先级的组合框,三个分别用于选择同步机制、显示计数线程运行和挂起线程的复选框以及一个用于显示运行结果的列表框。

---- 在本程序中使用了两个线程类CCounterThread 和CDisplayThread,这两个线程类共同操作定义在CMutexesDlg 中的字符串对象m_strNumber。本程序对同步类CMutex 的使用方法就是按照本文所讲述的融入的方法来实现的。同步访问类CSingleLock 的锁对象则在各线程的具体实现中定义。

---- 下面介绍该例程的具体实现:

利用AppWizard 生成一个名为Mutexes 基于对话框的应用程序框架。

利用对话框编辑器在对话框中填加以下内容:三个组合框,三个复选框和一个列表框。三个组合框分别允许改变进程优先级和两个线程优先级,其ID 分别设置为:IDC_PRIORITYCLASS、IDC_DSPYTHRDPRIORITY 和IDC_CNTRTHRDPRIORITY。三个复选框分别对应着同步机制选项、显示计数线程执行选项和暂停选项,其ID 分别设置为IDC_SYNCHRONIZE、IDC_SHOWCNTRTHRD 和IDC_PAUSE。列表框用于显示线程显示程序中两个线程的共同操作对象m_strNumber,其ID 设置为IDC_DATABOX。

创建类CWinThread 的派生类CExampleThread。该类将作为本程序中使用的两个线程类:CCounterThread 和CDisplayThread 的父类。这样做的目的仅是为了共享两个线程类的共用变量和函数。
---- 在CExampleThread 的头文件中填加如下变量:
    CMutexesDlg * m_pOwner;//指向类CMutexesDlg指针
        BOOL m_bDone;//用以控制线程执行
及函数:
    void SetOwner(CMutexesDlg* pOwner) 
{ m_pOwner=pOwner; };//取类CMutexesDlg的指针
    然后在构造函数当中对成员变量进行初始化:
        m_bDone=FALSE;//初始化允许线程运行
        m_pOwner=NULL;//将该指针置为空
        m_bAutoDelete=FALSE;//要求手动删除线程对象
创建两个线程类CCounterThread 和CdisplayThread。这两个线程类是CExampleThread 的派生类。分别重载两个线程函数中的::Run() 函数,实现各线程的任务。在这两个类当中分别加入同步访问类的锁对象sLock,这里将根据同步机制的复选与否来确定是否控制对共享资源的访问。不要忘记需要加入头文件#include "afxmt.h"。
---- 计数线程::Run() 函数的重载代码为:
int CCounterThread::Run() 
{
    BOOL fSyncChecked;//同步机制复选检测
    unsigned int nNumber;//存储字符串中整数

if (m_pOwner == NULL)
        return -1;
    //将同步对象同锁对象联系起来
CSingleLock sLock(&(m_pOwner- >m_mutex));
    while (!m_bDone)//控制线程运行,为终止线程服务
    {
    //取同步机制复选状态
fSyncChecked = m_pOwner- >
IsDlgButtonChecked(IDC_SYNCHRONIZE);
    //确定是否使用同步机制
    if (fSyncChecked)
        sLock.Lock();
    //读取整数
_stscanf((LPCTSTR) m_pOwner- >m_strNumber,
_T("%d"), &nNumber);
    nNumber++;//加1
    m_pOwner- >m_strNumber.Empty();//字符串置空
    while (nNumber != 0) //更新字符串
    {
m_pOwner- >m_strNumber +=
(TCHAR) (0+nNumber%10);
nNumber /= 10;
    }
    //调整字符串顺序
    m_pOwner- >m_strNumber.MakeReverse();
    //如果复选同步机制,释放资源
    if (fSyncChecked)
        sLock.Unlock();
    //确定复选显示计数线程
    if (m_pOwner- >IsDlgButtonChecked(IDC_SHOWCNTRTHRD))
        m_pOwner- >AddToListBox(_T("Counter: Add 1"));
    }//结束while

m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
    return 0;
}
显示线程的::Run()函数重载代码为:
int CDisplayThread::Run() 
{
    BOOL fSyncChecked;
    CString strBuffer;

ASSERT(m_pOwner != NULL);
    if (m_pOwner == NULL)
        return -1;
    CSingleLock sLock(&(m_pOwner- >m_mutex));
    while (!m_bDone)
    {
fSyncChecked = m_pOwner- > 
IsDlgButtonChecked(IDC_SYNCHRONIZE);
    if (fSyncChecked)
        sLock.Lock();
    //构建要显示的字符串
    strBuffer = _T("Display: ");
    strBuffer += m_pOwner- >m_strNumber;
    if (fSyncChecked)
        sLock.Unlock();
    //将字符串加入到列表框中
    m_pOwner- >AddToListBox(strBuffer);
    }//结束while
    m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
    return 0;
}

3在CMutexesDlg的头文件中加入如下成员变量:
        CString m_strNumber;//线程所要操作的资源对象
        CMutex m_mutex;//用于同步机制的互斥量
        CCounterThread* m_pCounterThread;//指向计数线程的指针
        CDisplayThread* m_pDisplayThread;//指向显示线程的指针
    首先在对话框的初始化函数中加入如下代码对对话框进行初始化:
    BOOL CMutexesDlg::OnInitDialog()
{
    ……
    //初始化进程优先级组合框并置缺省为NORMAL
    CComboBox* pBox;
    pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS);
    ASSERT(pBox != NULL);
    if (pBox != NULL){
        pBox- >AddString(_T("Idle"));
        pBox- >AddString(_T("Normal"));
        pBox- >AddString(_T("High"));
        pBox- >AddString(_T("Realtime"));
        pBox- >SetCurSel(1); 
    }
    //初始化显示线程优先级组合框并置缺省为NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_DSPYTHRDPRIORITY);
    ASSERT(pBox != NULL);
    if (pBox != NULL){
    pBox- >AddString(_T("Idle"));
    pBox- >AddString(_T("Lowest"));
    pBox- >AddString(_T("Below normal"));
    pBox- >AddString(_T("Normal"));
    pBox- >AddString(_T("Above normal"));
    pBox- >AddString(_T("Highest"));
    pBox- >AddString(_T("Timecritical"));
    pBox- >SetCurSel(3); 
    }
    //初始化计数线程优先级组合框并置缺省为NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_CNTRTHRDPRIORITY);
    ASSERT(pBox != NULL);
    if (pBox != NULL){
        pBox- >AddString(_T("Idle"));
        pBox- >AddString(_T("Lowest"));
        pBox- >AddString(_T("Below normal"));
        pBox- >AddString(_T("Normal"));
        pBox- >AddString(_T("Above normal"));
        pBox- >AddString(_T("Highest"));
        pBox- >AddString(_T("Timecritical"));
        pBox- >SetCurSel(3); 
    }
    //初始化线程挂起复选框为挂起状态
    CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
    pCheck- >SetCheck(1);
    //初始化线程
    m_pDisplayThread = (CDisplayThread*)
        AfxBeginThread(RUNTIME_CLASS(CDisplayThread), 
                    THREAD_PRIORITY_NORMAL,
                    0, 
                    CREATE_SUSPENDED);
    m_pDisplayThread- >SetOwner(this);

m_pCounterThread = (CCounterThread*)
        AfxBeginThread(RUNTIME_CLASS(CCounterThread), 
                    THREAD_PRIORITY_NORMAL,
                    0, 
                    CREATE_SUSPENDED);
    m_pCounterThread- >SetOwner(this);
    ……
}
    然后填加成员函数:
    void AddToListBox(LPCTSTR szBuffer);//用于填加列表框显示
    该函数的实现代码为:
void CMutexesDlg::AddToListBox(LPCTSTR szBuffer)
{
    CListBox* pBox = (CListBox*) GetDlgItem(IDC_DATABOX);
    ASSERT(pBox != NULL);
    if (pBox != NULL){
        int x = pBox- >AddString(szBuffer);
        pBox- >SetCurSel(x);
        if (pBox- >GetCount() > 100)
            pBox- >DeleteString(0);
    }
}
---- 然后利用ClassWizard 填加用于调整进程优先级、两个线程优先级以及用于复选线程挂起的函数。
---- 调整进程优先级的代码为:

void CMutexesDlg::OnSelchangePriorityclass() 
{
    DWORD dw;
    //取焦点选项
CComboBox* pBox = (CComboBox*)
GetDlgItem(IDC_PRIORITYCLASS);
    int nCurSel = pBox- >GetCurSel();
    switch (nCurSel)
    {
    case 0:
        dw = IDLE_PRIORITY_CLASS;break;
    case 1:
    default:
        dw = NORMAL_PRIORITY_CLASS;break;
    case 2:
        dw = HIGH_PRIORITY_CLASS;break;
    case 3:
        dw = REALTIME_PRIORITY_CLASS;break;
    }
    SetPriorityClass(GetCurrentProcess(), dw);//调整优先级
}
---- 由于调整两个线程优先级的代码基本相似,单独设置一个函数根据不同的ID 来调整线程优先级。该函数代码为:
void CMutexesDlg::OnPriorityChange(UINT nID)
{
    ASSERT(nID == IDC_CNTRTHRDPRIORITY || 
        nID == IDC_DSPYTHRDPRIORITY);
    DWORD dw;
    //取对应该ID的焦点选项
    CComboBox* pBox = (CComboBox*) GetDlgItem(nID);
    int nCurSel = pBox- >GetCurSel();
    switch (nCurSel)
    {
    case 0:
        dw = (DWORD)THREAD_PRIORITY_IDLE;break;
    case 1:
        dw = (DWORD)THREAD_PRIORITY_LOWEST;break;
    case 2:
dw = (DWORD)THREAD_PRIORITY_BELOW_NORMAL;break;
    case 3:
    default:
        dw = (DWORD)THREAD_PRIORITY_NORMAL;break;
    case 4:
dw = (DWORD)THREAD_PRIORITY_ABOVE_NORMAL;break;
    case 5:
        dw = (DWORD)THREAD_PRIORITY_HIGHEST;break;
    case 6:
        dw = (DWORD)THREAD_PRIORITY_TIME_CRITICAL;break;
    }
    if (nID == IDC_CNTRTHRDPRIORITY)
        m_pCounterThread- >SetThreadPriority(dw);
//调整计数线程优先级
    else
        m_pDisplayThread- >SetThreadPriority(dw);
//调整显示线程优先级
}
    这样线程优先级的调整只需要根据不同的ID来调用该函数:
void CMutexesDlg::OnSelchangeDspythrdpriority()
{ OnPriorityChange(IDC_DSPYTHRDPRIORITY);}
void CMutexesDlg::OnSelchangeCntrthrdpriority() 
{ OnPriorityChange(IDC_CNTRTHRDPRIORITY);}
    复选线程挂起的实现代码如下:
void CMutexesDlg::OnPause() 
{
    //取挂起复选框状态
    CButton* pCheck = (CButton*)GetDlgItem(IDC_PAUSE);
    BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
    if (bPaused)    {
        m_pCounterThread- >SuspendThread();
        m_pDisplayThread- >SuspendThread();
    }//挂起线程
    else    {
        m_pCounterThread- >ResumeThread();
        m_pDisplayThread- >ResumeThread();
    }//恢复线程运行
}
---- 程序在::OnClose() 中实现了线程的终止。在本例程当中对线程的终止稍微复杂些。需要注意的是成员变量m_bDone 的作用,在线程的运行当中循环检测该变量的状态,最终引起线程的退出。这样线程的终止是因为函数的退出而自然终止,而非采用强行终止的方法,这样有利于系统的安全。该程序中使用了PostMessage 函数,该函数发送消息后立即返回,这样可以避免阻塞。其实现的代码为:
    void CMutexesDlg::OnClose() 
{
    int nCount = 0;
    DWORD dwStatus;
    //取挂起复选框状态
    CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
    BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
    
if (bPaused == TRUE){
        pCheck- >SetCheck(0);//复选取消
        m_pCounterThread- >ResumeThread();
//恢复线程运行
        m_pDisplayThread- >ResumeThread();
    }

if (m_pCounterThread != NULL){
VERIFY(::GetExitCodeThread(m_pCounterThread- >
m_hThread, &dwStatus));//取计数线程结束码
        if (dwStatus == STILL_ACTIVE){
            nCount++;
            m_pCounterThread- >m_bDone = TRUE;
        }//如果仍为运行状态,则终止
        else{
            delete m_pCounterThread;
            m_pCounterThread = NULL;
        }//如果已经终止,则删除该线程对象
    }
    
    if (m_pDisplayThread != NULL){
VERIFY(::GetExitCodeThread(m_pDisplayThread- >
m_hThread, &dwStatus));//取显示线程结束码
        if (dwStatus == STILL_ACTIVE){
            nCount++;
            m_pDisplayThread- >m_bDone = TRUE;
        }//如果仍为运行状态,则终止
        else{
            delete m_pDisplayThread;
            m_pDisplayThread = NULL;
        }//如果已经终止,则删除该线程对象
    }
    if (nCount == 0)//两个线程均终止,则关闭程序
CDialog::OnClose();
    else //否则发送WM_CLOSE消息
PostMessage(WM_CLOSE, 0, 0);
}
---- 在例程具体实现中用到了许多函数,在这里不一一赘述,关于函数的具体意义和用法,可以查阅联机帮助。

Windows 95多线程间同步事件的控制方法

在设计多线程应用程序中有时必须在线程之间保持一定的同步关系,才能使用户能够对独立运行的线程进行有效的控制。为此本文在简要介绍Windows 95中线程的概念及其创建方法后,提出了一种在多线程之间利用event对象实现事件同步的控制方法。最后还介绍了在不同应用程序之间进行同步事件控制的方法,这种方法使得不同应用程序进行相互间的同步事件控制变得很简单。

  关键词:Windows95线程同步事件event对象Win32

  一,引言Windows 95是一个多任务、多线程的操作系统,其中的每一个应用程序都是一个进程(process)。进程可以创建多个并发的线程(thread),同时进程也以主线程(primary thread)的形式被系统调度。所谓的线程是系统调度的一个基本单位,在程序中线程是以函数的形式出现的,它的代码是进程代码的一部分,并与进程及其派生的其它线程共享进程的全局变量和文件打开表等公用信息。主线程类似于UNIX系统中的父进程,线程则类似于子进程。主线程也是一个线程,称作主线程仅仅是为了和它创建的线程区别开来。

  每个线程都相对于主线程而独立运行,为了使得线程能对用户的控制作出响应,必须控制线程的运行,比如用户可暂停、终止一个线程的运行或改变线程运行的条件等。而且在用户控制与线程运行之间有时应该有一定的同步控制关系,以保证用户对线程的有效控制。线程可以根据不同的条件对用户的控制作出不同的响应。为了实现上述目的必须使用系统提供的同步对象(Synchronization Object),如event对象。编写多线程应用程序必须使用Win32 API。

  二,线程的创建方法调用Win32 API中的CreateThread函数创建线程。

  hThread=CreateThread(NULL,0,&TEventWindow::ThreadFunc,this,0,&hThreadId); 第一个参数设定线程的安全属性,因其仅用于Windows NT,故不设定。第二个参数为0 指定线程使用缺省的堆栈大小。第三个参数指定线程函数,线程即从该函数的入口处开始运行,函数返回时就意味着线程终止运行。第四个参数为线程函数的参数,可以是指向任意数据类型的指针。第五个参数设定线程的生成标志。hThreadId存放线程的标识号。

  线程函数如下定义,上述的this参数是指向线程所属窗口的句柄指针,通过thrdWin 参数传送过来,利用这个指针再调用相应的LoopFunc函数,线程的具体事务都在这个函数中执行。

BOOL TEventWindow::CanClose(){

HANDLE Signa [2]={hThread,hNoCloseEvent};

SetEvent(hCloseEvent);

if(WaitForMultipleObjects(2,SignalsC,FALSE,-1)==0) return TRUE;

else return FALSE;

}

  三,线程的同步事件控制方法Windows 95提供两种基本类型的系统对象,一种是彼此互斥的对象,用来协调访问数据,如mutex对象;一种是事件同步对象,用来发送命令或触发事件,安排事件执行的先后次序,如event对象。系统对象在系统范围内有效,它们都具有自己的安全属性、访问权限和以下两种状态中的一种:Signaled和nonSignaled。对于event对象调用SetEvent函数可将其状态设为Signaled,调用ResetEvent函数则可将其状态设为nonSignaled。

  演示程序中的线程在一个大循环中不断地将运行结果显示出来,当用户要关闭窗口时线程才终止运行。不过必须在窗口关闭之前先终止线程的运行,否则线程运行的结果将会显示在屏幕的其他地方,所以有必要在线程结束与关闭窗口这两个事件之间建立起同步关系。为此在TEventWindow类的构造函数中创建两个event对象,用来实现事件同步。

  hCloseEvent=CreateEvent(0,FALSE,FALSE,0); hNoCloseEvent=CreateEvent(0,FALSE,FALSE,0); 第二个参数为FALSE表示创建的是一个自动event对象,第三个参数为FALSE表示对象的初始状态为nonSignaled,第四个参数为0表示该对象没有名字。在TEventWindow类的构造函数中还同样创建hWatchEvent和hNtyEvent对象,初始状态都为nonSignaled。

  用户要关闭窗口时,程序首先调用CanClose函数,在该函数中设置hCloseEvent对象的状态为Signaled,利用这个方法来通知线程,要求线程终止运行。然后主线程调用函数WaitForMultipleObjects(该函数以下简称wait函数),wait函数先判断对象hThread和hNoCloseEvent中任意一个的状态是否为Signaled,如果都不是就堵塞主线程的运行,直到上述条件满足;如果有一个对象的状态为Signaled,wait函数就返回,不再堵塞主线程。

  如果对象是自动event对象,wait函数在返回之前还会将对象的状态设为nonSignaled。

  wait函数中的参数FALSE表示不要求两个对象的状态同时为Signaled,参数-1表示要无限期地等待下去直到条件满足,参数2表示SignalsC数组中有两个对象。在Windows 95 中线程也被看作是一种系统对象,同样具有两种状态。线程运行时其状态为nonSignaled,如果线程终止运行,则其状态被系统自动设为Signaled(可以通过线程的句柄hThread得到线程状态),此时wait函数返回0,表示第一个对象满足条件,于是CanClose返回TRUE 表示窗口可以关闭;如果线程不能满足终止运行的条件,就设置hNoCloseEvent对象的状态为Signaled,此时wait函数返回1,表示第二个对象满足条件,于是CanClose返回FALSE 表示窗口暂时还不能关闭。

  BOOL TEventWindow::CanClose(){ HANDLE SignalsC[2]={hThread,hNoCloseEvent}; SetEvent(hCloseEvent); if(WaitForMultipleObjects(2,SignalsC,FALSE,-1)==0) return TRUE; else return FALSE; } 另一个用户控制的例子是,用户使主线程暂停运行直到线程满足某种条件为止。比如用户选择“Watch”菜单后,主线程调用如下函数开始对线程的运算数据进行监测。首先设置hWatchEvent对象的状态为Signaled,以此来通知线程,主线程此时已进入等待状态并开始对数据进行监测,然后主线程调用wait函数等待线程的回应。线程在满足某个条件后就设置hNtyEvent对象的状态为Signaled,使主线程结束等待状态,继续运行。

  void TEventWindow::CmWatch(){ SetEvent(hWatchEvent); WaitForSingleObject(hNtyEvent,-1); ::MessageBox(GetFocus(),"线程已符合条件,主线程继续运行!","",MB_OK); } 线程函数所调用的LoopFunc是一个大循环,它不断地判断同步对象的状态,并根据这些对象的状态执行相应的操作,这些对象在数组SignalsL中列出。在这个数组中各元素的排列顺序是很重要的,前两个对象分别对应两种不同的用户控制事件,通过判断对象的状态可以知道发生的是哪一种用户控制。只有当前面两个对象的状态都不是Signaled时才会判断第三个对象的状态,这样一方面保证线程能检测到所有的用户控制事件,另一方面又保证了在不发生用户控制事件时线程也能继续运行。为此特地在TEventWindow类的构造函数中创建的对象hNoBlockEvent的状态始终为Signaled。

  hNoBlockEvent=CreateEvent(0,TRUE,TRUE,"MyEvent"); 第二个参数为TRUE表示创建的是一个手工event对象,其状态是不会被wait函数所改变的,除非显式地调用ResetEvent函数。第三个参数为TRUE表示对象初始状态为Signaled,第四个参数定义了该对象的名字为“MyEvent”。

  LoopFunc函数调用wait函数,如果检测到hCloseEvent的状态为Signaled,此时wait 函数返回0,线程知道用户要关闭窗口了,就判断线程是否可以终止,条件是iCount$#@62;100,如果满足终止条件LoopFunc函数就返回,实际上就终止了线程的运行;如果不满足条件线程就设置hNoCloseEvent对象的状态为Signaled,让主线程知道线程暂时还不能终止。由于hCloseEvent是自动event对象,所以wait函数返回0时还会将对象hCloseEvent的状态设置为nonSignaled,这样在第二次循环时,wait函数就不会判断出hCloseEvent对象的状态为Signaled,避免了线程错误地再次去判断是否会满足终止条件。如果wait函数检测到对象hWatchEvent的状态为Signaled,此时wait函数返回1,线程知道主线程已进入等待状态并在对数据进行监测,就设置变量bWatch的值为TRUE。如果前面的两个事件都未发生,则前面两个对象的状态都为nonSignaled,于是wait函数就检测第三个对象的状态,由于第三个对象hNoBlockEvent的状态始终为Signaled,所以线程就无阻碍地继续运行下去,将变量iCount不断加一,当变量大于200时,如果bWatch为TRUE,就设置hNtyEvent的状态为Signaled,从而使主线程停止等待,继续运行。

DWORD TEventWindow::LoopFunc(){

HANDLE SignalsL[3]={hCloseEvent,hWatchEvent,hNoBlockEvent};

static BOOL bWatch=false;int dwEvent;

while(1){

dwEvent=WaitForMultipleObjects(3,SignalsL,FALSE,-1);

switch(dwEvent){

case 0: if(iCount$#@62;100) return 0;

else SetEvent(hNoCloseEvent);

break;

case 1: bWatch=TRUE;break;

case 2: ++iCount;

if(bWatch && iCount$#@62;200) SetEvent(hNtyEvent);

break;

}

}

}

  四,进程间的多线程同步事件控制方法由于event对象是系统范围内有效的,所以另一个进程(即一个应用程序,本身也是一个线程)可调用OpenEvent函数,通过对象的名字获得对象的句柄,但对象必须是已经创建的,然后可将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。

  这样可以实现一个进程的线程控制另一进程生成的线程的运行。如下面的语句就是通过对象名字“MyEvent”获得了上面进程生成的hNoBlockEvent对象的句柄,再使用这个句柄将对象状态设为nonSignaled。在上述的LoopFunc函数中由于该对象的状态已经改变,使得上面的线程暂停运行。

  HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); ResetEvent(hEvent); OpenEvent函数的第一个参数表示函数的调用线程对event对象的访问权限,比如让线程拥有对象所有的访问权限,就选参数EVENT_ALL_ACCESS,这样线程就能用ResetEvent 函数改变对象的状态;参数true表示由这个进程派生的子进程可以继承该句柄;最后一个?

  数指出了event对象的名字。用下面的语句设置对象hNoBlockEvent的状态为Signaled,就可以使线程继续运行,如SetEvent(hEvent)。

  进程不再使用该句柄时尽可以用CloseHandle函数关闭对象句柄,但对于同一个event 对象而言,因为它可能还在别的线程中被使用,所以只有在它的所有被引用的句柄都关闭后对象才会被系统释放.

  五,结束语多线程编程技术在多媒体、网络通讯、数学计算和实时控制方面有着很广阔的应用前景。当然在实际编程中情况往往是很复杂的,这时应注意的是如何将任务准确地划分成可并发的线程以及象文中提到的SignalsL数组中元素的排列顺序等问题。本文所讲内容对于在Windows NT或在某些支持多线程的UNIX系统中设计多线程应用程序也是有所帮助的。

多线程技术在VC++串口通信程序中的应用研究

1 概述

  在现代的各种实时监控系统和通信系统中,在Windows 9X/NT下利用VC++对RS-232串口编程是常用的手段。Windows 9X/NT是抢先式的多任务操作系统,程序对CPU的占用时间由系统决定。多任务指的是系统可以同时运行多个进程,每个进程又可以同时执行多个线程。进程是应用程序的运行实例,拥有自己的地址空间。每个进程拥有一个主线程, 同时还可以建立其他的线程。线程是操作系统分配CPU时间的基本实体,每个线程占用的CPU时间由系统分配,系统不停的在线程之间切换。进程中的线程共享进程的虚拟地址空间,可以访问进程的资源,处于并行执行状态,这就是多线程的基本概念。

  2 VC++对多线程的支持

  使用MFC开发是较普遍的VC++编程方法。在VC++6.0下,MFC应用程序的线程由CWinThread对象表示。VC++把线程分为两种:用户界面线程和工作者线程。用户界面线程能够提供界面和用户交互,通常用于处理用户输入并相应各种事件和消息;而工作者线程主要用来处理程序的后台任务。

  程序一般不需要直接创建CWinThread对象,通过调用AfxBeginThread()函数就会自动创建一个CWinThread对象,从而开始一个进程。创建上述的两种线程都利用这个函数。

  线程的终止取决于下列事件之一:线程函数返回;线程调用ExitThread()退出;异常情况下用线程的句柄调用TerminateThread()退出;线程所属的进程被终止。
3 多线程在串口通信中的应用

  3.1 串口通信对线程同步的要求

  因为同一进程的所有线程共享进程的虚拟地址空间,而在Windows 9X/NT系统下线程是汇编级中断,所以有可能多个线程同时访问同一个对象。这些对象可能是全局变量,MFC的对象,MFC的API等。串口通信的几个特点决定了必须采用措施来同步线程的执行。

  串口通信中,对于每个串口对象,只有一个缓冲区,发送和接收都要用到,必须建立起同步机制,使得在一个时候只能进行一种操作,否则通信就会出错。

  进行串口通信处理的不同线程之间需要协调运行。如果一个线程必须等待另一个线程结束才能运行,则应该挂起该线程以减少对CPU资源的占用,通过另一进程完成后发出的信号(线程间通信)来激活。

  VC++提供了同步对象来协调多线程的并行,常用的有以下几种:

   CSemaphore:信号灯对象,允许一定数目的线程访问某个共享资源,常用来控制访问共享资源的线程数量。

   Cmutex:互斥量对象,一个时刻至多只允许一个线程访问某资源,未被占用时处于有信号状态,可以实现对共享资源的互斥访问。

   CEvent:事件对象,用于使一个线程通知其他线程某一事件的发生,所以也可以用来封锁对某一资源的访问,直到线程释放资源使其成为有信号状态。适用于某一线程等待某事件发生才能执行的场合。

   CCriticalSection:临界区对象,将一段代码置入临界区,只允许最多一个线程进入执行这段代码。一个临界区仅在创建它的进程中有效。

  3.2 等待函数

  Win32 API提供了能使线程阻塞其自身执行的等待函数,等待其监视的对象产生一定的信号才停止阻塞,继续线程的执行。其意义是通过暂时挂起线程减少对CPU资源的占用。在某些大型监控系统中,串口通信只是其中事务处理的一部分,所以必须考虑程序执行效率问题,当串口初始化完毕后,就使其处于等待通信事件的状态,减少消耗的CPU时间,提高程序运行效率。

  常用的等待函数是WaitForSingleObject()和WaitForMultipleObjects(),前者可监测单个同步对象,后者可同时监测多个同步对象。

  3.3 串口通信的重叠I/O方式

  MFC对于串口作为文件设备处理,用CreateFile()打开串口,获得一个串口句柄。打开后SetCommState()进行端口配置,包括缓冲区设置,超时设置和数据格式等。成功后就可以调用函数ReadFile()和WriteFile()进行数据的读写,用WaitCommEvent()监视通信事件。CloseHandle()用于关闭串口。

  在ReadFile()和WriteFile()读写串口时,可以采取同步执行方式,也可以采取重叠I/O方式。同步执行时,函数直到执行完毕才返回,因而同步执行的其他线程会被阻塞,效率下降;而在重叠方式下,调用的读写函数会立即返回,I/O操作在后台进行,这样线程就可以处理其他事务。这样,线程可以在同一串口句柄上实现读写操作,实现"重叠"。

  使用重叠I/O方式时,线程要创建OVERLAPPED结构供读写函数使用,该结构最重要的成员是hEvent事件句柄。它将作为线程的同步对象使用,读写函数完成时hEvent处于有信号状态,表示可进行读写操作;读写函数未完成时,hEvent被置为无信号。

4 程序关键代码的实现

  程序专门建立了一个串口通信类,下面给出关键成员函数的核心代码。

BOOL InitComm file://串口初始化,这里只给出关键步骤的代码,下同
{
 HANDLE m_hComm;
 COMMTIMEOUTS m_CommTimeouts;
 m_hComm = CreateFile("COM1", file://在这里只使用串口1
  GENERIC_READ | GENERIC_WRITE, file://打开类型为可读写
  0, file://以独占模式打开串口
  NULL, file://不设置安全属性
  OPEN_EXISTING,
  FILE_FLAG_OVERLAPPED, file://重叠I/O方式
  0);
 if (m_hComm == INVALID_HANDLE_VALUE) file://打开不成功
  {return FALSE;}
 m_CommTimeouts.ReadIntervalTimeout = 1000; 
 file://进行超时设置,读者应根据自己的实际需要设置
 m_CommTimeouts.ReadTotalTimeoutMultiplier = 500;
 m_CommTimeouts.ReadTotalTimeoutConstant = 5000;
 m_CommTimeouts.WriteTotalTimeoutMultiplier = 500;
 m_CommTimeouts.WriteTotalTimeoutConstant = 5000;
 if (!SetCommTimeouts(m_hComm, &m_CommTimeouts))
  {CloseHandle(m_hComm);
   return FALSE;}
 PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); file://清缓冲
 return TRUE;
}

以上是专门针对COM1的初始化,如果要利用同一函数对不同串口初始化,则要在初始化前先进入代码临界区,以保证在某一时刻只进行一个串口的初始化。

  在串口初始化成功后,就可以建立监控线程处理串口通信事件。下面是该线程的关键代码。

UINT CommThread(LPVOID pParam) file://用于监控串口的工作者线程
{
 BOOL bResult = FALSE;
 if (m_hComm) file://查看端口是否打开,这里m_hComm同上,作者在这里做了简化
  PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT |    PURGE_TXABORT);
  for (;;) file://只要线程运行,就处于监视端口行为的无限循环
  {
   bResult = WaitCommEvent(m_hComm, &Event, &m_ov); 
   file://m_ov是OVERLAPPED类型的成员变量
   if (!bResult)
    { file://进行出错处理}
   else
   {
    Event = WaitForMultipleObjects(4, m_hEvent, FALSE, INFINITE);
    file://无限等待设定的事件发生,数组m_hEvent根据需要定义了须响应的接收,发送,关闭端口事件和OVERLAPPED类型的hEvent事件
    switch (Event)
    { file://读写事件的响应处理过程,在此略}
    }
    return 0;
 }

这样监控主程序就可以使用AfxBeginThread()函数来产生CommThread串口监控线程。如果要实现对所有端口的同时监控,可以分别对端口建立监控线程。

  5 小结

  作为一个机房监控系统的组成部分,本串口通信程序在VC++6.0下编译通过,在使用windows 98/NT的局域网里运行良好。

虚拟设备驱动程序开发篇

打开”金笼子”-VXD技术漫谈

说起 VxD 技术,很多接触电脑的人都会感到陌生,不过提起CIH病毒,我相信您一定不再摇头。

  有过一定Windows编程经验的人可能听说过 VxD 这个词。80386的保护模式给了我们非常多的“保护”,对于一个不懈地追求自由的人来说,“镀金的笼子也是笼子”,翻遍Windows编程的书籍,也许最终只找到这样一扇通向自由的门:试一试 VxD 。

  
VxD是什么?为什么CIH会如此猖獗?

  CIH运行在Ring 0,挂接IFS(Installable File System)服务,截获File Opening操作,然后把自己“传染”到别的PE格式的文件中,如果到了该发作的时间,它就攻击主板的Flash Memory。

  CIH之所以能破坏硬件资源(通过攻击主板的Flash Memory,达到破坏硬件的目的),正是因为它利用了 VxD 技术。对于一般的运行于Ring 3的Windows应用程序来说,想破坏主板的Flash Memory是不可能的,因为这是80386保护模式对于代码权限的盘查所不允许的,其结果顶多是引起操作系统异常,也就是在Windows中经常出现的、令用户心跳加速的、蓝底白

  VxD = Virtual “something" Device,这里的x代表Something。比如说VKD代表Virtual Keyboard Device(键盘驱动),VPICD 代表 Virtual Programmable Interrupt Device(可编程中断控制器驱动)。VxD是Windows 3.1、Windows 95的驱动程序模型(Windows 98也支持VxD)。

  在很多人的印象中,VxD是与硬件设备打交道的,对于软件来说没有太大的作用,其理由之一是:用来开发VxD的DDK一般来说都分发给硬件开发商,对于搞软件开发的人来? SDK才是看家本领,而SDK中对于VxD却是言及甚少。这种想法可是大错而特错了。

  由于VxD运行在系统的Ring 0级,拥有与操作系统同等的级别,所以,VxD可以最大程度地帮助我们走出80386保护模式强加给我们的“镀金的笼子”。我们就拿最近轰动一时的CIH病毒来说一说VxD这柄利剑的强大。字的“General Protection Fault”,在这里,我们该感谢那个“镀金的笼子”,至少它使我们免受邻居家那个只学过Turbo C编程的小孩的搔扰。

除了搞破坏, VxD还可以用来做些什么呢?

  在这里我简单谈一下 VxD 的应用。

  由于 VxD 可以虚拟根本不存在的硬件,因此,可以利用 VxD 虚拟硬件狗来破解一些有版权的软件的保护(怎么又是干坏事?我只是想告诉硬件狗的制造者:有些吝啬鬼想通过这种手段来省银子,早作提防啊!)。

  由于 VxD 工作在操作系统的最底层,所以,掌握它 能使你具备扩展操作系统的能力。有没有想过在Windows 95里按下Ctrl+Alt+Delete键,而后弹出的是你自己设计的对话框(聪明的你一定可以发挥你天才的想像力,比如说显示一张你女朋友的照片,以提醒自己关了电脑之后别忘了去赴约会), VxD 可以帮助你跟Windows 95开开玩笑。

  好吧,说点正经的应用。想不想利用 VxD 来作些“实时”控制之类的东西?当然,作实时工控,首选的操作系统还是DOS,但是如果你经不住Windows那张漂亮面孔的诱惑,也可以在Windows下作“准实时”控制应用。一般来说,通过 VxD 的协助,在很多情况下,我们还是可以获得比较满意的实时效果的。据测试,486/66的PC机上运行Windows 95,在没有别的应用程序干扰的情况下, VxD 可以处理10kHz的硬件中断,不会漏掉一个中断!精心设计你的应用程序和 VxD (最好再买台好点的电脑),相信你可以获得满意的“实时”效果。

  再讲一个有趣的应用吧(今天有点发晕了,平时我可不大爱吹牛的)。如果你有一堆16位的DLL,(假设你曾在Windows 31下作过应用程序开发,并曾为讨老板的欢心而奋力地写过许多功能不错的DLL),而你又懒得通过重写代码或者利用烦琐的Thunk移值到Windows 95下,那么,试一试 VxD ,可能会给你“它山之石可攻玉”的经历。当然,我的前提是你对于 VxD 已经有了比较多的认识并有一些实战经验,否则你大概会因此导致神经衰弱的。

  再说一个比较有趣的应用。想不想使DOS TSR、Win16应用程序、Win32应用程序之间产生一些协作?想不想打破Win32应用程序4GB独立线性地址空间带来的限制?通过 VxD 作中介,你会发现原来“万物通于一理”。别相信坐在你对面的那个权威的“No”,相信你自己。

  对于 VxD ,我们已经说得够多了,也许你已经兴奋得想立刻去书摊上找本《21天轻轻松松掌握 VxD 技术》来一读为快了。先安静会儿,现在开始泼冷水。

VxD的明天依然美好吗?

  1996年的Windows Hardware Engineering Conference(WinHEC)会议上,Microsoft宣布了一种新的Windows设备驱动程序模型——Win32 Driver Model(WDM)。这种新的设备驱动程序模型将成为Windows 2000(即Windows NT 5.0)的核心。

  这个消息令从事Windows设备驱动程序(VxD)开发的人感到沮丧(虽然大家早已预料到Windows系列与Windows NT系列最终将走到一起)。WDM的浮出水面把一个吃尽苦头的VxD开发者又推到了一个新的起跑线上。如果你曾去DDK的汇编代码里观光过,你一定可以体会这个消息对VxD开发者是个沉重的打击,而对于Windows NT设备驱动程序(Kernel Mode Driver)者来说,却是另一番心情——因为WDM基本等于Kernel Mode Driver+Plug and Play。

  VxD将让位于WDM,现在令我们欣慰的是Microsoft宣布Windows 98(Windows 98支持VxD)可能会坚持到200X年,Windows 2000的诞生也许在2000年之后。在这期间,掌握VxD技术的你还是可以主动要求老板给你加薪的。即使到了WDM一统天下之时,也不用灰心,因为无论是VxD还是WDM,都要求开发人员对计算机硬件有着全面而细致的了解。通过VxD的锻炼,你至少熟悉了计算机的硬件资源并对保护模式有了比较深刻的认识,这些东西都是将来从事WDM开发的硬功夫。

  好了,该说说Windows NT了。在Windows NT中,80386保护模式的“保护”比Windows 95中更坚固,这个“镀金的笼子”更加结实,更加难以打破。在Windows 95中,至少应用程序I/O操作是不受限制的,而在Windows NT中,我们的应用程序连这点权限都被剥夺了。

  Windows NT的驱动程序模型与Windows 3.1、Windows 95是截然不同的。所以说,如果有人告诉你,他的运行Windows NT的机器被CIH破坏了,你可以充一回专家,教训教训他:“这是不可能的,记住,CIH利用的是VxD技术,而VxD在Windows NT中是跑不动的”。

  在Windows NT中,存在三种Device Driver:

  1.“Virtual device Driver” (VDD)。通过VDD,16位应用程序,如DOS 和Win16应用程序可以访问特定的I/O端口(注意,不是直接访问,而是要通过VDD来实现访问)。

  2.“GDI Driver”,提供显示和打印所需的GDI函数。

  3.“Kernel Mode Driver”,实现对特定硬件的操作,比如说CreateFile, CloseHandle (对于文件对象而言), ReadFile, WriteFile, DeviceIoControl 等操作。“Kernel Mode Driver”还是Windows NT中唯一可以对硬件中断和DMA进行操作的Driver。SCSI miniport Driver和 网卡NDIS Driver都是Kernel Mode Driver的一种特殊形式。

 好了,就说到这里吧,对于Kernel Mode Driver我也知之甚少。

  如果你对Windows设备驱动程序开发有兴趣的话,建议你去如下的Web站点去逛逛。
VxD技术及其在实时反病毒中的应用

indows9x平台反病毒产品大多属静态反病毒软件,指导思想是"以杀为主",这一方式的缺点是病毒在被清除之前可能早已造成了严重危害一个好的反病毒软件应该是"以防为主,以杀为辅",在病毒入侵时就把它清除掉,这就是实时反病毒技术。

  Windows9x使用IntelCPU的Ring0和Ring3两个保护级。系统进程运行于Ring0,因而具有对系统全部资源的访问权和管理权;而普通用户进程运行于Ring3,只能访问自己的程序空间,不允许对系统资源进行直接访问许多操作受到限制。显然这种普通用户进程是无法胜任实时反病毒工作的,必须使后台监视进程运行在Ring0优先级,实现这一目的基础就是VxD技术。

  一、VxD技术的特点

  VxD即虚拟设备驱动程序,用作Windows9x系统和物理设备之间的接口。但它不仅适用于硬件设备,也适用于按VxD规范所编制的各种软件"设备"

  VxD技术的实质是:通过加载具有Ring0最高优先级的VxD,运行于Ring3上的应用程序能够以一定的接口控制VxD的动作,从而达到控制系统的目的。实时反病毒软件之所以要使用VxD技术,关键有二:(1)VxD拥有系统最高运
行权限(2)许多Windows9x系统底层功能只能在VxD中调用,应用程序如果要用必须编个VxD作为中介。VxD作为应用程序在系统中的一个代理,应用程序通过它来完成任何自己本身做不到的事情,通过这一手段,Windows9x系统为普通应用程序留下了扩充接口。很不幸,这一技术同样为病毒所利用,CIH病毒正是利用了VxD技术才得以驻留内存、传染执行文件、毁坏硬盘和FlashBIOS。

  Windows9x系统下有众多的VxD,每个VxD可提供4种服务,即PM(保护模式)API、V86(虚拟86)API、Win32服务和VxD服务,前3种分别供应用程序在16位保护模式、V86模式以及32位保护模式下调用,VxD服务则只供其他VxD使用用户开发的VxD可提供任意上述服务。除此之外,应用程序还可通过调用API函数DeviceIoControl与支持IOCTL接口的VxD进行通信,执行Win32API不支持的系统低级操作。

 二、VxD技术的实现

  VxD的操作基于寄存器,所以一般用汇编语言编写,它的关键部分是一个和普通窗口的消息处理过程WndProc相类似的控制过程,不同之处在于它的处理对象是系统发来的控制消息。这些消息共51种,在VxD自加载至卸出整个生命周期内,操作系统不断向它发送各种控制消息,VxD根据自己的需要选择处理,其余的忽略。系统向VxD发送控制消息时将? 代号放在EAX寄存器中并在EBX寄存器中放系统虚拟机(VM)句柄。

  对动态VxD来说,最重要的消息有三个:SYS_DYNAMIC_DEVICE_INIT、SYS_DYNAMIC_DEVICE_EXIT以及W32_DEVICEIOCONTROL,消息代号分别是1Bh、1Ch、23h。当VxD被动态加载至内存时。
  系统向其发送SYS_DYNAMIC_DEVICE_INIT消息,VxD应在此时完成初始化设置并建立必要的数据结构;当VxD将被卸出内存时,系统向其发送SYS_DYNAMIC_DEVICE_EXIT消息VxD在收到后应清除所作设置并释放相关数据结构;当应用程序调用API函数DeviceIoControl与VxD进行通信时,系统向VxD发送W32_DEVICEIOCONTROL消息,它是应用程序和VxD联系的重要手段,此时ESI寄存器指向一个DIOCParams结构,VxD从输入缓冲区获取应用程序传来数据,相应处理后将结果放在输出缓冲区回送应用程序,达到相互传递数据的目的。

  应用程序向VxD发出DeviceIoControl调用时,第2个参数用于指定进行何种控制,控制过程从DIOCParams结构+0Ch处取得此控制码再进行相应处理控制码的代号和含义由应用程序和VxD自行约定,系统预定义了DIOC_GETVERSION (0)和DIOC_CLOSEHANDLE(-1)两个控制码,当应用程序调用API函数CreateFile("\\.\VxDName",...)动态加载一VxD时,系统首先向该VxD的控制过程发送SYS_DYNAMIC_DEVICE_INIT控制消息,若VxD返回成功,系统将再次向VxD发送带有控制码DIOC_OPEN(即DIOC_GETVERSION,值为0)的W32_DEVICEIOCONTROL消息以决定此VxD是否能够支持设备IOCTL接口,VxD必须清零EAX寄存器以表明支持IOCTL接口,这时CreateFile将返回一个设备句柄hDevice,通过它应用程序才能使用DeviceIoControl函数对VxD进行控制。
  同一个VxD可用CreateFile打开多次,每次打开时都会返回此VxD的一个唯一句柄,但是系统内存中只保留一份VxD,系统为每个VxD维护一个引用计数,每打开一次计数值加1。当应用程序调用API函数CloseHandle(hDevice)关闭VxD句柄时,VxD将收到系统发来的带控制码DIOC_CLOSEHANDLEW32_DEVICEIOCONTROL消息,同时该VxD的引用计数减1,当最终引用计数为0时,系统向VxD发送控制消息SYS_DYNAMIC_DEVICE_EXIT,然后将其从内存中清除。在极少数情况下应用程序也可调用API函数DeleteFile("\\.\VxDName")忽略引用计数的值直接将VxD卸出内存,这将给使用同一VxD的其他应用程序造成毁灭性影响,应避免使用。

 --一个典型的VxD控制过程代码如下:

 BeginProcVXD_Control
 cmpeax,1Bh
 ;SYS_DYNAMIC_DEVICE_INIT消息
 jzvxd_dynamic_init_handle
 cmpeax,1Ch
;SYS_DYNAMIC_DEVICE_EXIT消息
jzvxd_dynamic_exit_handle
cmpeax,23h
;W32_DEVICEIOCONTROL消息
jnzexit_control_proc
movecx,[esi+0Ch]
;从DIOCParams+0Ch处取控制码
....
;处理控制码
EndProcVXD_Control

 三、实时反病毒的关键技术-FileHooking

  应用程序通过使用动态加载的VxD,间接获得了对Windows9x系统的控制权,但要实现对系统中所有文件I/O操作的实时监视,还要用到另一种关键技术-FileHooking,通过挂接一个处理函数,截获所有与文件I/O操作有关的系统调用。Windows9x使用32位保护模式可安装文件系统(IFS),由可安装文件系统管理器(IFSManager)协调对文件系统和设备的访问,它接收以Win32API函数调用形式向系统发出的文件I/O请求,再将请求转给文件系统驱动程序FSD,由它调用低级别的IOS系统实现最终访问。每个文件I/OAPI调用都有一个特定的FSD函数与之对应,IFSManager负责完成由API到FSD的参数装配工作,在完成文件I/OAPI函数参数的装配之后转相应FSD执行之前,它会调用一个称为FileSystemApiHookFunction的Hooker函数。通过安装自己的Hooker函数,就可以截获系统内所有对文件I/O的API调用,并适时对相关文件进行病毒检查,从而实现实时监控。

  上述过程由用户VxD调用系统VxDIFSMgr提供的服务完成,该VxD提供了丰富的底层文件操作功能:IFSMgr_InstallSyatemApiHook函数用来安装FileSystemApiHookFunction,IFSMgr_RemoveSystemApiHook用来卸除Hooker,IFSMgr_Ring0_FileIO用来对文件和磁盘扇区进行读写访问等等。当由IFSManager转入SystemApiHookFunction时,带有6个参数:

FileSystemApiHookFunction(
pIFSFuncFSDFnAddr,
//对应FSD服务函数地址
intFunctionNum,
//与API对应的FSD服务功能号(详见下面)
intDrive,
//驱动器代号(1=A,2=B,3=C...)
intResourceFlags,
//资源标志(详见下面)
intCodePage,
//代码页(0=ANSI,1=OEM)
pioreqpir
//指向IOREQ结构的指针
)

  参数中比较重要的是FSD功能号、驱动器号和IOREQ结构指针3项。如需截获某个文件I/OAPI调用,只需在Hooker中对相应FSD功能号进行处理

  系统中可挂接多个Hooker,形成一条链。IFSMgr_InstallFileSystemApiHook安装Hooker成功时返回前一个Hooker地址,每个Hooker在做特定处理后总应调用前一个Hooker,最后安装的Hooker最先被调用。在VxD中调用其他VxD服务采用INT20h指令后跟一个双字的特殊格式,其中高字为被调用VxD的ID号(系统VxD的ID固定),低字为该VxD之服务号,这一形式称为VxDcall,如:

int20h
dd00400043h
;VxDCallIFSMgr_InstallSystemApiHook
int20h
dd00400044h
;VxDCallIFSMgr_RemoveSystemApiHook r> 
int20h
dd00400043h
;VxDCallIFSMgr_InstallSystemApiHook
int20h
dd00400044h
;VxDCallIFSMgr_RemoveSystemApiHook emApiHook ok

虚拟设备驱动程序的设计与实现

indows对系统底层操作采取了屏蔽的策略,因而对用户而言,系统变得更为安全,但这却给众多的硬件或者系统软件开发人员带来了不小的困难,因为只要应用中涉及到底层的操作,开发人员就不得不深入到Windows的内核去编写属于系统级的虚拟设备驱动程序。Win98与Win 95设备驱动程序的机理不尽相同,Win98不仅支持与Windows NT 5.0兼容的WDM(Win32 Driver Mode)模式驱动程序,而且还支持与Win95兼容的虚拟设备驱动程序VxD(Virtual Device Driver)。下面介绍了基于Windows 9x平台的虚拟环境、虚拟设备驱动程序VxD的基本原理和设计方法,并结合开发工具VToolsD给出了一个为可视电话音频卡配套的虚拟设备驱动程序VxD的设计实例。

  1.Windows 9x的虚拟环境 

  Windows 9x作为一个完整的32位多任务操作系统,它不像Window 3.x那样依赖于MS-DOS,但为了保证软件的兼容性,Windows 9x除了支持Win16应用程序和Win32应用程序之外,还得支持MS-DOS应用程序的运行。Windows 9x是通过虚拟机VM(Virtual Machine)环境来确保其兼容和多任务特性的。

  所谓Windows虚拟机(通常简称为Windows VM)就是指执行应用程序的虚拟环境,它包括MS-DOS VM和System VM两种虚拟机环境。在每一个MS-DOS VM中都只运行一个MS-DOS进程,而System VM能为所有的Windows应用程序和动态链接库DLL(Dynamic Link Libraries)提供运行环境。每个虚拟机都有独立的地址空间、寄存器状态、堆栈、局部描述符表、中断表状态和执行优先权。虽然Win16、Win32应用程序都运行在System VM环境下,但Win16应用程序共享同一地址空间,而Win32应用程序却有自己独立的地址空间。

  在编写应用程序时,编程人员经常忽略虚拟环境和实环境之间的差异,一般认为虚拟环境也就是实环境。但是,在编写虚拟设备驱动程序VxD时却不能这样做,因为VxD的工作是向应用程序代码提供一个与硬件接口的环境,为每一个客户虚拟机管理虚设备的状态,透明地仲裁多个应用程序,同时对底层硬件进行访问。这就是所谓虚拟化的概念。

  VxD在虚拟机管理器VMM(Virtual Machine Manager)的监控下运行,而VMM实际上是一个特殊的VxD。VMM执行与系统资源有关的工作,提供虚拟机环境(能产生、调度、卸载VM)、负责调度多线程占先时间片及管理虚拟内存等工作。VxD与VMM运行在其他任何虚拟机之外,VxD事实上就是实现虚拟机的软件的一部分。

  与大? 操作系统一样,Windows也是采用层次式体系结构。VMM和VxDs构成了Win 95的ring0级的系统核心(应用程序运行在ring3级,ring1、ring2级未被使用),具有系统的最高优先权。Windows还提供一些以"drv"为后缀名的驱动程序,主要是指串行口的通信程序和并行口的打印机程序。这些程序与VxD不同,它们是运行在ring3级上的。图1可以使你更好地理解Windows的虚拟环境。 
 2.深入理解VMM和VxD 

  如前所述,VxD是Virtual Device Driver的缩写,但有人将它理解为虚拟任何驱动程序。实际上,VxD并非仅指那些虚拟化的某一具体硬件的设备驱动程序。比如某些VxD能够虚拟化设备,而某些VxD作为设备驱动程序却并不虚拟化设备,还有些VxD与设备并没有什么关系,它仅向其他的VxD或是应用程序提供服务。

  VxD可以随VMM一起静态加载,也可以根据需要动态加载或卸载。正是由于VxD与VMM之间的紧密协作,才使得VxD具有了应用程序所不具备的能力,诸如可以不受限制地访问硬件设备、任意查看操作系统数据结构(如描述符表、页表等)、访问任何内存区域、捕获软件中断、捕获I/O端口操作和内存访问等,甚至还可以截取硬件中断。

  尽管VxD使用32位平面存储模式(flat memory model),但它的代码和数据仍使用分段管理,段有六种类型,即实模式初始化、保护模式初始化、可分页、不可分页、静态和只调试(debug only),每种类型又有代码段和数据段之分,所以VxD共有12个段。实模式代码段和数据段为16位(分段模式),其他段则是32位(平面模式)。“实模式初始化”段包含了在Windows初始化过程的最初阶段VMM变为保护模式之前要执行的代码。静态加载的VxD此时可以查看Windows启动前的实模式环境,决定是否继续加载,并通知VMM。加载完毕后,VMM进入保护模式并执行保护模式初始化代码,同样将执行结果再通知VMM。初始化完成后,“实模式初始化”段和“保护模式初始化”段即被遗弃。VxD的大部分代码都在其他的某一段中,“可分页”段允许虚拟存储管理器(Virtual Memory Manager)进行分页管理,大多数的VxD代码都应当在“可分页”段。“不可分页”段的内容主要包括:VxD的主入口点、硬件中断处理函数、所访问的数据以及能被另一个VxD中断处理函数调用的异步服务。“静态”段仅用于可以动态加载的VxD,当VxD卸载后,静态代码段和数据段都保留在内存中。“只调试”段只是VMM在Soft-ICE for Win 95等调试环境下才将其载入。

  VMM是通过VxD的设备描述符块DDB(Device Descriptor Block)来识别的。DDB向VMM提供了VxD的主入口点,还向应用程序和其他的VxD提供了入口点。VMM利用这个主入口点将VM及Windows自身的状态通知给VxD,然后VxD通过相应的工作来响应这些事件。由于VxD不仅仅服务于一个物理设备(比如多个串口)或仅与一个VM发生联系,所以VxD需要产生自己支持的数据结构(Supporting Data Structures)来保存每一个设备、每一个VM的配置和状态信息。VxD用一个或多个设备上下文结构来保存设备信息,如I/O端口基地址、中断向量等,VxD将自己的每个VM的状态信息保存在VMM的VM控制块中

VMM提供的服务包括:事件服务、内存管理服务、兼容执行和保护模式执行的服务、登录表服务、调度程序服务、同步服务、调试服务、I/O捕获服务、处理错误和中断服务、VM中断和回调服务、配置管理程序服务以及其他杂项服务。

  以上内容仅涉及到VxD设计的一小部分,作为VxD的开发人员必须掌握更多的知识。首先是操作系统的知识,如地址空间、执行上下文、资源加锁、进程间通信和异步事件处理等方面的知识;其次,对Intel处理器应有较深入的理解,包括寄存器、机器指令集、保护机制、分页机制,以及虚拟8086模式;最后,还必须熟悉VMM提供的各类服务和接口,熟悉Windows其他的系统VxD。

  3.开发工具VToolsD简介 

  VToolsD是专门用于开发VxD程序的一种工具软件,它包括VxD框架代码生成器QuickVxD、C运行库、VMM/VxD服务库、VxD的C++类库、VxDLoad和VxDView等实用工具以及大量的C、C++例程。由VC++、BC++的32位编译器编译生成的VxD程序可以脱离VToolsD环境运行。

  利用QuickVxD可以方便、快捷地生成VxD的框架,即生成后缀名为h、cpp和mak的三个文件。源文件包含了运行VxD的基本组件,其中包含控制消息处理、API入口点、以及VxD服务等函数框架,并且还定义了标志,设置了编译参数,声明了类,然后在C++环境下,向生成的各个处理函数体内添加自己的代码,最后使用编译器NMAKE生成标准的VxD程序。

  由于VxD运行在ring0级,所以调试程序相当困难。我使用的调试工具是Soft-ICE for Win 95。

  目前VToolsD的最新版本为3.0,它支持设备访问体系结构DAA(Device Access Architecture),所编写的程序代码将可以在所有Windows平台(包括Win 95、Win 98以及Windows NT)上共享。当然也可以使用Microsoft公司的DDK(Device Developer Kit)来开发VxD,但DDK不能像VToolsD那样通过屏蔽系统及VxD的底层技术细节提供丰富的C运行库和C++类库,而是让开发人员充分享用面向对象编程方法的方便与快捷,因此仅就该点而言,使用DDK是不方便的。

4.VxD程序设计实例 

  我在开发可视电话音频卡的设计过程中,用VToolsD 2.03、VC++ 5.0为自制的PC/XT总线扩展卡开发了虚拟设备驱动程序Audcard.vxd。该卡每20ms申请一次中断,中断由应用程序动态载入系统的Audcard.vxd响应并加以处理。中断服务程序ISR(Interrupt Service Routine)结束后,调用函数Shell_PostMessage( )向应用程序窗口发送自定义消息。应用程序接受消息后,再通过函数DeviceIoControl( )与VxD的接口函数OnW32DeviceIoControl( )互传缓冲区数据。程序结束即可动态卸载VxD。下图表示在Win 95下VxD对硬件中断的处理过程。

  Win95下硬件中断的处理过程

  当中断发生时,处理器转换为ring0级保护模式。Windows系统并不像DOS那样通过中断描述符表IDT(Interrupt Descriptor Table)直接指向中断处理过程,而是由IDT入口指向VMM中的程序。该程序将判断是否为中断调用,如果是,则把中断控制权交给虚拟可编程中断控制器VPICD(Virtual Programmable Interrupt Controller Device),VPICD实际上是一个重要的VxD。VPICD再将其交给另一个注册了该中断的VxD(如Audcard.vxd)来处理。VxD程序是通过调用VPICD服务VPICD_Virtualize_IRQ来注册中断的。

  虚拟设备驱动程序Audcard.vxd的部分源代码Audcard.h和Audcard.cpp在网上,网址为:www.pccomputing.com.cn。此应用程序使用了下列函数:CreateFile()动态加载VxD、CloseHandle()并动态卸载VxD、PreTranslateMessage()截获消息、DeviceIoControl()与VxD互传缓冲区数据。虚拟设备驱动程序Audcard.vxd经调试后工作正常,未发生过任何丢失数据或死机的现象。

  下面是虚拟设备驱动程序Audcard.vxd的部分源代码Audcard.h和Audcard.cpp,限于篇幅,由QuickVxD自动生成的Audcard.mak未列出。

  ①Audcard.h

  //AUDCARD.h - include file for VxD AUDCARD 
  #include 
  #define DEVICE_CLASS AudcardDevice 
  #define AUDCARD_DeviceID UNDEFINED_DEVICE_ID 
  #define AUDCARD_Init_Order UNDEFINED_INIT_ORDER#define AUDCARD_Major 
  #define AUDCARD_Minor 0 
  #define MY_IRQ 5 //定义5号中断 
  class MyHwInt:public VHardwareInt 
  { 
  public: 
  MyHwInt():VHardwareInt(MY_IRQ,0,0,0){} 
  virtual VOID OnHardwareInt(VMHANDLE); 
  }; 
  class AudcardDevice : public VDevice 
  { 
  public: 
  virtual BOOL OnSysDynamicDeviceInit(); 
  virtual BOOL OnSysDynamicDeviceExit(); 
  virtual DWORD OnW32DeviceIoControl(PIOCTLPARAMS pDIOCParams); 
  MyHwInt* pMyIRQ; 
  };

class AudcardVM : public VVirtualMachine 
  { 
  public: 
  AudcardVM(VMHANDLE hVM); 
  }; 
  class AudcardThread : public VThread 
  { 
  public: 
  AudcardThread(THREADHANDLE hThread); 
  };

  ②Audcard.cpp

  //AUDCARD.cpp - main module for VxD AUDCARD 
  #define DEVICE_MAIN 
  #include "audcard.h" 
  Declare_Virtual_Device(AUDCARD) 
  #define WM_USER_POSTVXD 0x1000  //自定义消息 
  #undef DEVICE_MAIN 
  AudcardVM::AudcardVM(VMHANDLE hVM) : VVirtualMachine(hVM) {} 
  AudcardThread::AudcardThread(THREADHANDLE hThread) : VThread(hThread) {} 
  BOOL AudcardDevice::OnSysDynamicDeviceInit() //动态加载时初始化 
  { 
  ......//硬件初始化

  pMyIRQ=new MyHwInt(); 
  if(pMyIRQ&&pMyIRQ-$#@62;hook()) //挂接中断 
  { 
  pMyIRQ-$#@62;physicalUnmask(); //允许中断 
  return TRUE; 
  } 
  else return FALSE; 
  } 
  BOOL AudcardDevice::OnSysDynamicDeviceExit()

  //动态卸载过程

  { 
  delete pMyIRQ; 
  return TRUE; 
  } 
  DWORD AudcardDevice::OnW32DeviceIoControl(PIOCTLPARAMS pDIOCParams)

  //与Win32应用程序的接口函数

  { 
  ...... 
  }

  VOID MyHwInt::OnHardwareInt(VMHANDLE hVM) 
  {

  ...... // 中断处理

  SHELL_PostMessage(AppWnd,WM_USER_POSTVXD ,0,0,0,NULL);

  //向应用程序窗口发送消息

  sendPhysicalEOI(); //通知VPICD中断结束 
  }

VxD技术及其在实时反病毒中的应用 
indows9x平台反病毒产品大多属静态反病毒软件,指导思想是"以杀为主",这一方式的缺点是病毒在被清除之前可能早已造成了严重危害一个好的反病毒软件应该是"以防为主,以杀为辅",在病毒入侵时就把它清除掉,这就是实时反病毒技术。

  Windows9x使用IntelCPU的Ring0和Ring3两个保护级。系统进程运行于Ring0,因而具有对系统全部资源的访问权和管理权;而普通用户进程运行于Ring3,只能访问自己的程序空间,不允许对系统资源进行直接访问许多操作受到限制。显然这种普通用户进程是无法胜任实时反病毒工作的,必须使后台监视进程运行在Ring0优先级,实现这一目的基础就是VxD技术。

  一、VxD技术的特点

  VxD即虚拟设备驱动程序,用作Windows9x系统和物理设备之间的接口。但它不仅适用于硬件设备,也适用于按VxD规范所编制的各种软件"设备"

  VxD技术的实质是:通过加载具有Ring0最高优先级的VxD,运行于Ring3上的应用程序能够以一定的接口控制VxD的动作,从而达到控制系统的目的。实时反病毒软件之所以要使用VxD技术,关键有二:(1)VxD拥有系统最高运
行权限(2)许多Windows9x系统底层功能只能在VxD中调用,应用程序如果要用必须编个VxD作为中介。VxD作为应用程序在系统中的一个代理,应用程序通过它来完成任何自己本身做不到的事情,通过这一手段,Windows9x系统为普通应用程序留下了扩充接口。很不幸,这一技术同样为病毒所利用,CIH病毒正是利用了VxD技术才得以驻留内存、传染执行文件、毁坏硬盘和FlashBIOS。

  Windows9x系统下有众多的VxD,每个VxD可提供4种服务,即PM(保护模式)API、V86(虚拟86)API、Win32服务和VxD服务,前3种分别供应用程序在16位保护模式、V86模式以及32位保护模式下调用,VxD服务则只供其他VxD使用用户开发的VxD可提供任意上述服务。除此之外,应用程序还可通过调用API函数DeviceIoControl与支持IOCTL接口的VxD进行通信,执行Win32API不支持的系统低级操作。

 二、VxD技术的实现

  VxD的操作基于寄存器,所以一般用汇编语言编写,它的关键部分是一个和普通窗口的消息处理过程WndProc相类似的控制过程,不同之处在于它的处理对象是系统发来的控制消息。这些消息共51种,在VxD自加载至卸出整个生命周期内,操作系统不断向它发送各种控制消息,VxD根据自己的需要选择处理,其余的忽略。系统向VxD发送控制消息时将? 代号放在EAX寄存器中并在EBX寄存器中放系统虚拟机(VM)句柄。

  对动态VxD来说,最重要的消息有三个:SYS_DYNAMIC_DEVICE_INIT、SYS_DYNAMIC_DEVICE_EXIT以及W32_DEVICEIOCONTROL,消息代号分别是1Bh、1Ch、23h。当VxD被动态加载至内存时。
  系统向其发送SYS_DYNAMIC_DEVICE_INIT消息,VxD应在此时完成初始化设置并建立必要的数据结构;当VxD将被卸出内存时,系统向其发送SYS_DYNAMIC_DEVICE_EXIT消息VxD在收到后应清除所作设置并释放相关数据结构;当应用程序调用API函数DeviceIoControl与VxD进行通信时,系统向VxD发送W32_DEVICEIOCONTROL消息,它是应用程序和VxD联系的重要手段,此时ESI寄存器指向一个DIOCParams结构,VxD从输入缓冲区获取应用程序传来数据,相应处理后将结果放在输出缓冲区回送应用程序,达到相互传递数据的目的。

  应用程序向VxD发出DeviceIoControl调用时,第2个参数用于指定进行何种控制,控制过程从DIOCParams结构+0Ch处取得此控制码再进行相应处理控制码的代号和含义由应用程序和VxD自行约定,系统预定义了DIOC_GETVERSION (0)和DIOC_CLOSEHANDLE(-1)两个控制码,当应用程序调用API函数CreateFile("\\.\VxDName",...)动态加载一VxD时,系统首先向该VxD的控制过程发送SYS_DYNAMIC_DEVICE_INIT控制消息,若VxD返回成功,系统将再次向VxD发送带有控制码DIOC_OPEN(即DIOC_GETVERSION,值为0)的W32_DEVICEIOCONTROL消息以决定此VxD是否能够支持设备IOCTL接口,VxD必须清零EAX寄存器以表明支持IOCTL接口,这时CreateFile将返回一个设备句柄hDevice,通过它应用程序才能使用DeviceIoControl函数对VxD进行控制。
  同一个VxD可用CreateFile打开多次,每次打开时都会返回此VxD的一个唯一句柄,但是系统内存中只保留一份VxD,系统为每个VxD维护一个引用计数,每打开一次计数值加1。当应用程序调用API函数CloseHandle(hDevice)关闭VxD句柄时,VxD将收到系统发来的带控制码DIOC_CLOSEHANDLEW32_DEVICEIOCONTROL消息,同时该VxD的引用计数减1,当最终引用计数为0时,系统向VxD发送控制消息SYS_DYNAMIC_DEVICE_EXIT,然后将其从内存中清除。在极少数情况下应用程序也可调用API函数DeleteFile("\\.\VxDName")忽略引用计数的值直接将VxD卸出内存,这将给使用同一VxD的其他应用程序造成毁灭性影响,应避免使用。

 --一个典型的VxD控制过程代码如下:

 BeginProcVXD_Control
 cmpeax,1Bh
 ;SYS_DYNAMIC_DEVICE_INIT消息
 jzvxd_dynamic_init_handle
 cmpeax,1Ch
;SYS_DYNAMIC_DEVICE_EXIT消息
jzvxd_dynamic_exit_handle
cmpeax,23h
;W32_DEVICEIOCONTROL消息
jnzexit_control_proc
movecx,[esi+0Ch]
;从DIOCParams+0Ch处取控制码
....
;处理控制码
EndProcVXD_Control

 三、实时反病毒的关键技术-FileHooking

  应用程序通过使用动态加载的VxD,间接获得了对Windows9x系统的控制权,但要实现对系统中所有文件I/O操作的实时监视,还要用到另一种关键技术-FileHooking,通过挂接一个处理函数,截获所有与文件I/O操作有关的系统调用。Windows9x使用32位保护模式可安装文件系统(IFS),由可安装文件系统管理器(IFSManager)协调对文件系统和设备的访问,它接收以Win32API函数调用形式向系统发出的文件I/O请求,再将请求转给文件系统驱动程序FSD,由它调用低级别的IOS系统实现最终访问。每个文件I/OAPI调用都有一个特定的FSD函数与之对应,IFSManager负责完成由API到FSD的参数装配工作,在完成文件I/OAPI函数参数的装配之后转相应FSD执行之前,它会调用一个称为FileSystemApiHookFunction的Hooker函数。通过安装自己的Hooker函数,就可以截获系统内所有对文件I/O的API调用,并适时对相关文件进行病毒检查,从而实现实时监控。

  上述过程由用户VxD调用系统VxDIFSMgr提供的服务完成,该VxD提供了丰富的底层文件操作功能:IFSMgr_InstallSyatemApiHook函数用来安装FileSystemApiHookFunction,IFSMgr_RemoveSystemApiHook用来卸除Hooker,IFSMgr_Ring0_FileIO用来对文件和磁盘扇区进行读写访问等等。当由IFSManager转入SystemApiHookFunction时,带有6个参数:

FileSystemApiHookFunction(
pIFSFuncFSDFnAddr,
//对应FSD服务函数地址
intFunctionNum,
//与API对应的FSD服务功能号(详见下面)
intDrive,
//驱动器代号(1=A,2=B,3=C...)
intResourceFlags,
//资源标志(详见下面)
intCodePage,
//代码页(0=ANSI,1=OEM)
pioreqpir
//指向IOREQ结构的指针
)

  参数中比较重要的是FSD功能号、驱动器号和IOREQ结构指针3项。如需截获某个文件I/OAPI调用,只需在Hooker中对相应FSD功能号进行处理

  系统中可挂接多个Hooker,形成一条链。IFSMgr_InstallFileSystemApiHook安装Hooker成功时返回前一个Hooker地址,每个Hooker在做特定处理后总应调用前一个Hooker,最后安装的Hooker最先被调用。在VxD中调用其他VxD服务采用INT20h指令后跟一个双字的特殊格式,其中高字为被调用VxD的ID号(系统VxD的ID固定),低字为该VxD之服务号,这一形式称为VxDcall,如:

int20h
dd00400043h
;VxDCallIFSMgr_InstallSystemApiHook
int20h
dd00400044h
;VxDCallIFSMgr_RemoveSystemApiHook r> 
int20h
dd00400043h
;VxDCallIFSMgr_InstallSystemApiHook
int20h
dd00400044h
;VxDCallIFSMgr_RemoveSystemApiHook emApiHook ok

虚拟设备驱动程序两关键问题(上)

设计中,两个尤为关键,且又令人困扰的问题是VxD的虚拟化和VxD与应用程序间的通信机制。下面,对这两个问题作一详细的探讨。

  一、VxD的虚拟化 

  由于Windows允许同时运行多个任务,所以出现多个进程试图同时访问同一物理设备的情况时,如果多个应用程序通过同一个DLL驱动程序(注意和虚拟设备驱动程序VxD的区别)访问设备,不需要对该设备虚拟化,驱动程序使之顺序访问;如果是多个Windows应用程序对相同设备同时访问,由于都运行于System VM(系统虚拟机),所以也不需要虚拟化,它们的访问将由一个驱动程序(Windows driver DLL)进行检测并使之串行化,而不是依靠VxD;如果多个VM试图访问同一设备,由于DOS应用程序能够直接操纵硬件,所以必须对该设备进行虚拟化,一个虚拟化设备的VxD负责可靠地检测多个VM试图访问同一设备的情况,并采取仲裁的策略来解决这种冲突。这里可能有以下几种解决方案:

  1、允许一个VM访问物理设备,同时忽略其它的VM。这是最简单的虚拟化形式。如VPD(Virtual Printer Device)。

  2、允许一个VM访问物理设备,同时为其它的VM虚拟化设备。如VKD(Virtual Keyboard Device)分配给一个VM,并使之获得物理键盘的访问权(包括键盘中断在内),对其它的VM而言,VKD只向它们提供一个空的键盘缓冲区。

  3、允许多个VM共享同一物理设备。尽管存在假象,但从VM的观点来看,这种方法与独享访问一样。如VDD(Virtual Display Device),使每一个Windows环境下的DOS VM认为是直接写入显存,其实只是被VDD映射到了一个窗口的缓冲区。

  4、VxD独立访问物理设备的同时,允许一个VM访问虚拟设备,这是最复杂的虚拟化形式。如VCD(Virtual Com Device),VCD缓冲区接收串行数据并通过映射中断透明地传给相应的一个VM,VM在中断处理过程中读取串口数据寄存器,这些数据的实质是VCD缓冲区已经接收的数据。

  与物理设备一样,硬件中断很多时候也必须虚拟化,这种情况更为复杂。虚拟化中断实质上就是将硬件产生的中断映射到需要它的每一个VM(不管该VM是否正在运行),替代VxD进行服务。在这里我们给出一个虚拟化中断的VxD实例的几个重要回调过程,并采用最简单的仲裁策略来解决访问冲突(见程序1)。

  typedef struct 
  { 
 IRQHANDLE IrqHandle; 
  VMHANDLE VMOwner; 
  Char DeviceName[8]; 
  BOOL bVMIsServicing; 
  } DEVICE_CONTEXT;

void _stdcall MaskChangHandler ( VMHANDLE hVM , IRQHANDLE hIRQ , BOOL bMasking )

  //当一个VM在中断控制器中屏蔽或打开中断hIRQ时,VPICD调用该过程

  { 
  if ( !bMasking ) //若为打开中断 
  { 
  if ( !device.VMOwner ) 
  { 
  device.VMOwner = hVM; //若无任何VM占有该中断,则将该中断的拥有权设为当前VM 
  } 
  else 
  { 
  if ( device.VMOwner != hVM ) 
  { 
  device.VMOwner = SHELL_Resolve_Contention ( device.VMOwner , hVM , device.DeviceName );

  //若已有VM占有该中断,则用户可通过对话框在两者间作出选择

  } 
  } 
  VPICD_Physically_Unmask ( hIRQ ); //打开该物理中断 
  } 
  else 
  { 
  device.VMOwner = 0; 
  VPICD_Physically_Mask ( hIRQ ); //屏蔽该物理中断 
  } 
  }

  BOOL _stdcall HwIntHandler ( VMHANDLE hVM , IRQHANDLE hIRQ )

  //当中断hIRQ发生,VPICD立即调用该过程

  { 
  if ( device.VMOwner && !device.bVMIsServicing ) //若有VM占有该中断并且不在上一次的中断处理中 
  { 
  VPICD_Set_Int_Request ( device.VMOwner , hIRQ ); //请见本例程后的讨论 
  } 
  else 
  { 
  ...... 
  } 
  return TRUE; 
  }

void _stdcall VirtIntHandler ( VMHANDLE hVM , IRQHANDLE hIRQ )

  //当VPICD每次向VM模拟中断时,调用该过程

  {

  device.bVMIsServicing = TRUE; //设置中断处理标志

  }

  

  void _stdcall IRETHandler ( VMHANDLE hVM , IRQHANDLE hIRQ )

  //当从VM的中断处理返回,执行该回调

  { 
  device.bVMIsServicing = FALSE; //清除中断处理标志 
   } 
  (程序1)

  由于中断是异步产生的,所以当VxD调用VPICD(虚拟可编程中断控制器)服务VPICD_Set_Int_Request将该中断映射到VM时,该VM应处于执行状态。

  (1)在映射的第一步,VPICD通过调用VMM(虚拟机管理器)服务Call_Priority_VM_Event强制调度所希望的VM,使用最高的优先权(Time_Critical_Boost);

  (2)VPICD提供一个该服务的回调,所以当VM被调度运行时,VMM即可通知VPICD;

  (3)然后VPICD通过调用另一个VMM服务Simulate_Int来调整VM的运行环境。该服务将VM的CS、IP和标志寄存器压入VM的堆栈,从VM的中断向量表IVT取出新的CS、IP和标志寄存器,并且清除中断标志;

  (4)当VPICD从回调返回,并且VMM变回V86模式时,VM便立即执行已向VPICD注册的中断处理过程。

  编写虚拟化设备的VxD与编写非虚拟化设备的VxD有很大的不同,主要是它要用到一组完全不同的VMM和VxD服务。实际上,现在很多为新设备所编写的VxD根本就不再虚拟化,因为并没有DOS或Windows应用程序直接访问这些硬件。

二、VxD与应用程序间的通信机制 

  由于VxD并不仅仅处理硬件,所以在大多数情形下,VxD还向应用程序提供一个接口。通过该接口,应用程序就能够做与硬件有关的事情了。

  Windows 9x具有VxD与应用程序双向通信的机制。下面叙述的应用程序均指Win32应用程序。

  应用程序到VxD的通信机制是:VxD并不象Win16应用程序接口那样输出一个特殊的API过程(保护模式API过程或V86模式API过程)来支持应用程序,取而代之的是它的控制过程必须能够处理一个特殊的消息:W32_DEVICEIOCONTROL。VMM代替调用DeviceIoControl函数的应用程序向VxD发送此消息。消息参数可确定VxD消息响应函数、输入输出缓冲区指针及缓冲区大小,并绑定在DIOCPARAMETERS结构中。通过这一接口,不仅仅可以读写设备,而且还能在应用程序和VxD之间互传指针,从而达到特殊应用的目的。

  有时只需调用应用程序与VxD间的接口,便能及时获得所需信息和服务。但还有一些特殊情况,必须由VxD异步通知应用程序,这就需要用到VxD到应用程序的通信机制。

  VxD到应用程序的接口关系要比应用程序到VxD的接口关系复杂得多。其间有两种调用方法:一种是使用PostMessage函数。通过调用这一由外壳VxD(SHELL VxD)提供的新服务,便可通知应用程序;另一种是使用特殊的Win32技术。这种技术的独到之处在于Win32 API支持多线程。

  在Win32技术中,尽管采用的APC(Asynchronous Procedure Calls)异步过程调用机制和Win32事件机制都依赖于唤醒一个Win32应用程序线程,但仍略有不同。VxD到应用程序最简单的通信机制就是通过APC,这种方法对应用程序和VxD相对要简单一些。应用程序首先动态加载VxD(CreateFile),并用DeviceIoControl将回调函数的地址传给VxD,然后应用程序执行Win32调用SleepEx将其自身置为“挂起”(asleep yet alertable)状态时。当应用程序处于“挂起”状态,VxD能够通过VWIN32 VxD提供的QueueUserApc服务调用应用程序的回调函数。另一种更有效的方法是使用Win32事件机制。如果应用程序运行多个线程,当子线程等待着VxD来唤醒它的同时,主线程能够继续做自己的工作。例如,当一个子线程在等待VxD缓存接收的数据时,主线程可同时监控用户的输入。一旦缓冲区达到门限,VxD将唤醒等待的子线程。对于线程间的通知,VxD使用线程事件,就象应用程序的多线程机制所做的那样。在Windows 95下,VxD可访问与多线程应用程序非常相同的一些Win32事件API(由VWIN32 VxD提供)。这些VWIN32事件服务包括:_VWIN32_ResetWin32Event、_VWIN32_SetWin32Event、_VWIN32_PulseWin32Event、_VWIN32_WaitSingleObject、_VWIN32_WaitMultipleObjects。利用这些服务,VxD可唤醒一个等待的Win32应用程序线程,或是等待被一个Win32应用程序线程唤醒。不幸的是VxD不只是通过简单调用相应的事件服务,就能够获得Win32事件的句柄。因此,为获得Win32事件的句柄要涉及到一个复杂的过程和一个未公布的系统调用。事件通常是由应用程序产生(Win32 API CreateEvent),然后使用未公布的Win32 API函数OpenVxDHandle将获得的事件句柄转换为VxD事件句柄,再通过DeviceIoControl将这一ring 0级事件句柄传给VxD,于是VxD便可将其作为VWIN32事件函数的参数来使用。

用VToolsD开发一个简单的VxD

这一次,我们讲一下如何用VToolsD开发一个最简单的VxD,以及用SoftIce进行源程序级的调试。


  VToolsD的使用

  在VtoolsD中,有一个最重要的VxD开发工具:QuickVxD。QuickVxD可以为我们自动生成VxD源程序框架,而且QuickVxD提供了许多VxD的特性选项,例如可以选择要生成的VxD是动态加载的或是静态加载的,要使用的编程语言是C还是C++等等。

  我们要利用QuickVxD自动生成的是一个可动态加载的、基于C语言的VxD框架。之所以选用动态加载的VxD,是为了调试VxD的方便。每次修改代码,重新编译连接之后,要使VxD重新生效,如果采用静态加载的VxD,那就不得不重新启动电脑,而若采用了动态加载的VxD,那只须使用VToolsD带的另一个开发工具VxDLoad就可以卸出或重新加载内存中的VxD。之所以采用C语言而不是C++,是因为其简洁易懂。请按照如图1~图4进行选择。按下Generate Now按钮,我们就获得了动态加载的、基于C语言的VxD的源程序。

  如果您是按照上一篇文章中讲过的VToolsD的编译环境设置系统,那我们就可以编译刚才生成的这个最简单的VxD了。在DOS提示符下输入指令:

  nmake -f myfirst.mak

  看一下当前目录下是否生成了myfirst.vxd,如果有,那我们下面准备对这个VxD进行源程序级的调试。如果没有,那么很可能是您的编译环境没有正确配置,请找来上一篇文章好好读读。

  用VxDLoad加载myfirst.vxd(见图5)

  按下Load按钮,会出现VxD load successfully消息框。


  用SoftIce调试VxD

  对于SoftIce选单作如下选择:

  (1)File→Open Module选择我们刚才生成的myfirst.vxd。

  (2)Module→Translate,如果Symbol Loader提示无法加载一些asm文件,那就跳过所有的asm文件。

  (3)Module→Load。

  按下Ctrl+D,进入SoftIce运行环境中(如果您还没有按照上一篇文章中安装SoftIce的话,那就无法再进行下面的测试)。输入如下指令:

  :file ?

  myfirst.c

  :file myfirst.c

  这时,在SoftIce中,您将会看到myfirst.c的源程序。

系统篇

VC编程制作系统托盘程序
Windows操作系统中的某些程序运行时不显示运行窗口,只在任务栏上显示一个图标,表示程序正在运行,用户可以通过鼠标与应用程序交互,比如金山毒霸等应用程序,我们有时也需要编制一些仅在后台运行的类似程序,为了不干扰前台程序的运行界面和不显示不必要的窗口,应使程序运行时的主窗口不可见。同时将一个图标显示在任务栏右端静态通告区中并响应用户的鼠标动作。下面介绍Visual C++开发这类程序的设计方法。

  一、隐藏程序的主窗口

  首先,要使程序的主窗口不可见,并且不在任务栏上出现任务按钮,要做到这两点,需分别设置主边框窗口的风格和扩展风格:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 cs.style =WS_POPUP;//使主窗口不可见
 cs.dwExStyle |=WS_EX_TOOLWINDOW;//不显示任务按钮
 return CFrameWnd::PreCreateWindow(cs);
}

   二、将表示程序运行的图标加入任务栏

  在主框架窗口的CMainFrame::OnCreate()函数中调用上述函数,就可以在任务条上显示图标这一步是利用系统API函数Shell_NotifyIcon()将一个图标显示在任务栏的通告区中。该函数的原型为:在调用该函数之前,需要确定其参数的取值。其中Shell_NotifyIcon()函数的第一个参数是一个预定义的消息,可以取如下值之一:NIM_ADD、NIM_DELETE或NIM_MODIFY,分别表示添加图标、删除图标或修改图标。另一个参数为指向NOTIFYICONDATA类型的指针。其原型为:

typedef struct _NOTIFYICONDATA {
 DWORD cbSize;
 HWND hWnd;
 UINT uID;
 UINT uFlags;
 UINT uCallbackMessage;
 HICON hIcon; 
 charszTip[64]; }
NOTIFYICONDATA

  在该结构的成员中,cbSize为该结构所占的字节数,hWnd为接受该图标所发出的消息的窗口的句柄(鼠标在任务栏上程序图标上动作时图标将发出消息,这个消息用户要自己定义),uID为被显示图标的ID,uFlags指明其余的几个成员(hIcon、uCallBackMessage和szTip)的值是否有效,uCallbackMessage为一个用户自定义的消息,当用户在该图标上作用一些鼠标动作时,图标将向应用程序的主框架窗口(hWnd成员中指定的窗口)发出该消息,。hIcon为将在任务栏上被显示图标的句柄,szTip鼠标停留在该图标上时显示的字符串。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 NOTIFYICONDATA tnd;
 tnd.cbSize=sizeof(NOTIFYICONDATA);
 tnd.hWnd=this->m_hWnd;
 tnd.uID=IDR_MAINFRAME;
 tnd.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
 tnd.uCallbackMessage=WM_MYMESSAGE;
 file://用户自定义的消息,即鼠标在任务栏上程序图标上动作时图标发送的消息
 tnd.hIcon=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDR_MAINFRAME));
 strcpy(tnd.szTip,"测试程序");//图标提示为"测试程序"
 Shell_NotifyIcon(NIM_ADD,&tnd);//向任务栏添加图标
}

三、用户与程序交互的实现

  用户进行交互,也就是当用户在该图标上单击或双击鼠标左键或右键时要执行相应的操作,至少也要响应用户终止该程序的意愿。上面已经提到,当用户在图标上进行鼠标动作时,将向hWnd成员中指定的窗口发出自定义的消息,该消息为uCallbackMessage成员所指定的WM_MYESSAGE,取值为WM_USER+101(如何自定义消息,我就不多说了)。我们要实现任务就是在hWnd窗口中响应该自定义消息:

void CMainFrame::OnMYMESSAGE(WPARAM wParam,LPARAM lParam)
{
 UINT uID;//发出该消息的图标的ID
 UINT uMouseMsg;//鼠标动作
 POINT pt;
 uID=(UINT) wParam;
 uMouseMsg=(UINT) lParam;
 if(uMouseMsg==WM_RBUTTONDOWN)//如果是单击右键
 {
  switch(uID)
  {
  case IDR_MAINFRAME://如果是我们的图标
   GetCursorPos(&pt);//取得鼠标位置
   AfxGetApp( )-> m_pMainWnd->ShowWindow(SW_SHOWNORMAL);//显示程序窗口
   break;
   default:
  }
 }
 return; 
}

   四、程序结束时删除程序图标

  当程序结束时,需要删去通告区中的图标,这时还应该调用Shell_NotifyIcon函数,只不过第一个参数是表示删除图标的NIM_DELETE了:

void CMainFrame::~CmainFrame() 
{
 NOTIFYICONDATA tnid;
 tnid.cbSize=sizeof(NOTIFYICONDATA);
 tnid.hWnd=this->m_hWnd;
 tnid.uID=IDR_MAINFRAME;//保证删除的是我们的图标
 Shell_NotifyIcon(NIM_DELETE,&tnid);
}

  上述实现步骤可以实现托盘程序,笔者在开发的IC卡程序中就应用了以上技术,希望可以对朋友们有所帮助。

也谈制作系统托盘程序
看到贵网站刊登的文章《制作系统托盘程序》,颇有一番感触,这篇文章是《程序员大本营》上一篇文章的变体,但遗憾的是原来的BUG依然,希望作者能够再仔细想一想。其实,在任务条上添加托盘比较容易实现,调用VC中的函数就可以解决,只是注意协调。接下来,我将给大家提供一个类,可以很容易的达到我们的目的,希望它能给大家一点帮助,如果你发现BUG也欢迎你和我联系。运行程序,左键双击或者右键单击任务条上的托盘,我们可以看到效果。

  1. 有关类CsystemTray的说明:(文章的最后有本类的具体实现代码)

  CSystemTray是CObject的扩展类,实现以下功能:

   1、在任务条显示托盘图标。

   2、设置提示Tip

   3、设置图标的形状

  主要函数说明:

Create(CWnd* pWnd, UINT uCallbackMessage, LPCTSTR szTip, HICON icon, UINT uID);

  功能:

   生成一个图标。

  参数说明:

   pWnd:程序的主窗口,

   uCallbackMessage:对应的消息映射,

   szTip:鼠标停留时的提示文字,

   icon:显示的图标,

   uID:与之对应的菜单ID

  · BOOL CSystemTray::SetIcon(HICON hIcon)

    BOOL CSystemTray::SetIcon(LPCTSTR lpszIconName)

    BOOL CSystemTray::SetIcon(UINT nIDResource)

    BOOL CSystemTray::SetStandardIcon(LPCTSTR lpIconName)

    BOOL CSystemTray::SetStandardIcon(UINT nIDResource)

   功能:更改托盘上的图标。

  void CSystemTray::ShowIcon()

   功能:显示图标。

  · BOOL CSystemTray::SetTooltipText(LPCTSTR pszTip)

    BOOL CSystemTray::SetTooltipText(UINT nID)

   功能:提示显示文字。

 2. 使用本类的步骤如下:

  第一步:在VC变成环境下,建立一个工程,基于对话框或者是单文档(或者是多文档)你随便,所有的选项都取默认值即可。

  第二步:在mainfrm.h中定义变量CSystemTray m_TrayIcon;并添加函数声明:

   afx_msg LRESULT OnTrayNotification(WPARAM wParam, LPARAM lParam);

   afx_msg void OnSysCommand(UINT nID, LPARAM lParam);

  第三步:在StaAFX.h中定义消息:

   #define WM_ICON_NOTIFY WM_USER + 1

  第四步:自定义菜单IDR_POPUPMENU,其中最少包括一项:ID:ID_VIEW_MAIN_WINDOW,Caption为"显示主窗口",并为该项在类CmainFrame中添加消息映射函数,COMMAND和UPDATE_COMMAND_UI。

  第五步:在Mainframe.cpp中添加以下内容:

1. BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_VIEW_MAIN_WINDOW, OnViewMainWindow)
ON_UPDATE_COMMAND_UI(ID_VIEW_MAIN_WINDOW, OnUpdateViewMainWindow)
ON_MESSAGE(WM_ICON_NOTIFY, OnTrayNotification)
ON_WM_SYSCOMMAND()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
2. void CMainFrame::OnViewMainWindow() 
{
if(IsWindowVisible())
{
ShowWindow(SW_SHOWMINIMIZED);
ShowWindow(SW_HIDE);
m_TrayIcon.SetIcon(IDI_ICON1);
}
else
{
ShowWindow(SW_SHOW);
ShowWindow(SW_RESTORE);
m_TrayIcon.SetIcon(IDI_ICON2);
}
}

void CMainFrame::OnUpdateViewMainWindow(CCmdUI* pCmdUI) 
{
pCmdUI->SetCheck(IsWindowVisible());
}
3. int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;
//创建托盘图标
if (!m_TrayIcon.Create(this, WM_ICON_NOTIFY,"大屏实时显示程序", NULL, IDR_POPUPMENU))
return -1;
m_TrayIcon.SetIcon(IDI_ICON1);
SetMenu(NULL);
return 0;
}
4. LRESULT CMainFrame::OnTrayNotification(WPARAM wParam, LPARAM lParam)
{
if (wParam != IDR_POPUPMENU)
return 0L;

CMenu menu, *pSubMenu;
if (LOWORD(lParam) == WM_RBUTTONUP)

CPoint pos;
GetCursorPos(&pos);
if (!menu.LoadMenu(IDR_POPUPMENU)) return 0;

if (!(pSubMenu=menu.GetSubMenu(0))) return 0;

::SetMenuDefaultItem(pSubMenu->m_hMenu, 3, TRUE);
SetForegroundWindow(); 
pSubMenu->TrackPopupMenu(TPM_RIGHTALIGN | TPM_BOTTOMALIGN | TPM_RIGHTBUTTON, pos.x, pos.y,
this);
menu.DestroyMenu();
}
else 
if (LOWORD(lParam) == WM_LBUTTONDBLCLK) 
{
if (!menu.LoadMenu(IDR_POPUPMENU)) return 0;
if (!(pSubMenu = menu.GetSubMenu(0))) return 0;

SetForegroundWindow();
//激活第2个菜单项
SendMessage(WM_COMMAND, pSubMenu->GetMenuItemID(1), 0);
menu.DestroyMenu();

}
return 0;
}

void CMainFrame::OnSysCommand(UINT nID, LPARAM lParam) 
{
if(nID==SC_MINIMIZE)
{
ShowWindow(SW_SHOWMINIMIZED);
ShowWindow(SW_HIDE);
}
else
CFrameWnd::OnSysCommand(nID, lParam);
}
3. 附录:类的实现代码
CsystemTray的头文件
#ifndef _INCLUDED_SYSTEMTRAY_H_
#define _INCLUDED_SYSTEMTRAY_H_

/
// CSystemTray window

class CSystemTray : public CObject
{
// Construction/destruction
public:
CSystemTray();
CSystemTray(CWnd* pWnd, UINT uCallbackMessage, LPCTSTR szTip, HICON icon, UINT uID);
virtual ~CSystemTray();

// Operations
public:
CFrameWnd * m_pFrame;
BOOL Enabled() { return m_bEnabled; }
BOOL Visible() { return !m_bHidden; }

//Create the tray icon
Create(CWnd* pWnd, UINT uCallbackMessage, LPCTSTR szTip, HICON icon, UINT uID);

//Change or retrieve the Tooltip text
BOOL SetTooltipText(LPCTSTR pszTooltipText);
BOOL SetTooltipText(UINT nID);
CString GetTooltipText() const;

//Change or retrieve the icon displayed
BOOL SetIcon(HICON hIcon);
BOOL SetIcon(LPCTSTR lpIconName);
BOOL SetIcon(UINT nIDResource);
BOOL SetStandardIcon(LPCTSTR lpIconName);
BOOL SetStandardIcon(UINT nIDResource);
HICON GetIcon() const;
void HideIcon();
void ShowIcon();
void RemoveIcon();
void MoveToRight();

//Change or retrieve the window to send notification messages to
BOOL SetNotificationWnd(CWnd* pNotifyWnd);
CWnd* GetNotificationWnd() const;

//Default handler for tray notification message

// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CSystemTray)
//}}AFX_VIRTUAL

// Implementation
protected:
BOOL m_bEnabled; // does O/S support tray icon?
BOOL m_bHidden; // Has the icon been hidden?
NOTIFYICONDATA m_tnd;

DECLARE_DYNAMIC(CSystemTray)
};

#endif

///
CsystemTray的实现文件
#include "stdafx.h"
#include "SystemTray.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

IMPLEMENT_DYNAMIC(CSystemTray, CObject)

/
// CSystemTray construction/creation/destruction

CSystemTray::CSystemTray()
{
memset(&m_tnd, 0, sizeof(m_tnd));
m_bEnabled = FALSE;
m_bHidden = FALSE;
}

CSystemTray::CSystemTray(CWnd* pWnd, UINT uCallbackMessage, LPCTSTR szToolTip, HICON icon, UINT uID)
{
Create(pWnd, uCallbackMessage, szToolTip, icon, uID);
m_bHidden = FALSE;
}

BOOL CSystemTray::Create(CWnd* pWnd, UINT uCallbackMessage, LPCTSTR szToolTip, HICON icon, UINT uID)
{
// this is only for Windows 95 (or higher)
VERIFY(m_bEnabled = ( GetVersion() & 0xff ) >= 4);
if (!m_bEnabled) return FALSE;

//Make sure Notification window is valid
VERIFY(m_bEnabled = (pWnd && ::IsWindow(pWnd->GetSafeHwnd())));
if (!m_bEnabled) return FALSE;

//Make sure we avoid conflict with other messages
ASSERT(uCallbackMessage >= WM_USER);

//Tray only supports tooltip text up to 64 characters
ASSERT(_tcslen(szToolTip) <= 64);

// load up the NOTIFYICONDATA structure
m_tnd.cbSize = sizeof(NOTIFYICONDATA);
m_tnd.hWnd = pWnd->GetSafeHwnd();
m_tnd.uID = uID;
m_tnd.hIcon = icon;
m_tnd.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
m_tnd.uCallbackMessage = uCallbackMessage;
strcpy (m_tnd.szTip, szToolTip);

// Set the tray icon
m_pFrame = (CFrameWnd*)pWnd;
VERIFY(m_bEnabled = Shell_NotifyIcon(NIM_ADD, &m_tnd));
return m_bEnabled;
}

CSystemTray::~CSystemTray()
{
RemoveIcon();
}

/
// CSystemTray icon manipulation

void CSystemTray::MoveToRight()
{
HideIcon();
ShowIcon();
}

void CSystemTray::RemoveIcon()
{
if (!m_bEnabled) return;

m_tnd.uFlags = 0;
Shell_NotifyIcon(NIM_DELETE, &m_tnd);
m_bEnabled = FALSE;
}

void CSystemTray::HideIcon()
{
if (m_bEnabled && !m_bHidden) {
m_tnd.uFlags = NIF_ICON;
Shell_NotifyIcon (NIM_DELETE, &m_tnd);
m_bHidden = TRUE;
}
}

void CSystemTray::ShowIcon()
{
if (m_bEnabled && m_bHidden) {
m_tnd.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
Shell_NotifyIcon(NIM_ADD, &m_tnd);
m_bHidden = FALSE;
}
}

BOOL CSystemTray::SetIcon(HICON hIcon)
{
if (!m_bEnabled) return FALSE;

m_tnd.uFlags = NIF_ICON;
m_tnd.hIcon = hIcon;

return Shell_NotifyIcon(NIM_MODIFY, &m_tnd);
}

BOOL CSystemTray::SetIcon(LPCTSTR lpszIconName)
{
HICON hIcon = AfxGetApp()->LoadIcon(lpszIconName);

return SetIcon(hIcon);
}

BOOL CSystemTray::SetIcon(UINT nIDResource)
{
HICON hIcon = AfxGetApp()->LoadIcon(nIDResource);

return SetIcon(hIcon);
}

BOOL CSystemTray::SetStandardIcon(LPCTSTR lpIconName)
{
HICON hIcon = LoadIcon(NULL, lpIconName);

return SetIcon(hIcon);
}

BOOL CSystemTray::SetStandardIcon(UINT nIDResource)
{
HICON hIcon = ::LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(nIDResource));

return SetIcon(hIcon);
}

HICON CSystemTray::GetIcon() const
{
HICON hIcon = NULL;
if (m_bEnabled)
hIcon = m_tnd.hIcon;

return hIcon;
}

//
// CSystemTray tooltip text manipulation

BOOL CSystemTray::SetTooltipText(LPCTSTR pszTip)
{
if (!m_bEnabled) return FALSE;

m_tnd.uFlags = NIF_TIP;
_tcscpy(m_tnd.szTip, pszTip);

return Shell_NotifyIcon(NIM_MODIFY, &m_tnd);
}

BOOL CSystemTray::SetTooltipText(UINT nID)
{
CString strText;
VERIFY(strText.LoadString(nID));

return SetTooltipText(strText);
}

CString CSystemTray::GetTooltipText() const
{
CString strText;
if (m_bEnabled)
strText = m_tnd.szTip;

return strText;
}

// CSystemTray notification window stuff

BOOL CSystemTray::SetNotificationWnd(CWnd* pWnd)
{
if (!m_bEnabled) return FALSE;

//Make sure Notification window is valid
ASSERT(pWnd && ::IsWindow(pWnd->GetSafeHwnd()));

m_tnd.hWnd = pWnd->GetSafeHwnd();
m_tnd.uFlags = 0;

return Shell_NotifyIcon(NIM_MODIFY, &m_tnd);
}

CWnd* CSystemTray::GetNotificationWnd() const
{
return CWnd::FromHandle(m_tnd.hWnd);
}

枚举NT services

下面的文章提供了访问NT中所有Service的功能,每次列举Services时,函数会返回一个列表。 列表的内容依赖于你所使用的参数。 (我认为这是一种很巧妙的编程方法,它极大的减轻了数据和函数的冗余,利用一个STATIC函数来产生本身对象的列表或者是来产生对象)

  Class declaration:声明

  class TTrixServiceInfo {

  public:

   CString ServiceName;

   CString DisplayName;

   CString BinaryPath;

   DWORD ServiceType;

   DWORD StartType;

   DWORD ErrorControl;

   DWORD CurrentState;

  public:

   TTrixServiceInfo();

   TTrixServiceInfo& operator=(const TTrixServiceInfo& source);

   CString GetServiceType(void);

   CString GetStartType(void);

   CString GetErrorControl(void);

   CString GetCurrentState(void);

   static TTrixServiceInfo *EnumServices(DWORD serviceType,

   DWORD serviceState,DWORD *count);

  };

Description:类的每一个实例都包含了SERVICE的各种信息,如果想得到SERVICE的列表,请调用TTrixServiceInfo::EnumServices(...)。

参数ServiceType的取值可能是:SERVICE_WIN32 and SERVICE_DRIVER.

参数ServiceState的取值可能是:SERVICE_ACTIVE and SERVICE_INACTIVE.

EnumServices(...)将返回TTrixServiceInfo对象的列表,(如果出错返回NULL)。列表中对象的个数可以通过参数返回时得到。

下面是具体的代码:

  TTrixServiceInfo *lpservice = NULL;

  DWORD count;

  lpservice =           TTrixServiceInfo::EnumServices(SERVICE_WIN32,SERVICE_ACTIVE|SERVICE_INACTIVE,&count/*得到个数*/);

  if (lpservice) {//如果正确

    for (DWORD index = 0; index < count; index ++) {

     printf("%d. %s, %s\n", index, lpservice[index].DisplayName,

     lpservice[index].GetCurrentState());

     }

    delete [] lpservice;

   }

  Source code:

  TTrixServiceInfo::TTrixServiceInfo()

  {

   ServiceName.Empty();

   DisplayName.Empty();

   BinaryPath.Empty();

   ServiceType = 0;

   StartType = 0;

   ErrorControl = 0;

   CurrentState = 0;

  }

  TTrixServiceInfo& TTrixServiceInfo::operator=(const TTrixServiceInfo& source)

  {

   ServiceName = source.ServiceName;

   DisplayName = source.DisplayName;

   BinaryPath = source.BinaryPath;

   ServiceType = source.ServiceType;

   StartType = source.StartType;

   ErrorControl = source.ErrorControl;

   CurrentState = source.CurrentState;

   return *this;

  }

  CString TTrixServiceInfo::GetServiceType(void)

  {

   // Winnt.h

   CString str = "UNKNOWN";

   if (ServiceType & SERVICE_WIN32) {

    if (ServiceType &

     SERVICE_WIN32_OWN_PROCESS)

     str = "WIN32_OWN_PROCESS";

    else if (ServiceType &

     SERVICE_WIN32_SHARE_PROCESS)

     str = "WIN32_SHARE_PROCESS";

    if (ServiceType &

      SERVICE_INTERACTIVE_PROCESS)

      str += "(INTERACTIVE_PROCESS)";

    }

     switch (ServiceType) {

      case SERVICE_KERNEL_DRIVER:

        str = "KERNEL_DRIVER"; break;

      case SERVICE_FILE_SYSTEM_DRIVER:

        str = "FILE_SYSTEM_DRIVER";

        break;

      };

    return str;

   }

  CString TTrixServiceInfo::GetStartType(void)

  {

   // Winnt.h

   TCHAR *types[] = {

   "BOOT_START", // 0

   "SYSTEM_START", // 1

   "AUTO_START", // 2

   "DEMAND_START", // 3

   "DISABLED" // 4

  };

  return CString(types[StartType]);

  }

  CString TTrixServiceInfo::GetErrorControl(void)

  {

   // Winnt.h

   TCHAR *types[] = {

   "ERROR_IGNORE", // 0

   "ERROR_NORMAL", // 1

   "ERROR_SEVERE", // 2

   "ERROR_CRITICAL" // 3

   };

  return CString(types[ErrorControl]);

  }

  CString TTrixServiceInfo::GetCurrentState(void)

  {

   // Winsvc.h

   TCHAR *types[] = {

   "UNKNOWN",

   "STOPPED", // 1

   "START_PENDING", // 2

   "STOP_PENDING", // 3

   "RUNNING", // 4

   "CONTINUE_PENDING", // 5

   "PAUSE_PENDING", // 6

   "PAUSED" // 7

   };

   return CString(types[CurrentState]);

  }

  // ServiceType = bit OR of SERVICE_WIN32, SERVICE_DRIVER

  // ServiceState = bit OR of SERVICE_ACTIVE, SERVICE_INACTIVE

  TTrixServiceInfo *TTrixServiceInfo::EnumServices(DWORD serviceType,DWORD

  serviceState,DWORD *count)

  {

   // Maybe check if serviceType and serviceState have at least one constant specified

   *count = 0;

   TTrixServiceInfo *info = NULL;

   SC_HANDLE scman = ::OpenSCManager(NULL,NULL,SC_MANAGER_ENUMERATE_SERVICE);

   if (scman) {

    ENUM_SERVICE_STATUS service, *lpservice;

    BOOL rc;

    DWORD bytesNeeded,servicesReturned,resumeHandle = 0;

    rc = ::EnumServicesStatus(scman,serviceType,serviceState,&service,sizeof(service),

       &bytesNeeded,&servicesReturned,&resumeHandle);

    if ((rc == FALSE) && (::GetLastError() == ERROR_MORE_DATA)) {

     DWORD bytes = bytesNeeded + sizeof(ENUM_SERVICE_STATUS);

     lpservice = new ENUM_SERVICE_STATUS [bytes];

          ::EnumServicesStatus(scman,serviceType,serviceState,lpservice,bytes,

          &bytesNeeded,&servicesReturned,&resumeHandle);

     *count = servicesReturned; // Not a chance that 0 services is returned

     info = new TTrixServiceInfo [servicesReturned];

  TCHAR Buffer[1024];

  // Should be enough for service info

  QUERY_SERVICE_CONFIG *lpqch = (QUERY_SERVICE_CONFIG*)Buffer;

  for (DWORD ndx = 0; ndx < servicesReturned; ndx++) {

    info[ndx].ServiceName = lpservice[ndx].lpServiceName;

    info[ndx].DisplayName = lpservice[ndx].lpDisplayName;

    info[ndx].ServiceType = lpservice[ndx].ServiceStatus.dwServiceType;

    info[ndx].CurrentState = lpservice[ndx].ServiceStatus.dwCurrentState;

    SC_HANDLE sh = ::OpenService(scman,lpservice[ndx].lpServiceName,SERVICE_QUERY_CONFIG);

    if (::QueryServiceConfig(sh,lpqch,sizeof(Buffer),&bytesNeeded)) {

     info[ndx].BinaryPath = lpqch->lpBinaryPathName;

     info[ndx].StartType = lpqch->dwStartType;

     info[ndx].ErrorControl = lpqch->dwErrorControl;

    }

    ::CloseServiceHandle(sh);

   }

   delete [] lpservice;

   }

   ::CloseServiceHandle(scman);

   }

   return info;

  }

Windows 95的内存共享

下面给出一个共享内存的例子。在我们开发的中心监控显示软件中,工作站上的监视软件由几个独立进程组成,各自监视相应的子系统;由各监视进程共享的网络通讯模块负责网上监视信息的接收,并经由子线程写入对应监视进程的数据区。信息接收模块和监视进程共享内存数据区,由互斥控制对象保证内存读写访问的同步。

  信息接收模块的程序如下:

  CMutex mMutex1(TRUE, "Info1_Mutex"); //互斥对象

  CSingleLock mSLock1 ( & mMutex1 );

  DWORD dSize1; //信息区大小

  HANDLE hMem1; //文件映像对象

  InfoStruct1 pInfo1; //信息数据结构

  hMem1 = CreateFileMapping(

  (HANDLE(-1), 0, PAGE_READWRITE, 0,ShSize, "System1_Info");

  ( hMem1 == 0 ) //创建文件映像对象失败

  essageBox(hwnd, "Cannot Create FileMap for SubSystem1 ","Error", MB_OK);

  turn false;

  if (GetLastError() == ERROR_ALREADY_EXIST) //对象已经存在

  essageBox(hwnd, "FileMap Already Exists", "Error", MB_OK);

  turn false;

  pInfo1 = (InfoStruct1 *) MapViewOfFile( hMem1, FILE_MAP_WRITE, 0,0,0);

  if ( pInfo1 == 0)

  { MessageBox(hwnd, "Cannot Create View of File Mapping", "Error", MB_OK);

  seHandle( hMem1);

  urn false;

  }

  子线程的工作:

  mSLock1.Lock();

  write information to pInfo1…

  mSLock1.Unlock();

  监视进程的程序结构同接收模块类似,如下所示:

  ………

  (GetLastError()!= ERROR_ALREADY_EXIST) //对象不存在

  essageBox(hwnd, "FileMap doesnot Exists", "Error", MB_OK);

  turn false;

  ………

  mSLock1.Lock();

  read information from pInfo1…

  mSLock1.Unlock();

  七、结论

  随着硬件速度的发展和软件规模的扩大,多任务操作系统下进程之间的通讯量也在增加,进程之间通讯的手段多种多样,其中内存共享方法的效率高,使用方便,愿本文的介绍对你有所裨益,希望能为你的学习和工作节省一点宝贵的时间。

VISUAL C++ 系统及硬件编程相关推荐

  1. 《Python硬件编程实战》——2.8 在Mac中安装Python

    本节书摘来自华章计算机<Python硬件编程实战>一书中的第2章,第2.8节,作者:李茂 著, 更多章节内容可以访问云栖社区"华章计算机"公众号查看. 2.8 在Mac ...

  2. micropython编程软件下载_MicroPython可视化拼插编辑器:让硬件编程更智能!

    原标题:MicroPython可视化拼插编辑器:让硬件编程更智能! MicroPython是Python 3语言的精简高效实现 ,包括Python标准库的一小部分,经过优化可在微控制器和受限环境中运行 ...

  3. python硬件编程_Python学习日记_《Python硬件编程实战》笔记_Mr_Ouyang

    书名: Python硬件编程实战 作者: 李茂 出版社: 机械工业出版社 [此处需要插入图片 Python封面] 笔者简评:不太适宜购买,全书大篇幅在用图片来解释极简单的细节,对于那些需要作者去深挖. ...

  4. 【Computer Organization笔记07】实验课:可编程逻辑器件介绍,硬件编程方法与原则,硬件编程流程

    本次笔记内容: P13 计算机组成原理(13) P14 计算机组成原理(14) 本节课对应幻灯片: 组成原理52 verilogHDL 组成原理53 fpgahardware 本节课内容为" ...

  5. 基于ARM的嵌入式系统外围硬件设计

    基于ARM的嵌入式系统外围硬件设计 一.嵌入式接口分类 (1)基本嵌入式接口 (2)嵌入式网络接口 (3)标准协议接口 (4)无线通信接口 二.基本嵌入式接口 1.实时时钟 (1)功能:专用的电路单元 ...

  6. 嵌入式系统的硬件核心是什么?

    嵌入式逐渐成为热门行业,所以一些人也开始蠢蠢欲动了,开始要对嵌入式下手了,其实只要你真的对嵌入式感兴趣并且想要学习的话,那么什么时候都不算晚,毕竟兴趣是最好的老师,下面一起先来看看嵌入式系统的硬件核心 ...

  7. 【SoC FPGA学习】五、基于虚拟地址映射的 Linux 硬件编程,在已有工程上实现按键与LED的联动

    参考教程中的以下章节: 小知识点: hps_0.h 文件是qsys生成的FPGA侧外设 0xfc00 0000~0xffff ffff一共是0xffffffff - 0xfc000000 + 1 = ...

  8. 如何学习嵌入式系统(硬件篇),含51单片机学习资料

    学习嵌入式之前我们需要了解什么是嵌入式. (官方说法)嵌入式系统是一种专用的计算机系统,作为装置或设备的一部分.国内普遍认同的嵌入式系统定义为:以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用 ...

  9. ARM:嵌入式系统之硬件总复习

    嵌入式系统之硬件总复习 快要期末考试啦,这几天在复习嵌入式系统,想着有好几天没有更新csdn了,就更新一篇嵌入式系统的文章! 本篇文章覆盖以下知识点: 如何亮灯 时钟周期如何计算 printf函数如何 ...

最新文章

  1. JavaScript深度学习
  2. 你的CSDN粉丝为何那么少
  3. 栈相关经典题:每日温度
  4. vs2013新建web项目异常 0x80070002 处理
  5. springboot主要注解及其作用
  6. 链表数据结构原理图/内存结构图/内存图
  7. java面向对象super_【JavaSE】面向对象之super、final
  8. Java中竟有18种队列?45张图!安排
  9. ABP架构学习系列二:ABP中配置的注册和初始化
  10. 基于统计的压缩算法:游程编码
  11. Switchhosts软件安装包
  12. IP报文格式和实例分析
  13. 四、两个三层交换机互联
  14. C++MYSQL:获取表结构:MYSQL_FEILD
  15. 使用pg_repack实现在线vacuum
  16. day18_雷神_django第一天
  17. Java大牛给初学者的学习路线建议
  18. html中radio设置默认值
  19. 如何做一个基于微信酒店预订小程序系统毕业设计毕设作品
  20. xxx不在 sudoers 文件中,此事将被报告

热门文章

  1. python 文件格式转换器_python数据转换工具Into
  2. 百度PM·Star高校互联网产品设计大赛 我的作品——百度日历
  3. 给大家推荐几款小型的Linux发行版
  4. 用mne绘制fNIRS脑地形图(topomap)
  5. java网上商城学年论文_java网上商城论文(DOC毕业设计论文)
  6. js显示格式化代码并高亮(vue中实现代码高亮)
  7. 微信、支付宝二码合一扫码支付实现思路
  8. python周报第十五周
  9. 自己作为CA签署SSL证书
  10. 软考-进度网络图计算知识点(二)