Bootstrap

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

前言

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

依赖

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

winapi = { version = "0.3.9", features = ["processthreadsapi"] }

实现

接口函数定义

读取进程CPU利用率和读取系统的一样,需要两次获取并根据间隔时间进行计算,读取进程的数据需要调用 GetProcessTimes 函数,下面是此函数在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是自定义的,详情见上一篇文章:系统错误代码转为可读文本信息

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 的相关代码放在这个结构体的实现中

解析时间

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

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 也是这么做的,有兴趣的可以看看源码实现

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

测试

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

/// 获取进程句柄
///
/// - 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秒后再读取进程的启动时间和退出时间
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
;