Windows系统进程监控工具开发指南

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍了如何开发一个系统工具,用于获取电脑进程列表并实时显示每个进程的CPU使用率、内存占用、上行和下载速度。通过使用Windows API和WinPCAP库,开发者可以深入掌握进程监控与网络流量分析技术,并实现通过PID结束进程的功能。该工具适用于系统管理、性能优化及网络监控软件的开发实践。
获取电脑进程列表,显示各个进程的CPU,内存,上行速度,下载速度

1. 进程监控基础概念

在现代计算机系统中,进程是程序执行的基本单位,承担着资源分配与任务调度的核心职责。理解进程的生命周期、状态转换及其与CPU、内存、I/O等资源的交互机制,是实现高效监控的前提。

进程通常经历就绪、运行、阻塞三种基本状态的切换,依赖于操作系统的调度算法和资源可用性。监控进程的核心目标在于实时掌握其资源消耗情况,及时发现异常行为,从而保障系统稳定与安全。

本章将从操作系统底层原理出发,深入解析进程管理机制,为后续章节中使用Windows API进行进程监控打下坚实基础。

2. 获取进程列表的Windows API使用方法

在Windows系统开发与系统级编程中,获取当前运行的进程列表是实现进程监控、资源分析和安全审计的重要基础操作。Windows提供了一系列系统级API,允许开发者以编程方式访问进程快照、读取进程信息并进行结构化处理。本章将深入解析Windows进程管理机制,并结合实际代码示例,详细讲解如何使用CreateToolhelp32Snapshot、Process32First、Process32Next等核心API函数,获取完整的进程列表信息,并进行格式化输出与权限控制。

2.1 Windows进程管理机制概述

Windows操作系统中的进程是资源分配和调度的基本单位,每个进程都有独立的虚拟地址空间、一组系统资源(如句柄、线程等)以及安全上下文。系统通过句柄(Handle)对进程进行引用和操作,开发者可以通过系统API访问这些句柄并获取进程的详细信息。

2.1.1 进程对象与句柄管理

Windows内核通过对象管理器(Object Manager)管理所有系统资源,包括进程对象。每个进程对象由一个唯一的进程标识符(PID)标识,系统通过句柄来访问这些对象。句柄是一个抽象的引用,它并不直接指向对象本身,而是作为访问令牌供程序使用。

进程句柄可以通过OpenProcess函数获取,该函数需要指定访问权限(如PROCESS_QUERY_INFORMATION、PROCESS_VM_READ等)。一旦获取句柄,即可调用其他API函数获取进程的详细信息,例如内存使用、线程状态、模块列表等。

2.1.2 Windows任务管理器与系统进程列表的获取原理

Windows任务管理器(Task Manager)是用户最常使用的系统监控工具之一,其“进程”标签页显示了当前系统中所有运行的进程。任务管理器底层调用的是Windows提供的进程快照API,例如CreateToolhelp32Snapshot,来获取当前进程列表。

具体流程如下:

graph TD
    A[任务管理器启动] --> B[调用CreateToolhelp32Snapshot创建快照]
    B --> C[获取进程快照数据]
    C --> D[遍历快照数据]
    D --> E[显示进程PID、名称、父进程等信息]

通过这一机制,任务管理器能够高效地获取并展示系统中所有进程的状态信息,为用户提供直观的监控界面。

2.2 使用Windows API获取进程列表

Windows API提供了一组用于进程快照处理的函数,其中最关键的是CreateToolhelp32Snapshot、Process32First和Process32Next。这些函数配合使用,可以获取当前系统中所有进程的列表。

2.2.1 CreateToolhelp32Snapshot函数的调用与参数解析

CreateToolhelp32Snapshot函数用于创建一个进程、线程或模块的快照。其原型如下:

HANDLE CreateToolhelp32Snapshot(
  DWORD dwFlags,
  DWORD th32ProcessID
);
  • dwFlags :指定要捕获的快照类型。常用的标志包括:
  • TH32CS_SNAPPROCESS :捕获进程快照。
  • TH32CS_SNAPTHREAD :捕获线程快照。
  • TH32CS_SNAPMODULE :捕获模块快照。
  • TH32CS_SNAPHEAP :捕获堆快照。
  • th32ProcessID :当 dwFlags 包含 TH32CS_SNAPHEAP 时,此参数为指定进程的PID;否则通常设为0。

调用示例:

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
    // 错误处理
}

逻辑分析
- TH32CS_SNAPPROCESS 表示我们要获取系统中所有进程的快照。
- 0 表示我们希望获取整个系统的进程列表,而不是某个特定进程下的子进程。
- 如果返回值为 INVALID_HANDLE_VALUE ,说明快照创建失败,需调用GetLastError获取错误代码。

2.2.2 Process32First与Process32Next函数的使用流程

在获取快照之后,需要使用Process32First和Process32Next函数来遍历快照中的每个进程。

Process32First函数原型:
BOOL Process32First(
  HANDLE hSnapshot,
  LPPROCESSENTRY32 lppe
);
Process32Next函数原型:
BOOL Process32Next(
  HANDLE hSnapshot,
  LPPROCESSENTRY32 lppe
);
  • hSnapshot :由CreateToolhelp32Snapshot返回的快照句柄。
  • lppe :指向PROCESSENTRY32结构体的指针,用于接收进程信息。

示例代码:

PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

if (!Process32First(hSnapshot, &pe32)) {
    // 错误处理
    CloseHandle(hSnapshot);
    return;
}

do {
    // 输出进程信息
    printf("Process Name: %s, PID: %u\n", pe32.szExeFile, pe32.th32ProcessID);
} while (Process32Next(hSnapshot, &pe32));

CloseHandle(hSnapshot);

逻辑分析
- 首先调用Process32First获取第一个进程的信息。
- 然后使用do-while循环调用Process32Next遍历所有进程。
- 每次循环中, pe32.szExeFile 表示进程的可执行文件名, pe32.th32ProcessID 为进程的PID。
- 最后调用CloseHandle关闭快照句柄。

2.2.3 获取进程PID、名称、父进程ID等关键信息

PROCESSENTRY32结构体中包含多个字段,其中常用字段如下:

字段名 描述
dwSize 结构体大小,必须初始化
cntUsage 引用计数(已弃用)
th32ProcessID 进程ID(PID)
th32DefaultHeapID 默认堆ID(已弃用)
th32ModuleID 模块ID
cntThreads 线程数
th32ParentProcessID 父进程ID
pcPriClassBase 优先级基值
dwFlags 标志位
szExeFile 可执行文件名(MAX_PATH长度)

例如,获取父进程ID的代码如下:

printf("Parent PID: %u\n", pe32.th32ParentProcessID);

通过这些字段,我们可以获取到进程的完整信息,并用于后续的监控与分析。

2.3 进程信息的解析与输出

获取到原始进程数据后,需要对数据进行结构化处理、筛选和格式化输出,以满足不同监控需求。

2.3.1 进程快照数据的结构化处理

为了便于后续处理,可以将进程信息封装成结构体数组或链表。例如:

typedef struct _PROCESS_INFO {
    DWORD pid;
    char name[MAX_PATH];
    DWORD parentPid;
} PROCESS_INFO, *PPROCESS_INFO;

然后在遍历快照时将数据填充到结构体中:

PROCESS_INFO processes[1024];
int count = 0;

do {
    processes[count].pid = pe32.th32ProcessID;
    strcpy_s(processes[count].name, MAX_PATH, pe32.szExeFile);
    processes[count].parentPid = pe32.th32ParentProcessID;
    count++;
} while (Process32Next(hSnapshot, &pe32));

2.3.2 进程信息的筛选与格式化输出

我们可以根据需求筛选特定进程,例如只显示“explorer.exe”或PID大于1000的进程:

for (int i = 0; i < count; i++) {
    if (strcmp(processes[i].name, "explorer.exe") == 0) {
        printf("Explorer PID: %u\n", processes[i].pid);
    }
}

格式化输出可以使用printf、sprintf等函数,或者输出为CSV、JSON等结构化格式。

2.3.3 示例代码演示与调试技巧

以下是一个完整的获取并输出进程列表的示例代码:

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

int main() {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %u\n", GetLastError());
        return 1;
    }

    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    if (!Process32First(hSnapshot, &pe32)) {
        printf("Failed to get first process. Error: %u\n", GetLastError());
        CloseHandle(hSnapshot);
        return 1;
    }

    printf("PID\t\tProcess Name\n");
    printf("--------------------------------\n");

    do {
        printf("%u\t\t%s\n", pe32.th32ProcessID, pe32.szExeFile);
    } while (Process32Next(hSnapshot, &pe32));

    CloseHandle(hSnapshot);
    return 0;
}

调试技巧
- 使用调试器设置断点,观察pe32结构体的各个字段。
- 检查hSnapshot是否为有效句柄。
- 调用GetLastError()获取错误码,定位API调用失败原因。
- 添加日志输出,跟踪代码执行流程。

2.4 进程权限与访问控制

在访问进程信息时,可能会遇到权限不足的问题。例如,某些系统进程(如PID=4的System进程)需要管理员权限才能访问其详细信息。

2.4.1 获取系统权限的必要性

部分进程(如系统进程、服务进程)具有较高的安全上下文,普通用户权限无法访问其完整信息。例如,调用OpenProcess时如果权限不足,将返回错误。

2.4.2 OpenProcess函数的使用与错误处理

OpenProcess函数用于获取指定进程的句柄,其原型如下:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);
  • dwDesiredAccess :访问权限,如PROCESS_QUERY_INFORMATION。
  • bInheritHandle :是否可继承句柄。
  • dwProcessId :目标进程的PID。

示例代码:

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ProcessID);
if (hProcess == NULL) {
    printf("Failed to open process %u. Error: %u\n", pe32.th32ProcessID, GetLastError());
}

错误处理建议
- 检查错误码:常见错误包括ERROR_ACCESS_DENIED(权限不足)、ERROR_INVALID_PARAMETER(PID无效)。
- 提升程序权限:以管理员身份运行程序可提高访问权限。
- 使用SeDebugPrivilege:启用调试权限可增强对系统进程的访问能力。

通过本章的深入讲解,读者不仅掌握了Windows系统中获取进程列表的核心API函数及其调用流程,还了解了如何对进程信息进行结构化处理、筛选输出以及权限控制。这些内容为后续章节中实现更复杂的进程监控功能(如CPU、内存监控)打下了坚实基础。

3. CPU使用率获取与计算实现

CPU是计算机系统中最核心的资源之一,其使用率的监控直接关系到系统的性能分析与资源调度。本章将从操作系统调度机制出发,深入剖析如何获取系统级与进程级别的CPU使用率,并结合Windows API实现具体的数据采集与计算逻辑。通过本章内容,读者将掌握从底层系统调用到高阶性能监控的完整实现路径,为构建高效的系统监控工具打下坚实基础。

3.1 CPU调度与时间片分配机制

Windows操作系统采用抢占式多任务调度机制,通过时间片轮转的方式为各个线程分配CPU时间。理解CPU调度机制是准确获取CPU使用率的前提。

3.1.1 线程状态与CPU占用时间的关系

线程在运行过程中会经历多种状态变化,包括就绪(Ready)、运行(Running)、等待(Waiting)等。CPU时间主要消耗在运行状态下的线程上。

  • 运行状态 :线程正在执行,消耗CPU时间。
  • 就绪状态 :线程等待调度器分配CPU时间片。
  • 等待状态 :线程等待I/O、锁、信号量等资源,不消耗CPU时间。

通过Windows线程调度器提供的API,可以获取每个线程在不同状态下的累计时间,进而推算出其对CPU的占用情况。

线程时间统计结构体

Windows中用于获取线程时间的结构体是 FILETIME ,其定义如下:

typedef struct _FILETIME {
  DWORD dwLowDateTime;
  DWORD dwHighDateTime;
} FILETIME, *PFILETIME;

每个线程的用户态和内核态时间可以通过 GetThreadTimes 函数获取:

BOOL GetThreadTimes(
  HANDLE hThread,
  LPFILETIME lpCreationTime,
  LPFILETIME lpExitTime,
  LPFILETIME lpKernelTime,
  LPFILETIME lpUserTime
);
参数名 说明
hThread 线程句柄
lpCreationTime 线程创建时间
lpExitTime 线程退出时间
lpKernelTime 内核态执行时间(100纳秒单位)
lpUserTime 用户态执行时间(100纳秒单位)
示例:获取当前线程的时间信息
#include <windows.h>
#include <stdio.h>

int main() {
    HANDLE hThread = GetCurrentThread();
    FILETIME creationTime, exitTime, kernelTime, userTime;

    if (GetThreadTimes(hThread, &creationTime, &exitTime, &kernelTime, &userTime)) {
        ULARGE_INTEGER user, kernel;
        user.LowPart = userTime.dwLowDateTime;
        user.HighPart = userTime.dwHighDateTime;

        kernel.LowPart = kernelTime.dwLowDateTime;
        kernel.HighPart = kernelTime.dwHighDateTime;

        printf("User mode time: %llu ns\n", user.QuadPart * 100);
        printf("Kernel mode time: %llu ns\n", kernel.QuadPart * 100);
    } else {
        printf("Failed to get thread times.\n");
    }

    return 0;
}

逐行解释
- GetCurrentThread() :获取当前线程句柄。
- GetThreadTimes() :获取线程的各个时间戳。
- 使用 ULARGE_INTEGER 结构将 FILETIME 转换为64位整数,便于计算。
- 输出用户态与内核态时间,单位转换为纳秒(1单位 = 100纳秒)。

3.1.2 用户态与内核态时间划分

操作系统将CPU时间划分为用户态(User Mode)与内核态(Kernel Mode)两部分:

  • 用户态时间 :应用程序直接执行的代码时间。
  • 内核态时间 :由操作系统内核执行的服务时间,如系统调用、中断处理等。

监控这两类时间可以帮助我们判断CPU瓶颈是来自应用程序本身还是系统调用层。

系统时间划分流程图
graph TD
    A[System CPU Time] --> B[User Mode Time]
    A --> C[Kernel Mode Time]
    A --> D[Interruption Time]
    A --> E[DPC/Interrupt Time]

说明 :系统总CPU时间由多个部分组成,其中用户态和内核态时间是主要组成部分,而中断处理时间则反映系统响应外部事件的开销。

3.2 获取CPU使用率的核心方法

获取CPU使用率的基本思路是通过采集系统或进程在不同时间点的CPU时间戳,计算其差值,从而得出单位时间内的CPU占用比例。

3.2.1 GetSystemTimes函数的调用与系统时间解析

Windows提供 GetSystemTimes 函数用于获取系统的空闲、用户态和内核态时间:

BOOL GetSystemTimes(
  PFILETIME lpIdleTime,
  PFILETIME lpKernelTime,
  PFILETIME lpUserTime
);
参数名 说明
lpIdleTime 系统空闲时间
lpKernelTime 内核态总时间
lpUserTime 用户态总时间

这些时间值是累计的,因此需要两次调用取差值来计算CPU使用率。

示例:计算系统CPU使用率
#include <windows.h>
#include <stdio.h>
#include <stdint.h>

double CalculateCPULoad(uint64_t idleTicks, uint64_t totalTicks) {
    double idleRatio = (double)idleTicks / totalTicks;
    return (1.0 - idleRatio) * 100.0;
}

uint64_t FileTimeToInt64(const FILETIME *ft) {
    uint64_t high = ft->dwHighDateTime;
    uint64_t low = ft->dwLowDateTime;
    return (high << 32) | low;
}

void GetCPULoad() {
    FILETIME idleTime, kernelTime, userTime;

    if (GetSystemTimes(&idleTime, &kernelTime, &userTime)) {
        static uint64_t lastTotal = 0, lastIdle = 0;

        uint64_t currentTotal = FileTimeToInt64(&kernelTime) + FileTimeToInt64(&userTime);
        uint64_t currentIdle = FileTimeToInt64(&idleTime);

        uint64_t totalDelta = currentTotal - lastTotal;
        uint64_t idleDelta = currentIdle - lastIdle;

        if (totalDelta != 0) {
            double cpuLoad = CalculateCPULoad(idleDelta, totalDelta);
            printf("Current CPU Load: %.2f%%\n", cpuLoad);
        }

        lastTotal = currentTotal;
        lastIdle = currentIdle;
    } else {
        printf("Failed to get system times.\n");
    }
}

int main() {
    while (1) {
        GetCPULoad();
        Sleep(1000); // 每秒更新一次
    }
    return 0;
}

逐行解释
- GetSystemTimes 获取系统时间戳。
- FileTimeToInt64 FILETIME 转换为64位整数。
- CalculateCPULoad 根据空闲与总时间差值计算CPU使用率。
- 主循环中每秒调用一次,实现动态监控。

3.2.2 使用NtQuerySystemInformation函数获取详细信息

若需获取更详细的CPU使用信息,如每个逻辑处理器的使用率,可以使用未公开的 NtQuerySystemInformation 函数,需加载 ntdll.dll

函数原型定义
typedef NTSTATUS (NTAPI *PNtQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
);
示例:获取系统处理器性能信息
#include <windows.h>
#include <stdio.h>

#define SystemProcessorPerformanceInformation 8

typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION {
    LARGE_INTEGER IdleTime;
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER DpcTime;
    LARGE_INTEGER InterruptTime;
    ULONG   InterruptCount;
} SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION, *PSYSTEM_PROCESSOR_PERFORMANCE_INFORMATION;

int main() {
    HMODULE hNtDll = LoadLibrary("ntdll.dll");
    if (!hNtDll) {
        printf("Failed to load ntdll.dll\n");
        return -1;
    }

    PNtQuerySystemInformation NtQuerySystemInformation = 
        (PNtQuerySystemInformation)GetProcAddress(hNtDll, "NtQuerySystemInformation");
    if (!NtQuerySystemInformation) {
        printf("Failed to get NtQuerySystemInformation\n");
        return -1;
    }

    SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION info[64];
    ULONG size;
    NTSTATUS status = NtQuerySystemInformation(SystemProcessorPerformanceInformation, info, sizeof(info), &size);

    if (status == 0) {
        int processorCount = size / sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION);
        for (int i = 0; i < processorCount; i++) {
            ULONGLONG total = info[i].KernelTime.QuadPart + info[i].UserTime.QuadPart;
            ULONGLONG idle = info[i].IdleTime.QuadPart;

            double usage = (total - idle) * 100.0 / total;
            printf("CPU Core %d: %.2f%%\n", i, usage);
        }
    } else {
        printf("NtQuerySystemInformation failed with status %lx\n", status);
    }

    FreeLibrary(hNtDll);
    return 0;
}

逐行解释
- 使用 LoadLibrary GetProcAddress 动态加载 ntdll.dll
- 定义 SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION 结构体。
- 调用 NtQuerySystemInformation 获取各逻辑处理器的性能信息。
- 计算每个核心的使用率并输出。

3.3 进程级别的CPU使用率计算

3.3.1 获取单个进程的CPU时间戳

获取进程级别的CPU使用率,关键在于获取进程的用户态与内核态时间。可以使用 OpenProcess GetProcessTimes 函数实现。

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);

BOOL GetProcessTimes(
  HANDLE hProcess,
  LPFILETIME lpCreationTime,
  LPFILETIME lpExitTime,
  LPFILETIME lpKernelTime,
  LPFILETIME lpUserTime
);
示例:获取指定进程的CPU时间
#include <windows.h>
#include <stdio.h>

void GetProcessCPUUsage(DWORD pid) {
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
    if (!hProcess) {
        printf("Failed to open process %lu\n", pid);
        return;
    }

    FILETIME creationTime, exitTime, kernelTime, userTime;
    if (GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime)) {
        ULARGE_INTEGER user, kernel;
        user.LowPart = userTime.dwLowDateTime;
        user.HighPart = userTime.dwHighDateTime;

        kernel.LowPart = kernelTime.dwLowDateTime;
        kernel.HighPart = kernelTime.dwHighDateTime;

        printf("Process %lu - User mode time: %llu ns\n", pid, user.QuadPart * 100);
        printf("Process %lu - Kernel mode time: %llu ns\n", pid, kernel.QuadPart * 100);
    } else {
        printf("Failed to get process times for %lu\n", pid);
    }

    CloseHandle(hProcess);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <pid>\n", argv[0]);
        return 1;
    }

    DWORD pid = atoi(argv[1]);
    GetProcessCPUUsage(pid);
    return 0;
}

逐行解释
- OpenProcess 获取指定PID的进程句柄。
- GetProcessTimes 获取进程的时间戳。
- 将时间转换为纳秒并输出。

3.3.2 时间差值计算与百分比换算

要获得CPU使用率,需在两个时间点分别获取时间戳,并计算差值。

ULARGE_INTEGER prevUser, prevKernel, prevTotal;
ULARGE_INTEGER currUser, currKernel, currTotal;

currUser.QuadPart = FileTimeToInt64(userTime);
currKernel.QuadPart = FileTimeToInt64(kernelTime);
currTotal.QuadPart = currUser.QuadPart + currKernel.QuadPart;

double deltaTime = (double)(currTotal.QuadPart - prevTotal.QuadPart);
double deltaUser = (double)(currUser.QuadPart - prevUser.QuadPart);

double cpuUsage = (deltaUser / deltaTime) * 100.0;

3.3.3 多核CPU下的平均与峰值计算

对于多核系统,需分别监控每个核心的使用率,并计算整体平均与峰值。

示例:多核CPU使用率汇总表
核心编号 使用率(%)
0 12.5
1 23.8
2 5.3
3 17.1
平均值 14.675
峰值 23.8

3.4 实时监控与性能优化

3.4.1 定时器与多线程更新机制

为实现高效的实时监控,可使用 SetTimer CreateThread 实现定时刷新机制。

使用定时器刷新CPU使用率
SetTimer(hWnd, 1, 1000, NULL); // 每秒刷新一次

WM_TIMER 消息中调用刷新函数。

使用多线程实现异步监控
HANDLE hThread = CreateThread(NULL, 0, MonitorThread, NULL, 0, NULL);

3.4.2 避免频繁调用API带来的性能损耗

频繁调用 GetSystemTimes GetProcessTimes 可能带来性能开销。建议:

  • 设置合理采样间隔 (如1秒)
  • 缓存前一次时间戳
  • 仅在界面刷新时重新计算

此外,使用性能计数器(如 QueryPerformanceCounter )可进一步提升精度。

本章从CPU调度机制出发,深入剖析了系统级与进程级CPU使用率的获取方法,并通过代码示例展示了具体实现逻辑。下一章将围绕内存监控展开,进一步完善系统资源监控的知识体系。

4. 内存占用监控实现

4.1 内存管理的基本原理

4.1.1 虚拟内存与物理内存的关系

在现代操作系统中,内存管理的核心机制是 虚拟内存系统 ,它为每个进程提供了一个独立的、连续的地址空间,使得每个进程都认为自己拥有整个系统的内存资源。这种抽象的地址空间被称为 虚拟地址空间 ,而实际的物理内存则是被多个进程共享的。

虚拟内存与物理内存之间的映射关系由 页表(Page Table) 管理。操作系统将虚拟内存划分为固定大小的 内存页(Page) ,通常是4KB。每个内存页通过页表项(PTE)映射到物理内存中的某个 页帧(Page Frame) 。当进程访问某个虚拟地址时,CPU的内存管理单元(MMU)会自动查找页表,将其转换为对应的物理地址。

这种机制带来了几个关键优势:

  • 内存隔离 :每个进程拥有独立的虚拟地址空间,避免了进程间的直接内存干扰。
  • 按需加载 :只将当前需要的页面加载到物理内存中,其余部分可以保存在磁盘(页面文件或交换分区)中。
  • 内存扩展 :允许进程使用的虚拟内存大小超过物理内存总量。

虚拟内存与物理内存之间的关系可以用下图表示:

graph TD
    A[虚拟地址空间] -->|分页| B(页表)
    B --> C[物理内存]
    B --> D[页面文件]
    C --> E[实际物理内存]
    D --> F[磁盘上的虚拟内存]

通过这种方式,操作系统能够更高效地管理内存资源,同时为进程提供灵活的内存访问机制。

4.1.2 内存页、工作集与私有内存的概念

理解内存监控的关键在于掌握内存管理中的几个核心概念: 内存页、工作集、私有内存

内存页(Memory Page)

如前所述,内存页是虚拟内存的基本单位,通常大小为4KB。操作系统通过页表将虚拟内存页映射到物理内存页帧。

工作集(Working Set)

工作集是指一个进程当前正在使用的内存页集合。这些页面被保留在物理内存中,以便进程可以快速访问而不需要从磁盘换入。工作集的大小会动态变化,取决于进程的运行状态和操作系统的内存调度策略。

工作集的大小受限于系统的可用物理内存和操作系统的调度策略。当物理内存不足时,操作系统会将不常用的页面换出到磁盘(页面文件),从而缩小工作集。

私有内存(Private Memory)

私有内存是指一个进程中 不能被其他进程共享 的内存区域。它包括进程的代码段、堆栈、堆空间等。私有内存的大小反映了进程实际占用的物理内存资源,不会因为其他进程的运行而被共享或复用。

为了更直观地理解这些概念,以下表格总结了它们的区别与联系:

概念 描述 用途与特点
内存页 虚拟内存的基本单位,通常为4KB 用于内存映射、换入换出机制,便于操作系统管理
工作集 进程当前正在使用的内存页集合 影响进程性能,工作集越大,访问速度越快
私有内存 进程独占的内存空间,不与其他进程共享 反映进程的独立内存开销,用于内存监控和资源分析

理解这些概念有助于我们在后续章节中更准确地分析进程的内存使用情况,尤其是在进行内存监控和性能优化时具有重要意义。

4.2 获取进程内存信息的API

4.2.1 GlobalMemoryStatusEx函数的系统级内存统计

在Windows系统中, GlobalMemoryStatusEx 函数可以用来获取系统范围的内存使用情况。它返回一个 MEMORYSTATUSEX 结构,其中包含了当前系统的总内存、可用内存、已使用内存等信息。

以下是使用 GlobalMemoryStatusEx 的示例代码:

#include <windows.h>
#include <stdio.h>

int main() {
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(MEMORYSTATUSEX);
    if (GlobalMemoryStatusEx(&memInfo)) {
        printf("Total Memory: %llu MB\n", memInfo.ullTotalPhys / (1024 * 1024));
        printf("Available Memory: %llu MB\n", memInfo.ullAvailPhys / (1024 * 1024));
        printf("Used Memory: %llu MB\n", (memInfo.ullTotalPhys - memInfo.ullAvailPhys) / (1024 * 1024));
        printf("Memory Load: %lu%%\n", memInfo.dwMemoryLoad);
    } else {
        printf("Failed to retrieve memory status.\n");
    }
    return 0;
}
代码解析:
  • MEMORYSTATUSEX 是一个结构体,用于接收系统内存信息。
  • dwLength 字段必须设置为结构体的大小,以便函数可以正确识别版本。
  • GlobalMemoryStatusEx 调用成功后,结构体中的字段将被填充。
  • ullTotalPhys 表示总物理内存大小(以字节为单位)。
  • ullAvailPhys 表示当前可用的物理内存。
  • dwMemoryLoad 表示当前内存使用百分比。

该函数适用于监控整个系统的内存状态,帮助我们了解整体内存使用情况。

4.2.2 GetProcessMemoryInfo函数的进程级内存查询

除了系统级内存统计,我们还可以使用 GetProcessMemoryInfo 函数来获取特定进程的内存使用情况。该函数需要传入一个进程句柄,并返回一个 PROCESS_MEMORY_COUNTERS_EX 结构,其中包含了进程的内存统计信息。

以下是一个使用 GetProcessMemoryInfo 的示例代码:

#include <windows.h>
#include <psapi.h>
#include <stdio.h>

#pragma comment(lib, "psapi.lib")

int main() {
    DWORD pid = 0;  // 目标进程的PID
    printf("Enter the PID of the process to monitor: ");
    scanf("%lu", &pid);

    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
    if (!hProcess) {
        printf("Failed to open process.\n");
        return 1;
    }

    PROCESS_MEMORY_COUNTERS_EX pmc;
    if (GetProcessMemoryInfo((HANDLE)hProcess, (PPROCESS_MEMORY_COUNTERS)&pmc, sizeof(pmc))) {
        printf("Working Set Size: %llu KB\n", pmc.WorkingSetSize / 1024);
        printf("Private Usage: %llu KB\n", pmc.PrivateUsage / 1024);
        printf("Peak Working Set Size: %llu KB\n", pmc.PeakWorkingSetSize / 1024);
    } else {
        printf("Failed to get process memory info.\n");
    }

    CloseHandle(hProcess);
    return 0;
}
代码解析:
  • OpenProcess 用于获取目标进程的句柄,需要 PROCESS_QUERY_INFORMATION PROCESS_VM_READ 权限。
  • GetProcessMemoryInfo 接收进程句柄,并填充 PROCESS_MEMORY_COUNTERS_EX 结构。
  • WorkingSetSize 表示当前进程的工作集大小(即当前在物理内存中的页面大小)。
  • PrivateUsage 表示该进程的私有内存大小。
  • PeakWorkingSetSize 表示该进程历史上的最大工作集大小。

该函数非常适合用于监控特定进程的内存使用情况,帮助我们分析内存瓶颈和潜在的内存泄漏问题。

4.3 内存使用的多维度分析

4.3.1 工作集大小(Working Set)与私有字节数(Private Bytes)

在监控进程内存使用时,两个关键指标是 工作集大小(Working Set Size) 私有字节数(Private Bytes)

工作集大小(Working Set)

工作集大小指的是当前进程中正在使用的、驻留在物理内存中的内存总量。这个值会随着进程的运行状态和系统内存压力而变化。工作集大小较大意味着该进程当前活跃地使用较多内存,可能会影响其他进程的执行效率。

私有字节数(Private Bytes)

私有字节数是指进程独占的内存总量,这部分内存不能被其他进程共享。它包括堆、栈、代码段等私有内存区域。私有字节数的增长通常表示进程正在分配新的内存,如果持续增长而没有释放,可能表明存在内存泄漏。

4.3.2 提交大小(Commit Size)与峰值内存使用

除了工作集和私有内存,我们还需要关注 提交大小(Commit Size) 峰值内存使用(Peak Working Set Size)

提交大小(Commit Size)

提交大小是指进程已提交给操作系统的虚拟内存总量。它包括已分配的内存页,即使这些页面尚未被写入或读取。提交大小的增加可能意味着进程正在请求更多的虚拟内存资源。

峰值内存使用(Peak Working Set Size)

峰值内存使用是指该进程在其生命周期中曾经达到的最大工作集大小。它可以帮助我们了解该进程在高负载下的内存需求。

以下表格总结了这四个内存指标的含义和用途:

指标名称 含义描述 应用场景
工作集大小(Working Set) 当前驻留在物理内存中的内存总量 监控进程当前的内存使用情况
私有字节数(Private Bytes) 进程独占的内存总量 分析内存泄漏、内存分配行为
提交大小(Commit Size) 进程申请的虚拟内存总量 判断进程的内存需求和系统资源压力
峰值内存使用(Peak Working Set) 进程历史上达到的最大工作集大小 评估进程在高负载时的内存占用情况

4.3.3 内存泄漏的初步判断指标

内存泄漏是指程序在运行过程中不断分配内存而未能正确释放,导致内存占用持续增长。要判断是否存在内存泄漏,我们可以监控以下指标:

  1. 私有内存持续增长 :如果进程的私有内存不断增长且没有下降趋势,可能存在内存泄漏。
  2. 工作集大小异常波动 :如果工作集大小在没有明显负载变化的情况下剧烈波动,也可能暗示内存问题。
  3. 提交大小不断上升 :如果提交大小持续增长,可能表示程序在不断申请新的虚拟内存空间。

为了更有效地判断内存泄漏,可以使用性能监视工具(如Windows任务管理器、PerfMon、VisualVM等)进行长期监控,并结合日志分析来定位内存分配和释放的问题。

4.4 内存监控的实践应用

4.4.1 内存使用趋势图的绘制方法

为了更直观地观察内存使用情况,我们可以绘制内存使用趋势图。通常可以使用Python结合Matplotlib库来实现动态监控并绘制图表。

以下是一个使用Python实现的简单内存监控脚本:

import psutil
import matplotlib.pyplot as plt
import time

# 初始化数据列表
times = []
mem_usages = []

# 设置监控时间(秒)
duration = 60
interval = 1  # 每秒采集一次数据

plt.ion()  # 启用交互模式
fig, ax = plt.subplots()
line, = ax.plot(times, mem_usages, 'b-')

ax.set_xlabel('Time (s)')
ax.set_ylabel('Memory Usage (MB)')
ax.set_title('Memory Usage Over Time')

start_time = time.time()

while True:
    current_time = time.time() - start_time
    if current_time > duration:
        break

    mem_info = psutil.virtual_memory()
    mem_usage = mem_info.used / (1024 ** 2)  # 转换为MB
    times.append(current_time)
    mem_usages.append(mem_usage)

    line.set_xdata(times)
    line.set_ydata(mem_usages)

    ax.relim()
    ax.autoscale_view()
    fig.canvas.draw()
    fig.canvas.flush_events()
    time.sleep(interval)

plt.ioff()
plt.show()
代码解析:
  • 使用 psutil 库获取系统内存信息。
  • 使用 matplotlib 动态绘制内存使用趋势图。
  • 每隔一秒采集一次内存使用数据,并更新图表。
  • 最终绘制出内存使用随时间变化的趋势图。

该脚本可用于实时监控系统内存使用情况,帮助我们识别内存使用异常。

4.4.2 内存异常预警与阈值设置

在实际应用中,我们还需要设置内存使用预警机制,当内存使用超过设定阈值时进行通知或采取相应措施。

以下是一个基于Python的内存预警脚本:

import psutil
import time
import smtplib
from email.mime.text import MIMEText

# 设置内存使用警戒值(单位:MB)
THRESHOLD = 800  
CHECK_INTERVAL = 5  # 检查间隔时间(秒)

def send_alert_email(memory_usage):
    sender = "admin@example.com"
    receiver = "alert@example.com"
    password = "yourpassword"

    subject = "Memory Usage Alert"
    body = f"Warning: System memory usage is above threshold. Current usage: {memory_usage:.2f} MB"

    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = sender
    msg["To"] = receiver

    try:
        server = smtplib.SMTP("smtp.example.com", 587)
        server.starttls()
        server.login(sender, password)
        server.sendmail(sender, receiver, msg.as_string())
        print("Alert email sent.")
    except Exception as e:
        print("Failed to send alert email:", e)
    finally:
        server.quit()

while True:
    mem_info = psutil.virtual_memory()
    mem_usage = mem_info.used / (1024 ** 2)  # 转换为MB

    print(f"Current Memory Usage: {mem_usage:.2f} MB")
    if mem_usage > THRESHOLD:
        print("Memory usage exceeds threshold!")
        send_alert_email(mem_usage)

    time.sleep(CHECK_INTERVAL)
代码解析:
  • 使用 psutil 获取内存使用情况。
  • 如果内存使用超过设定阈值(如800MB),则调用 send_alert_email 发送邮件预警。
  • 邮件发送使用SMTP协议,需配置SMTP服务器和账户信息。

该脚本可以用于服务器环境下的内存监控预警系统,及时通知管理员采取措施,防止内存溢出或系统崩溃。

5. 网络数据包捕获与进程流量关联分析

5.1 网络监控的必要性与挑战

在现代系统中,网络流量已成为衡量系统性能与安全性的关键指标之一。随着云计算、微服务架构的普及,进程之间的通信越来越多地依赖网络,因此,对网络数据包的捕获与进程流量的关联分析显得尤为重要。

5.1.1 网络带宽与进程行为的关联性

一个进程的网络行为直接影响系统整体的带宽使用。例如,某些恶意软件会通过后台上传敏感数据,造成带宽资源的浪费,甚至导致系统响应缓慢。通过监控每个进程的网络流量,可以有效识别异常行为,及时采取措施。

此外,开发人员和系统管理员也常常需要了解某个服务或应用的网络使用情况,以优化网络性能,提升用户体验。

5.1.2 传统监控工具的局限性

传统的网络监控工具如Wireshark、tcpdump等虽然可以捕获和分析网络数据包,但它们通常只能提供协议层面的信息,而无法将流量与具体的进程关联起来。例如,我们可能看到某个IP地址和端口有大量数据传输,但无法直接知道是哪个进程在使用这些连接。

为了解决这个问题,我们需要结合网络数据包捕获技术与进程管理接口,将网络流量映射到具体的进程上,实现更细粒度的监控。

5.2 WinPcap/Npcap库的安装与初始化

要实现网络数据包的捕获与分析,Windows平台下常用的库是WinPcap(已停止更新)和其继任者Npcap。Npcap支持现代Windows系统,并提供了丰富的API接口。

5.2.1 WinPcap/Npcap的架构与功能简介

Npcap的核心组件包括:

  • NPF(Netgroup Packet Filter)驱动 :负责底层的数据包捕获。
  • Packet.dll :提供用户层的API接口。
  • wpcap.dll :提供更高级的封装接口,兼容WinPcap API。

通过这些组件,开发者可以实现:

  • 网络接口的枚举
  • 数据包的捕获与过滤
  • 数据包的注入(发送)

5.2.2 安装配置与驱动启用流程

  1. 下载Npcap安装包(https://nmap.org/npcap/)
  2. 运行安装程序,选择“Install Npcap in WinPcap API-compatible Mode”
  3. 安装完成后,系统会自动启用NPF服务

验证是否安装成功:

  • 打开命令提示符,执行:
    bash sc query npf
    如果服务状态为“RUNNING”,说明Npcap已正确安装并运行。

5.2.3 获取网卡列表与打开适配器

使用Npcap库获取本地网络适配器列表并打开指定网卡的代码示例如下:

#include <pcap.h>
#include <stdio.h>

int main() {
    pcap_if_t *alldevs;
    pcap_if_t *d;
    char errbuf[PCAP_ERRBUF_SIZE];

    // 获取设备列表
    if (pcap_findalldevs(&alldevs, errbuf) == -1) {
        fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
        return 1;
    }

    // 打印网卡列表
    int i = 0;
    for (d = alldevs; d != NULL; d = d->next) {
        printf("%d. %s", ++i, d->name);
        if (d->description)
            printf(" (%s)\n", d->description);
        else
            printf(" (No description available)\n");
    }

    if (i == 0) {
        printf("\nNo interfaces found! Make sure Npcap is installed.\n");
        return 2;
    }

    int inum;
    printf("Enter the interface number (1-%d): ", i);
    scanf("%d", &inum);

    // 跳转到选中的适配器
    for (d = alldevs, i = 1; i < inum; d = d->next, i++);

    // 打开适配器
    pcap_t *adhandle = pcap_open_live(d->name, 65536, 1, 1000, errbuf);
    if (!adhandle) {
        fprintf(stderr, "\nUnable to open the adapter. %s is not supported by Npcap\n", d->name);
        pcap_freealldevs(alldevs);
        return 3;
    }

    printf("\nListening on %s...\n", d->description);

    // 释放设备列表
    pcap_freealldevs(alldevs);

    return 0;
}

代码说明:

  • pcap_findalldevs() :枚举本地所有网络接口。
  • pcap_open_live() :打开指定网卡,参数 65536 表示最大捕获长度, 1 表示混杂模式。
  • errbuf :用于存储错误信息。

5.3 数据包捕获与解析

一旦网卡被打开,就可以使用 pcap_loop() 函数开始捕获数据包。捕获到的数据包是原始二进制格式,需要根据协议头(如以太网、IP、TCP/UDP)进行解析。

5.3.1 使用pcap_loop进行数据包实时捕获

void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) {
    // 解析数据包的逻辑将在后续章节实现
    printf("Packet captured, length: %d\n", header->len);
}

int main() {
    // ...(前面获取适配器的代码)

    // 开始捕获数据包
    pcap_loop(adhandle, 0, packet_handler, NULL);

    // 关闭适配器
    pcap_close(adhandle);

    return 0;
}
  • pcap_loop() :持续捕获数据包,直到手动停止。
  • packet_handler :回调函数,每次捕获到数据包时调用。

5.3.2 解析IP、TCP、UDP协议头信息

数据包结构如下:

+---------------------+
|     Ethernet Header |
+---------------------+
|         IP Header   |
+---------------------+
|        TCP/UDP Header|
+---------------------+
|       Data Payload  |
+---------------------+

定义结构体解析协议头:

struct ether_header {
    u_int8_t  ether_dhost[6];  // 目标MAC地址
    u_int8_t  ether_shost[6];  // 源MAC地址
    u_int16_t ether_type;      // 类型
};

struct ip_header {
    u_int8_t  ver_ihl;         // 版本和首部长度
    u_int8_t  tos;             // 服务类型
    u_int16_t tlen;            // 总长度
    u_int16_t identification;  // 标识符
    u_int16_t flags_fo;        // 标志位+片偏移
    u_int8_t  ttl;             // 生存时间
    u_int8_t  proto;           // 协议类型
    u_int16_t crc;             // 校验和
    u_int8_t  saddr[4];        // 源IP地址
    u_int8_t  daddr[4];        // 目的IP地址
};

struct tcp_header {
    u_int16_t sport;           // 源端口
    u_int16_t dport;           // 目的端口
    u_int32_t seqnum;          // 序列号
    u_int32_t acknum;          // 确认号
    u_int8_t  offset;          // 数据偏移
    u_int8_t  flags;           // 标志位
    u_int16_t window;          // 窗口大小
    u_int16_t checksum;        // 校验和
    u_int16_t urgptr;          // 紧急指针
};

packet_handler 中解析:

void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) {
    struct ether_header *eth = (struct ether_header *)pkt_data;
    if (ntohs(eth->ether_type) == 0x0800) {  // IP协议
        struct ip_header *ip = (struct ip_header *)(pkt_data + 14);
        printf("IP Packet: %d.%d.%d.%d -> %d.%d.%d.%d\n",
               ip->saddr[0], ip->saddr[1], ip->saddr[2], ip->saddr[3],
               ip->daddr[0], ip->daddr[1], ip->daddr[2], ip->daddr[3]);

        if (ip->proto == 6) {  // TCP协议
            struct tcp_header *tcp = (struct tcp_header *)((u_int8_t*)ip + ((ip->ver_ihl & 0xf) * 4));
            printf("TCP: %d -> %d\n", ntohs(tcp->sport), ntohs(tcp->dport));
        } else if (ip->proto == 17) {  // UDP协议
            struct tcp_header *udp = (struct tcp_header *)((u_int8_t*)ip + ((ip->ver_ihl & 0xf) * 4));
            printf("UDP: %d -> %d\n", ntohs(udp->sport), ntohs(udp->dport));
        }
    }
}

5.3.3 获取源IP、目标IP、端口号与数据长度

上面的代码已经实现了IP地址和端口的提取。数据长度可以通过 header->len 字段获取。

接下来我们可以在结构化输出中展示这些信息:

时间戳 源IP 源端口 目标IP 目标端口 协议 数据长度
12:34:56 192.168.1.1 5000 8.8.8.8 53 UDP 128
12:34:57 192.168.1.1 8080 192.168.1.2 49231 TCP 1514

5.4 进程与网络流量的关联分析

为了将网络数据包与具体进程关联,我们需要建立“五元组”(源IP、源端口、目标IP、目标端口、协议)与进程PID之间的映射关系。

5.4.1 根据五元组信息匹配进程

Windows系统提供了一个函数 GetExtendedTcpTable() ,可以获取当前TCP连接表,包含每个连接的五元组信息和所属进程ID。

#include <iphlpapi.h>
#include <tcpmib.h>

#pragma comment(lib, "iphlpapi.lib")

void print_tcp_connections() {
    PMIB_TCPTABLE_OWNER_PID tcpTable;
    ULONG size = 0;

    // 获取TCP连接表大小
    GetExtendedTcpTable(NULL, &size, TRUE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);

    tcpTable = (PMIB_TCPTABLE_OWNER_PID)malloc(size);
    if (!tcpTable) return;

    if (GetExtendedTcpTable(tcpTable, &size, TRUE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
        for (DWORD i = 0; i < tcpTable->dwNumEntries; i++) {
            MIB_TCPROW_OWNER_PID row = tcpTable->table[i];
            printf("PID: %u | %s:%d -> %s:%d | State: %d\n",
                   row.dwOwningPid,
                   inet_ntoa(*(struct in_addr*)&row.dwLocalAddr),
                   ntohs((u_short)row.dwLocalPort),
                   inet_ntoa(*(struct in_addr*)&row.dwRemoteAddr),
                   ntohs((u_short)row.dwRemotePort),
                   row.dwState);
        }
    }

    free(tcpTable);
}

该函数可以输出所有TCP连接的PID、本地/远程地址和端口等信息。

5.4.2 利用GetExtendedTcpTable函数获取连接表

此函数支持多种连接类型(TCP、UDP),也可以通过 OpenProcess 获取进程名称,从而实现流量与进程的关联。

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (hProcess != NULL) {
    char procName[MAX_PATH];
    if (GetModuleFileNameEx(hProcess, NULL, procName, MAX_PATH) > 0) {
        printf("Process Name: %s\n", procName);
    }
    CloseHandle(hProcess);
}

5.4.3 实现上行/下载速度的统计与显示逻辑

结合前面捕获的数据包和进程信息,我们可以维护一个进程级别的流量统计表:

PID 进程名 上行流量 下行流量 活跃连接数
1234 chrome.exe 2.3MB 5.1MB 12
5678 svchost.exe 100KB 200KB 3

通过定时更新数据包捕获与连接表信息,可以实现实时监控功能,支持:

  • 按进程排序
  • 设置流量阈值告警
  • 导出历史流量数据

本章内容通过捕获数据包、解析协议头、获取连接表和进程信息,实现了从原始网络数据到进程级别的映射分析。下一章将深入探讨如何将这些数据可视化,并构建一个完整的进程监控系统。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍了如何开发一个系统工具,用于获取电脑进程列表并实时显示每个进程的CPU使用率、内存占用、上行和下载速度。通过使用Windows API和WinPCAP库,开发者可以深入掌握进程监控与网络流量分析技术,并实现通过PID结束进程的功能。该工具适用于系统管理、性能优化及网络监控软件的开发实践。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值