移动处理器使用情况
了解如何计算 Windows Mobile 设备上每个正在运行的进程的处理器使用情况

目录
引言
本文是我关于“移动处理器使用率”技巧的后续文章,该技巧介绍了如何获取整体处理器使用率。在这里,我们将研究如何获取每个进程(甚至每个线程)的使用率统计信息。
基本算法相当简单
- 查询每个正在运行的线程在内核模式和用户模式下花费的时间。称此查询为 A。
- 等待预定的时间。
- 重复查询,称此查询为 B。
- 将查询 B 的时间减去查询 A 的时间。这将告诉我们在等待期间该线程花费的处理器使用时间。
我们只需将此算法应用于系统中每个进程中的每个线程!
我们将从揭示线程运行时间的秘密武器开始,即 GetThreadTimes。
顺便提一下,随附的演示应用程序使用了 boost 和 WTL。我曾考虑仅使用 Windows Mobile 6 SDK 自带的功能来编写它们,但 boost 库在使代码更具可读性、异常安全性和可用性方面做得非常出色,因此我决定使用它们。如果您不使用 boost(您真的应该使用!),本文提出的概念仍然适用。请注意,本文本身仅使用 C++03 和 Windows Mobile SDK。
GetThreadTimes
GetThreadTimes 提供了有关系统中每个正在运行线程的一系列通用信息。特别是,我们有兴趣了解线程在内核模式下执行以及在用户模式下执行所花费的时间。GetThreadTimes
以 FILETIME
结构的形式提供这些值。为了使用它们,我们将不得不将它们转换为毫秒。
/// Convert a FILETIME to ticks (ms)
DWORD GetThreadTick( const FILETIME& time )
{
__int64 tick = MAKEDWORDLONG( time.dwLowDateTime, time.dwHighDateTime );
return static_cast< DWORD >( tick /= 10000 );
}
FILETIME creation = { 0 },
exit = { 0 },
kernel = { 0 },
user = { 0 };
::GetThreadTimes( ( HANDLE )thread_id,
&creation,
&exit,
&kernel,
&user )
// time in ms spent in kernel space
DWORD kernel_tics = GetThreadTick( kernel );
// time in ms spent in user space
DWORD user_tics = GetThreadTick( user );
既然我们能够计算每个线程使用处理器的耗时,我们就必须找到一种方法来列出进程中的每个正在运行的线程。为此,我们将使用 ToolHelp API。
ToolHelp API
ToolHelp API 是 Windows Mobile 核心 OS 中的一组诊断工具,它允许我们在单个时间点捕获正在运行的进程使用的堆、模块和线程的快照。在此示例中,我们能够迭代系统中每个正在运行的线程。THREADENTRY32
结构告诉我们每个线程的 ID 及其父进程的 ID。
HANDLE snapshot = ::CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
if( INVALID_HANDLE_VALUE != snapshot )
{
THREADENTRY32 te = { 0 };
te.dwSize = sizeof( THREADENTRY32 );
if( ::Thread32First( snapshot, &te ) )
{
do
{
// The te.th32ThreadID member will give us the thread ID of
// every thread running in the system.
// te.th32OwnerProcessID tells us which process owns that
// thread.
} while( ::Thread32Next( snapshot, &te ) );
}
::CloseToolhelp32Snapshot( snapshot );
}
不幸的是,如果您要将此代码实际运行在您的系统上,您会很快发现只显示了来自 2 个进程的线程:您的进程和NK.exe。为了绕过此权限限制,我们将查看 SetProcPermissions API。
权限
SetProcPermissions 是平台构建器pkfuncs.h API 的一部分,通常仅由需要访问整个虚拟地址空间的驱动程序使用。为了确保我们谨慎地使用这些非凡的宇宙力量,我们将定义一个结构,该结构保证在我们的函数完成时(即使抛出异常)恢复我们原始的权限。
/// Temporarily grant phenomenal cosmic powers. This may not be necessary for
/// versions of windows mobile earlier than 6.
struct CosmicPowers
{
CosmicPowers()
{
old_permissions_ = ::SetProcPermissions( 0xFFFFFFFF );
}
~CosmicPowers()
{
::SetProcPermissions( old_permissions_ );
}
private:
DWORD old_permissions_;
}; // struct CosmicPowers
现在,我们所要做的就是在我们的线程迭代函数中创建一个 CosmicPowers
实例,这样我们就能看到系统中每个进程的线程。如果您没有访问平台构建器,也没关系。MSDN 提供了 函数签名,并告诉您它由coredll.lib导出(每个人都有)。
现在,我们拥有了开始收集线程处理器使用率统计信息所需的所有组件!
收集线程统计信息
现在我们必须考虑如何存储线程使用率统计信息。在本例中,我选择了嵌套的 std::map
关联容器。这使我们可以轻松地按数组样式访问数据,从而使我们的代码保持简洁优雅。
/// Time a thread has spent working
struct thread_times {
/// Time a thread has spent in kernel space
FILETIME kernel;
/// Time a thread has spent in user space
FILETIME user;
};
/// Time each process has spent working
/// @param DWORD - Thread ID
/// @param thread_times - Thread working times
typedef std::map< DWORD, thread_times > Threads;
/// Time each Process has spent working
/// @param DWORD - Process ID
/// @param Threads - Process' thread working times
typedef std::map< DWORD, Threads > Processes;
现在我们准备好将所有内容整合在一起。在本例中,我们将迭代系统中每个正在运行的线程,获取该线程使用的 CPU 时间,并返回我们的容器,将进程 ID 映射到线程 ID 和线程使用率。
/// Gets the list of currently running processes
Processes GetProcessList()
{
Processes process_list;
CosmicPowers we_are_powerful;
HANDLE snapshot = ::CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );
if( INVALID_HANDLE_VALUE != snapshot )
{
DWORD process_total = 0;
THREADENTRY32 te = { 0 };
te.dwSize = sizeof( THREADENTRY32 );
if( ::Thread32First( snapshot, &te ) )
{
do
{
FILETIME creation = { 0 },
exit = { 0 },
kernel = { 0 },
user = { 0 };
if( ::GetThreadTimes( ( HANDLE )te.th32ThreadID,
&creation,
&exit,
&kernel,
&user ) )
{
thread_times t = { kernel, user };
process_list[ te.th32OwnerProcessID ][ te.th32ThreadID ] = t;
}
} while( ::Thread32Next( snapshot, &te ) );
}
::CloseToolhelp32Snapshot( snapshot );
}
return process_list;
}
目前,我们的统计信息完全与 32 位进程标识符相关,这对于查看应用程序并想知道它占用多少处理器时间的任何人来说都不是很方便。因此,我们需要一种方法来 将 PID 与进程名称关联。
将 PID 与有用的名称关联
由于用户通过名称而不是 PID 来了解他们的应用程序,因此如果我们能将我们的信息与该进程名称关联起来会很好。为了与我们之前使用关联容器存储数据保持一致,我们在这里将再次使用它。这个容器会将唯一的 32 位进程标识符与其可执行文件的名称关联起来。
/// Associates process IDs to process names
/// @param DWORD - Process identifier
/// @Param std::wstring - process' executable's name
typedef std::map< DWORD, std::wstring > ProcessNames;
我们将再次转向 ToolHelp API,但这次我们将获取正在运行的进程的快照,而不是数量更多的线程列表。
/// Get a list associating currently running process IDs with names
ProcessNames GetProcessNameList()
{
ProcessNames name_list;
HANDLE snapshot = ::CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS |
TH32CS_SNAPNOHEAPS,
0 );
if( INVALID_HANDLE_VALUE != snapshot )
{
PROCESSENTRY32 pe = { 0 };
pe.dwSize = sizeof( PROCESSENTRY32 );
if( ::Process32First( snapshot, &pe ) )
{
do
{
name_list[ pe.th32ProcessID ] = pe.szExeFile;
} while( ::Process32Next( snapshot, &pe ) );
}
::CloseToolhelp32Snapshot( snapshot );
}
return name_list;
}
对于一个简单的示例,如果您想打印进程列表中每个进程的名称,您现在可以这样做
Processes procs = GetProcessList();
ProcessNames names = GetProcessNameList();
for( Processes::const_iterator p = procs.begin(); p != procs.end(); ++p )
NKDbgPrintfW( L"%s\r\n", names[ p->first ].c_str() );
现在,我们已经拥有了截至目前系统中每个线程的处理器使用率统计信息的完整快照。我们甚至知道该快照中每个进程的名称。但是,要获得百分比使用率,我们必须知道一个线程在固定时间内运行了多长时间。因此,现在我们必须 计算进程使用率统计信息。
计算进程使用率统计信息
虽然下面的代码看起来又长又吓人,但它主要是统计计算。它遵循的算法相当直接
- 获取所有正在运行进程的名称和 PID 的初始列表。
- 获取每个 PID 在内核模式和用户模式下花费时间的初始列表。
- 延迟间隔结束后,获取另一个列表,包含每个 PID 及其内核和用户时间。
- 计算在等待间隔期间每个进程在内核模式和用户模式下花费的时间。
- 如果任何进程 PID 不在我们名称列表中,请刷新我们的进程名称列表。这意味着我们有了一个新进程。
- 以某种方式将该统计信息报告给用户。
- 重复步骤 3。
// how often should we snapshot the system for new data?
DWORD interval = 3000;
// initial list of process IDs and names
PI::ProcessNames names = PI::GetProcessNameList();
// initial list of thread statistics
PI::Processes old_list = PI::GetProcessList();
DWORD start = ::GetTickCount();
while( true )
{
Sleep( interval );
PI::Processes new_list = PI::GetProcessList();
DWORD duration = ::GetTickCount() - start;
DWORD system_total = 0;
for( PI::Processes::const_iterator p2 = new_list.begin();
p2 != new_list.end();
++p2 )
{
PI::Processes::const_iterator p1 = old_list.find( p2->first );
if( p1 != old_list.end() )
{
DWORD user_total = 0;
DWORD kernel_total = 0;
for( PI::Threads::const_iterator t2 = p2->second.begin();
t2 != p2->second.end();
++t2 )
{
PI::Threads::const_iterator t1 = p1->second.find( t2->first );
if( t1 != p1->second.end() )
{
kernel_total += PI::GetThreadTick( t2->second.kernel ) -
PI::GetThreadTick( t1->second.kernel );
user_total += PI::GetThreadTick( t2->second.user ) -
PI::GetThreadTick( t1->second.user );
}
}
float user_percent = ( user_total ) /
static_cast< float >( duration ) * 100.0f;
float kernel_percent = ( kernel_total ) /
static_cast< float >( duration ) * 100.0f;
system_total += user_total + kernel_total;
// locate the process name by its ID
PI::ProcessNames::const_iterator found_name = names.find( p2->first );
// if the process ID isn't in the name list, it must be new.
// refresh the name list and try again.
if( found_name == names.end() )
{
names = PI::GetProcessNameList();
found_name = names.find( p2->first );
// still can't find the process ID? Just move on.
if( found_name == names.end() )
continue;
// At this point we have the process name, kernel time and user
// time. use this information in good health!
//
// user_percent = % of the time this process spent in user code
// kernel_percent = % of the time this process spent in kernel code
// found_name = name of the process' executable.
}
}
}
// calculate the total processor percent used
float percent_used = system_total / static_cast< float >( duration ) * 100.0f;
old_list = new_list;
start = ::GetTickCount();
}
结论
在本文中,我们讨论了如何获取收集所有正在运行线程信息的权限、如何获取每个线程使用处理器的耗时、哪些数据结构可用于组织该信息,以及最后如何使用该信息计算每个进程的 CPU 使用率统计信息。有关如何将该信息显示给用户的示例,请查看随附的示例应用程序。
历史
- 2011 年 2 月 18 日:初始版本