I was recently looking for a tool which would allow me to limit the total execution time of a process and its children. I haven’t found anything, so I decided to implement such a feature in Process Governor, my open-source process-monitoring application. You may download the v2.3 version from GitHub. In this post, I want to present you the new functionality and describe its implementation details.
When we know the PIDs of our running processes, we could use a simple command to wait for the processes to finish (the
Wait-Process cmdlet is an ideal example) and kill the remaining ones if they pass the limit. However, what if we only know the PID of the initial process? Tracking processes hierarchy in a script could become problematic. A simple and clear solution would be to assign a job object to the initial process, let it create new processes, wait the specified period and terminate the job if any of the processes is still running (terminating the job exits all the processes). There are, however, few questions we need to answer:
- How do we know all processes associated with the job finished their execution?
- What types of process execution time should we measure?
Waiting for processes to finish
According to the Microsoft documentation, the job becomes signaled only when “all of its processes are terminated because the specified end-of-job time limit has been exceeded.” So when we use the job object to limit the process memory, it makes no sense to wait for it as it will never get signaled. I thought of two ways how may implement this:
- Listen on the job completion port for the message with the
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZEROidentifier, meaning there are no active processes in the job
- Periodically call the QueryInformationJobObject function with the
JobObjectBasicAccountingInformationclass. It returns the JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure which holds the number of active processes in the job
The first method is not guaranteed to work as only the notifications for limits are sure to arrive. However, after multiple tests I have never observed this event to be missing. Moreover, after finding that even Raymond Chen suggests this method, I decided I will take the risk 🙂 So if Process Governor hangs for you, with no processes in the created job, please report it.
Limiting the process execution time
We can set the time limit on the process execution time by using:
- The clock time – the period when process was present in the system (not necessarily using CPU)
- The CPU time – the period when the process was using CPU. We can later split this time into the kernel-mode time and the user-mode time.
The Windows job object allows us to set the limit only on the user-mode time (JOBOBJECT_BASIC_LIMIT_INFORMATION structure). We can set the limit for each of the processes in the job or for the whole job (so the process times will sum up). Process Governor supports both those limit with accordingly the –process-utime and the –job-utime parameters. In Windows, you can observe the process user-mode time in, for example, Process Hacker:
or Process Explorer:
It might be an interesting experiment to open notepad and watch how kernel-mode and user-mode time changes. Moving the notepad window around the desktop increments the kernel-mode time, which is not surprising as windows rendering is implemented in the win32k.sys driver. On the other hand, opening a large file makes the user-mode time grow rapidly. When you use the –process-utime or the –job-utime option with the –recursive option, the limit applies to/includes the whole process tree.
Setting the user-mode time limit on either a process or a job might not necessarily be a feature you are looking for so I also implemented the clock time limit (-t or –timeout parameter). As there is no support for this limit in the job object, I periodically check if the timeout hasn’t passed (in between accepting the completion port messages). For instance, to limit the findstr process execution to 10s, you may run:
procgov -t 10s findstr /s test c:\Windows\
When you use the –recursive parameter the timeout is same for the parent process and all its children. I start counting time when the parent process starts or when Process Governor finishes attaching to the parent process. Process Governor exits before the specified timeout only when all the processes associated with the job complete their work. Otherwise, it terminates the job, killing all its processes.
Minor changes and improvements
With version 2.3 of the Process Governor, I also added a few minor changes. On start, it presents the currently chosen limits, for example:
PS me> procgov64 -t 10s -c 2 findstr /s test c:\Windows\ Process Governor v2.3.19031.2 - sets limits on your processes Copyright (C) 2019 Sebastian Solnica (lowleveldesign.org) CPU affinity mask: 0x3 Maximum committed memory (MB): (not set) Process user-time execution limit (ms): (not set) Job user-time execution limit (ms): (not set) Clock-time execution limit (ms): 10 000 Press Ctrl-C to end execution without terminating the process.
The -v option prints verbose message on the console, showing all the notifications received on the completion port from the job object.