Obtaining CPU usage by process

Hi there,

I'm working on an app that contains a mini system monitoring utility. I would like to list the top CPU-using processes.

As Quinn “The Eskimo!” has repeatedly cautioned, relying on private frameworks is just begging for maintenance effort in the future. Ideally, I want to go through public headers/frameworks.

I've gone to great lengths to try to find this information myself, and at this point I'm just struggling. I detail my research below.

Any pointers in the right direction would be much appreciated!

Attempts


Libproc


First I looked at libproc. Using proc_pidinfo with PROC_PIDTHREADINFO, I'm able to get each thread of an app, with its associated CPU usage percentage. Summing these, I could get the total for an app.

Unfortunately, this has two downsides:
  1. Listing a table of processes now takes O(proces_count) rather than just O(process_count), and causes way more syscalls to be made

  2. It doesn't work for processes owned by other users. Perhaps running as root could alleviate that, but that would involve making a priviliedged helper akin to the existing sysmond that Activity Monitor.app uses. I'm a little scared of that, because I don't want to put my users at risk.

Sysctl


Using the keys [CTL_KERN, KERN_PROC, KERN_PROC_PID, someProcessID], I'm able to get a kinfo_proc instance. Accessing its .kp_proc.p_pctcpu looked really promising, but that value is always zero.

Digging deeper, I found the kernel code that fills this struct in (fill_user64_externproc). The assignment of p_pctcpu is in a conditional region, relying on the _PROC_HAS_SCHEDINFO_ flag. Disassembling the kernel on my mac, I could confirm that the assignment of that field never happens (thus _PROC_HAS_SCHEDINFO_ wasn't set during compilation, and the value will always stay zero)

Reverse engineering Activity Monitor.app


Activity Monitor.app makes proc_info and sysctl system calls, but from looking at the disassembly, it doesn't look like that's where its CPU figures come from. From what I can tell, it's using private functions from /usr/lib/libsysmon.dylib.

That's a user library which wraps an XPC connection to sysmond (/usr/libexec/sysmond), allowing you to create requests (sysmon_request_create), add specific attributes you want to retrieve (sysmon_request_add_attribute), and then functions to query that data out (sysmon_row_get_value).

Getting the data "striaght from the horses mouth" like this sounds ideal. But unfortunately, the only documentation/usage I can find of sysmond is from bug databases demonstrating a privilege escalation vulnerability lol. There are some partial reverse engineered header files floating around, but they're incomplete, and have the usual fragility/upkeep issues associated with using private APIs.

On one hand, I don't want to depend on a private API, because that takes a lot of time to reverse engineer, keep up with changes, etc. On the other, making my own similar privileged helper would be duplicating effort, and expose a bigger attack surface. Needless to say, I have no confidence in being able to make a safer privileged helper than Apple's engineers lol

Reverse engineering iStat Menus


Looks like they're using proc_pid_rusage .

However, I don't know how to convert the cpu_*_time fields of the resulting struct rusage_info_v4 to compute a "simple" percentage. Even if I came up with some formula that produces plausible looking results, I have no real guarantee it's correct or equivalent to what Activity Monitor shows.
Post not yet marked as solved Up vote post of AMomchilov Down vote post of AMomchilov
4.2k views

Replies

I'm working on an app that contains a mini system monitoring utility

I think that is your error right there. It would be better to develop a regular 'ole app that can run on either iOS or macOS and then you never have to deal with these kinds of problems. I speak from experience here. I have an app that does a whole lot of "system monitoring" and I'm trying to put that thing to bed so I can work on a real app.

But if you insist, I can tell you the following:
Libproc is a nice approach. You can combine that with output from launchctl and get pretty detailed information. Of course, parsing output from launchctl is something else that Apple strongly cautions against. But you are correct that this will list only the user's own processes.

Sysctl: I didn't know about this approach. Thanks for researching it and determining that it doesn't work!

Reverse engineering: Don't try to reverse engineer what either Apple or other 3rd party apps are doing. You aren't Apple and don't have their special entitlements, so there's that. Other 3rd party software can be a royal mess.

You didn't mention one basic trick. Just call system tools like ps, top, or nettop. I use the libproc approach in the Mac App Store. But for my Developer ID version, I just call ps, top, or nettop. They work fine from user space, but not from the sandbox. ps is a good, general-purpose tool for lots of different information. However, you need to use top to get kerneltask usage. There are flags that you can use to have it do just a single iteration and then quit. But it's tricky. To get kerneltask usage, you need exactly two iterations. Plus, you will need to properly handle forking a task with a pseudo-terminal. That's a little tricky. I have found that NSTask is not sufficient. I use the low-level posix_spawn* functions instead. I'm not sure if these tools fall under Apple's warning to never parse tool output. Being part of the BSD layer, they are a little bit more stable than Apple's own tools.

Did I mention how nice it would be if you weren't writing this kind of software? It is like you just open the window and tiny birds fly in and just type out the code on your keyboard using SwiftUI while you drink Mojitos.
Hey, thanks for taking the time to write!

It would be better to develop a
regular 'ole app that can run on either iOS or macOS and then you never
have to deal with these kinds of problems.

Yeah, I know I'm getting myself into tricky waters, but it's a pretty useful utility I'm building for in-house use, that might even have eventual product potential. I've considered parsing CLI tools like ps, but as you know, that cuts off App Store access.

When you said "Libproc is a nice approach", which did you mean, using proc_pidinfo to get CPU info for each thread of each process (summing to get total process usage), or using proc_pid_rusage like iStat does? In the former case, that won't work for other user's processes, and in the latter case, I'm not sure how to convert the data proc_pid_rusage returns into a proper cpu usage percentage.

It is like you just open the window and tiny birds fly in and just type
out the code on your keyboard using SwiftUI while you drink Mojitos.

Lol

When you said "Libproc is a nice approach", which did you mean, using procpidinfo to get CPU info for each thread of each process (summing to get total process usage), or using  procpidrusage like iStat does? In the former case, that won't work for other user's processes, and in the latter case, I'm not sure how to convert the data procpidrusagereturns into a proper cpu usage percentage.

I don't know what iStat does. I can only speak for my own app.

There are a couple of different things going on. In my case, I want to recreate Activity Monitor as much as I can. So, I want to show overall CPU usage (broken down by system, user, and idle), similar CPU usage by core, and individual CPU usage by process. I don't care about individual threads.

I call host
processorinfo and hoststatistics to get the ticks at the start and end of my sample period. I get overall values and values for each core.

I already have a list of processes by calling launchctl. I go through each of these and call procpidinfo to get info for each process. I use the PROCPIDTASKINFO flavour. This returns ticks for system and user. I can take the ending ticks, subtract the starting ticks, and that is the number of ticks over the sample period. I also have the overall values. I can just divide the overall by the core count by the task values to get the percentage per task. (I also get memory usage too.)

In the sandbox, it won't all add up. I have to keep track of all the used ticks. There will be some left over. I represent this amount as one or more "unknown" processes. Sometimes this will be adware running as root (with maybe 80%), sometimes it will be kerneltask making a local snapshot (with maybe 109%), and sometimes it will be kerneltask throttling the cpu (anything over 400%). Most people won't have heavy usage in this area except for these specific cases. You've always got the oddballs running Postgres or something, but don't worry about them.

I haven't used procpidrusage, but now I want to look into that. The values shouldn't be any different than procpidinfo. But it would really be nice if this worked for kerneltask. I'm going to try that right now.

OK. proc_pidrusage is not useful. It does not work for kernel_task and it only works outside of the sandbox.

What was the best approach at the end of the day? (for macOS / iOS + appstore)