【Rust调用Windows API】读取进程启动时间、退出时间、CPU利用率

前言

上一篇文章 讲了读取系统的CPU利用率,本篇讲如何获取进程的CPU利用率,以及进程启动和退出的时间。

依赖

需要用到processthreadsapi.h的函数,在 Cargo.toml 添加相应的feature

shell 复制代码
winapi = { version = "0.3.9", features = ["processthreadsapi"] }

实现

接口函数定义

读取进程CPU利用率和读取系统的一样,需要两次获取并根据间隔时间进行计算,读取进程的数据需要调用 GetProcessTimes 函数,下面是此函数在Rust中的定义:

rust 复制代码
pub fn GetProcessTimes(
    hProcess: HANDLE,
    lpCreationTime: LPFILETIME,
    lpExitTime: LPFILETIME,
    lpKernelTime: LPFILETIME,
    lpUserTime: LPFILETIME,
) -> BOOL;

函数的几个参数就是输入和输出,可以理解为输入一个进程句柄,输出这个进程的若干信息,参数释义:

  • hProcess :线程的句柄,可通过 OpenProcess获得,此句柄至少需要 PROCESS_QUERY_LIMITED_INFORMATION权限,为了更好的兼容性,最好选择 ** PROCESS_QUERY_INFORMATION**权限
  • lpCreationTime :指向 FILETIME 的指针,用于接收进程创建的时间
  • lpExitTime :指向 FILETIME 的指针,用于接收进程退出的时间,如果进程未退出,这个指针指向的地址不会填充数据
  • lpKernelTime :指向 FILETIME 的指针,用于接收该进程在内核模式下的执行时间
  • lpUserTime :指向 FILETIME 的指针,用于接收该进程在用户模式下的执行时间

这里需要注意一下lpCreationTimelpExitTime 所对应的 FILETIME 表示的是从 UTC+0时区下1601年1月1日零点到对应行为点的间隔百纳秒数。

你可以把它理解成"时间戳",只是这个"时间戳"和我们平时所用的时间戳有两点不同:

  1. 它是从 1601年1月1日零点开始计算,而我们平时用的以Linux Epoch为基准的时间戳是从 1970年1月1日零点开始计算。
  2. 它的单位既不是秒也不是毫秒,而是百纳秒,顾名思义就是 100纳秒,100纳秒 = 1百纳秒

lpKernelTimelpUserTime就与上一篇讲的 读取系统CPU利用率 里读取到的参数一样了,它描述的是间隔的时间量,单位也是百纳秒。

接口调用

下面是接口调用的示例代码(Result和WinError是自定义的,详情见上一篇文章:系统错误代码转为可读文本信息

rust 复制代码
use winapi::shared::minwindef::FILETIME;
use winapi::um::processthreadsapi::GetProcessTimes;
use winapi::um::errhandlingapi::GetLastError;

pub struct ProcessTimes {
    creation: FILETIME,
    exit: FILETIME,
    kernel: FILETIME,
    user: FILETIME,
}

pub fn get_process_times(process_handle: HANDLE) -> error::Result<ProcessTimes> {
    let mut creation: FILETIME = unsafe { std::mem::zeroed() };
    let mut exit: FILETIME = unsafe { std::mem::zeroed() };
    let mut kernel: FILETIME = unsafe { std::mem::zeroed() };
    let mut user: FILETIME = unsafe { std::mem::zeroed() };
    unsafe {
        //  https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes
        let res = GetProcessTimes(
            process_handle,
            &mut creation as *mut FILETIME,
            &mut exit as *mut FILETIME,
            &mut kernel as *mut FILETIME,
            &mut user as *mut FILETIME,
        );

        if res == 0 {
            return Err(WinError::with_code(GetLastError()));
        }
    };

    Ok(ProcessTimes::new(creation, exit, kernel, user))
}

这个函数得到的是我们定义好的结构体 ProcessTimes,接下来把解析 FILETIME 的相关代码放在这个结构体的实现中

解析时间

上一篇介绍的几个工具函数这里再贴一下,这里也会用到

rust 复制代码
use std::time::Duration;
use winapi::shared::minwindef::FILETIME;

/// 将C中的 FILETIME 转为64位数字,单位为百纳秒
pub fn parse_filetime(filetime: FILETIME) -> u64 {
    u64::wrapping_shl(filetime.dwHighDateTime as u64, 32) | filetime.dwLowDateTime as u64
}

/// 解析 FILETIME 为 Duration,精度为纳秒
pub fn parse_filetime_duration(filetime: FILETIME) -> Duration {
    Duration::from_nanos(parse_filetime(filetime) * 100)
}

/// 计算两个 FILETIME 时间间隔,单位百纳秒
pub fn calculate_time_gap(start: FILETIME, end: FILETIME) -> i64 {
    let a = parse_filetime(start);
    let b = parse_filetime(end);

    (b - a) as i64
}

时间转换的逻辑这里简单讲一下思路,剩下的就看代码吧~ 在计算时先把百纳秒转为纳秒(这个百纳秒实在干扰我们思路,先干掉它),然后把这个所谓的"时间戳"转换为真正的Linux Epoch时间戳,1601年到1970年相差 369 年,只需要减掉这369年的纳秒数就可以了。

在windows开发环境下,你会发现 rust-std 标准库里的 SystemTime 也是这么做的,有兴趣的可以看看源码实现

rust 复制代码
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use winapi::shared::minwindef::FILETIME;

/// 1601年到1970年相差369年,此值为时间戳相差纳秒数描述
const INTERVALS_TO_UNIX_EPOCH_NANOS: u64 = 11644473600_000_000_000;

pub struct ProcessTimes {
    creation: FILETIME,
    exit: FILETIME,
    kernel: FILETIME,
    user: FILETIME,
}

impl ProcessTimes {
    pub fn new(creation: FILETIME, exit: FILETIME, kernel: FILETIME, user: FILETIME) -> Self {
        Self {
            creation,
            exit,
            kernel,
            user,
        }
    }

    /// 获取进程创建时间,返回windows内部的FILETIME结构,以 1601年1月1日0点以来在英国格林威治经过的时间量,单位:百纳秒
    pub fn get_creation(&self) -> FILETIME {
        self.creation
    }

    /// 获取进程创建到当前的时间间隔,格式化为 Duration
    ///
    /// windows api获取到的值是以 **1601年1月1日零点** 为起始时间的时间戳,并且时间戳的单位为:100纳秒
    ///
    /// 现在常用的是以UNIX系统Epoch时间 **1970年1月1日零点** 为起始时间的时间戳
    ///
    /// 帮助理解百纳秒单位:
    ///
    /// - 1秒 = 1_000_000_000 纳秒
    /// - 1秒 = 10_000_000 百纳秒
    /// - 1秒 = 1000 毫秒
    /// - 1毫秒 = 10_000 百纳秒
    /// - 1百纳秒 = 100 纳秒
    pub fn get_creation_duration(&self) -> Duration {
        let creation = parse_filetime(self.creation) * 100 - INTERVALS_TO_UNIX_EPOCH_NANOS;

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos() as u64;

        Duration::from_nanos(now - creation)
    }

    /// 获取进程退出时间。
    ///
    /// 如果进程已退出,返回windows内部的FILETIME结构,以 1601年1月1日0点以来在英国格林威治经过的时间量,单位:百纳秒
    ///
    /// 如果进程未退出,返回 None
    pub fn get_exit(&self) -> Option<FILETIME> {
        if self.exit.dwHighDateTime == 0 && self.exit.dwLowDateTime == 0 {
            return None;
        }
        Some(self.exit)
    }

    /// 获取进程退出到当前的时间间隔,格式化为 Duration
    pub fn get_exit_duration(&self) -> Option<Duration> {
        self.get_exit().map(|filetime| {
            let exit = parse_filetime(filetime) * 100 - INTERVALS_TO_UNIX_EPOCH_NANOS;

            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos() as u64;

            Duration::from_nanos(now - exit)
        })
    }

    /// 获取kernel用时,返回windows内部的FILETIME结构
    pub fn get_kernel(&self) -> FILETIME {
        self.kernel
    }

    /// 获取内核态用时,格式化为 Duration
    pub fn get_kernel_duration(&self) -> Duration {
        parse_filetime_duration(self.kernel)
    }

    /// 获取用户态用时,返回windows内部的FILETIME结构
    pub fn get_user(&self) -> FILETIME {
        self.user
    }

    /// 获取用户态用时,格式化为 Duration
    pub fn get_user_duration(&self) -> Duration {
        parse_filetime_duration(self.user)
    }
}

计算CPU利用率

和获取系统CPU利用率一样需要两次获取一个时间值,然后再计算求这期间的变化值,思路比较简单,这里不单独写代码了,为了一起展示进程启动和结束时间,见后面的测试代码~

测试

获取某个进程的句柄方法在本系列的 杀掉指定进程 一文中有写,这里就直接贴一下代码

rust 复制代码
/// 获取进程句柄
///
/// - pid: 进程描述符,进程ID
/// - access_privilege: 访问权限,见`winapi::um::winnt` 常量,或参阅 [官方文档](https://learn.microsoft.com/zh-cn/windows/win32/procthread/process-security-and-access-rights)
pub fn open_process(pid: DWORD, access_privilege: DWORD) -> error::Result<HANDLE> {
    unsafe {
        // https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
        let handle = OpenProcess(access_privilege, 0, pid);

        if handle.is_null() {
            //  https://learn.microsoft.com/zh-cn/windows/win32/debug/system-error-codes--0-499-
            return Err(WinError::new(format!("Could not open process {}", pid), GetLastError()));
        }
        Ok(handle)
    }
}

///	杀掉指定进程
pub fn kill_process(process_handle: HANDLE) -> error::Result<()> {
    unsafe {
        //  https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess
        let success = TerminateProcess(process_handle, 0);
        if success == 0 {
            return Err(WinError::new("Could not terminate process", GetLastError()));
        }
        Ok(())
    }
}

获取句柄时需要有 PROCESS_QUERY_INFORMATION 权限,这里为了演示退出时间需要杀掉进程,所以还需要 PROCESS_TERMINATE 权限。

测试代码思路:

  1. 用户输入一个PID,尝试获取该进程的句柄
  2. 读取该进程的启动时间
  3. 如果该进程还在运行,循环10次每次间隔1秒打印进程的CPU使用率
  4. sleep 3秒后杀掉该进程
  5. sleep 3秒后再读取进程的启动时间和退出时间
rust 复制代码
pub fn test_process_times() {
    print!("Please enter the process you want to monitor: ");
    stdout().flush().unwrap();

    let mut input = String::new();
    stdin().read_line(&mut input).unwrap();

    let pid = input.trim().parse::<u32>().unwrap();
    let handle = open_process(pid, PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE).unwrap();

    fn print_cpu_usage(handle: HANDLE) {
        let mut count = 0;
        let mut last_get_time = Instant::now();
        let mut last_times = get_process_times(handle).unwrap();


        while count < 10 {
            sleep(Duration::from_secs(1));
            let get_time = Instant::now();
            let cur_times = get_process_times(handle).unwrap();

            let creation = cur_times.get_creation_duration();
            println!("进程已启动:{}", format_duration_to_text(creation, false));

            match cur_times.get_exit_duration() {
                None => {}
                Some(exit) => {
                    println!("进程已退出:{}", format_duration_to_text(exit, false));
                    return
                }
            };

            let total = get_time.duration_since(last_get_time).as_nanos() as f64 / 100.0;
            let kernel = calculate_time_gap(last_times.get_kernel(), cur_times.get_kernel()) as f64;
            let user = calculate_time_gap(last_times.get_user(), cur_times.get_user()) as f64;

            let idle_percent = (total - kernel - user) * 100.0 / total;
            let cpu_percent = (kernel + user) * 100.0 / total;
            let kernel_percent = kernel * 100.0 / total;
            let user_percent = user * 100.0 / total;

            println!("CPU利用率:{:.2}% (Kernel: {:.2}%\tUser:{:.2}%)\tCPU空闲率:{:.2}%", cpu_percent, kernel_percent, user_percent, idle_percent);

            last_get_time = get_time;
            last_times = cur_times;
            count += 1;
        }

        println!()
    }

    print_cpu_usage(handle);

    println!("Sleep for 3 seconds and then kill the process...");
    sleep(Duration::from_secs(3));
    kill_process(handle).unwrap();
    print!("Process Killed");
    sleep(Duration::from_secs(3));

    print_cpu_usage(handle);
}

/// 将 duration 格式化为 `天:时:分:秒` 的格式
pub fn format_duration_to_text(duration: Duration, ignore_zero_prefix: bool) -> String {
    let mut s = String::new();

    let mut secs = duration.as_secs();
    let day = secs / SECONDS_PER_DAY;
    if day != 0 || !ignore_zero_prefix {
        s.push_str(&format!("{}:", day));
    }
    secs = secs % SECONDS_PER_DAY;

    let hour = secs / SECONDS_PER_HOUR;
    if hour != 0 || !ignore_zero_prefix {
        s.push_str(&format!("{}:", hour));
    }
    secs = secs % SECONDS_PER_HOUR;

    let minute = secs / 60;
    if minute != 0 || !ignore_zero_prefix {
        s.push_str(&format!("{}:", minute));
    }
    secs = secs % 60;

    s.push_str(&format!("{}", secs));

    s
}

结果

复制代码
Please enter the process you want to monitor: 5328
进程已启动:0:7:26:10
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:11
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:12
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:13
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:14
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:15
CPU利用率:1.56% (Kernel: 0.00% User:1.56%)     CPU空闲率:98.44%
进程已启动:0:7:26:16
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%
进程已启动:0:7:26:17
CPU利用率:4.64% (Kernel: 0.00% User:4.64%)     CPU空闲率:95.36%
进程已启动:0:7:26:18
CPU利用率:3.12% (Kernel: 0.00% User:3.12%)     CPU空闲率:96.88%
进程已启动:0:7:26:19
CPU利用率:0.00% (Kernel: 0.00% User:0.00%)     CPU空闲率:100.00%

Sleep for 3 seconds and then kill the process...
Process Killed进程已启动:0:7:26:26
进程已退出:0:0:0:4
相关推荐
魔道不误砍柴功3 小时前
《理解 Java 泛型中的通配符:extends 与 super 的使用场景》
java·windows·python
郁大锤7 小时前
Windows 下 Git 入门指南:从安装、配置 SSH 到加速 GitHub 下载
windows·git·ssh
一眼青苔8 小时前
网易云音乐如何修改缓存地址到D盘
windows
青铜弟弟8 小时前
在Windows创建虚拟环境如何在pycharm中配置使用
ide·windows·pycharm
无敌 喵星人11 小时前
ubuntu的各种工具配置
linux·运维·windows
用户0996918801811 小时前
rust 获取 hugging face 模型 chat template
rust
受之以蒙12 小时前
Rust FFI实战指南:跨越语言边界的优雅之道
笔记·rust
Anarkh_Lee12 小时前
Neo4j社区版在win下安装教程(非docker环境)
运维·数据库·windows
柑木13 小时前
Rust-开发必备-性能调优-Benchmark基准测试
rust
Bruce_Liuxiaowei13 小时前
[特殊字符]fsutil命令用法详解
windows·cmd