In Chapter 2, we mentioned that most processes in the system are in the sleep state. So, how does the kernel make each process run on the CPU when there are multiple executable processes in the system? In this chapter, we will discuss the Linux kernel's process scheduler (hereinafter referred to as the scheduler), which is responsible for allocating CPU resources to processes.
In books about computer science, the scheduler is explained as follows:
- Only one process can run at a time on a single logical CPU
- It allows multiple executable processes to use the CPU in turn, in units called "time slices"
For example, if there are three processes, p0, p1, and p2, it would look like the following figure.
Let's confirm whether this is actually the case in Linux by conducting an experiment.
Elapsed Time and CPU Time
To understand the content of this chapter, it is essential to understand the concepts of elapsed time and CPU time related to processes. This section explains these times. Their definitions are as follows:
- Elapsed Time: The time that passes from the start to the end of a process. It is like the time measured with a stopwatch from the start to the end of a process.
- CPU Time: The time that a process actually uses a logical CPU.
These terms probably won't make much sense just by looking at the explanation, so let's understand them through an experiment. If you run a process using the time command, you can get the elapsed time and CPU time from the start to the end of the target process. For example, let's run the following "load.py" which terminates after consuming some CPU resources.
#!/usr/bin/python3
# This parameter is to change the amount of CPU time used by this program. It is convenient to change this parameter to make the CPU time consumption several seconds.
NLOOP=100000000
for _ in range(NLOOP):
pass
The result is as follows.
$ time ./load
real 0m2.357s
user 0m2.357s
sys 0m0.000s
The output includes three lines starting with real, user, and sys. Among these, real indicates the elapsed time, and user and sys indicate the CPU time. user refers to the time when the process was running. In contrast, sys refers to the time when the kernel was operating as a result of system calls issued by the process. The load program continues to use the CPU from the start to the end of its execution and does not issue any system calls during that time, so real and user are almost the same, and sys is almost zero. The reason why it is "almost" is because the Python interpreter calls a few system calls at the beginning and end of the process.
Let's also run an experiment with the sleep command, which sleeps most of the time.
$ time sleep 3
real 0m3.009s
user 0m0.002s
sys 0m0.000s
Since it terminates after sleeping for 3 seconds, "real" is nearly 3 seconds. On the other hand, this command gives up the small mount of CPU time and goes to sleep right after starting, and when it begins to use only a little CPU time again 3 seconds later before terminating. So "user" and "sys" are almost 0. The differences of two values are shown in the following figure.
When Using Only One Logical CPU
To simplify the discussion, let's first consider the case of a single logical CPU. The "multiload.py" program will be used for the experiment. This program performs the following actions:
Usage: ./multiload [-m] <number of processes>
Executes a specified number of load processing processes for a set period of time and waits for all to finish.
The time it took to execute each process is outputted.
By default, all processes run only on a single logical CPU.
Meaning of the option:
-m: Allows each process to run on multiple CPUs.
Here is its source code.
#!/bin/bash
MULTICPU=0
PROGNAME=$0
SCRIPT_DIR=$(cd $(dirname $0) && pwd)
usage() {
exec >&2
echo Usage: ./multiload [-m] <number of processes>
Executes a specified number of load processing processes for a set period of time and waits for all to finish.
The time it took to execute each process is outputted.
By default, all processes run only on a single logical CPU.
Meaning of the option:
-m: Allows each process to run on multiple CPUs."
exit 1
}
while getopts "m" OPT ; do
case $OPT in
m)
MULTICPU=1
;;
\?)
usage
;;
esac
done
shift $((OPTIND - 1))
if [ $# -lt 1 ] ; then
usage
fi
CONCURRENCY=$1
if [ $MULTICPU -eq 0 ] ; then
# Pin the "load.py" program to CPU0
taskset -p -c 0 $$ >/dev/null
fi
for ((i=0;i<CONCURRENCY;i++)) do
time "${SCRIPT_DIR}/load.py" &
done
for ((i=0;i<CONCURRENCY;i++)) do
wait
done
Let's first set "" to 1 and run it. This is almost the same as running the load program alone.
$ ./multiload 1
real 0m2.359s
user 0m2.358s
sys 0m0.000s
In my environment, the elapsed time was 2.359 seconds. What about when the parallelism is 2 and 3?
$ ./multiload 2
real 0m4.730s
user 0m2.360s
sys 0m0.004s
real 0m4.739s
user 0m2.374s
sys 0m0.000s
$ ./multiload 3
real 0m7.095s
user 0m2.360s
sys 0m0.004s
real 0m7.374s
user 0m2.499s
sys 0m0.000s
real 0m7.541s
user 0m2.676s
sys 0m0.000s
As the level of parallelism increased by 2 times, 3 times, the CPU time did not change much, but the elapsed time increased by about 2 times, 3 times. This is because, as mentioned at the beginning, only one process can run at the same time on one logical CPU, and the scheduler gives CPU resources to each process in turn.
When Using Multiple Logical CPUs
Next, let's also take a look at the case of multiple logical CPUs. When the multiload program is run with the "-m" option, the scheduler tries to distribute the multiple load processes evenly across all logical CPUs. As a result, for instance, if there are two logical CPUs and two load processes, as shown in the following figure, the two load processes can each monopolize the resources of a logical CPU.
The logic of load balancing is very complex, so this book avoids detailed explanation.
Let's actually verify this. The results of running the multiload program with the -m option and parallelism from 1 to 3 are shown below.
$ ./multiload -m 1
real 0m2.361s
user 0m2.361s
sys 0m0.000s
$ ./multiload -m 2
real 0m2.482s
user 0m2.482s
sys 0m0.000s
real 0m2.870s
user 0m2.870s
sys 0m0.000s
$ ./multiload -m 3
real 0m2.694s
user 0m2.693s
sys 0m0.000s
real 0m2.857s
user 0m2.853s
sys 0m0.004s
real 0m2.936s
user 0m2.935s
sys 0m0.000s
For all processes, the values of real and user+sys were almost the same. In other words, we can see that each process was able to monopolize the resources of a logical CPU.
For all processes, the values of real and user+sys were almost the same. In other words, we can see that each process was able to monopolize the resources of a logical CPU.
Cases where "user + sys" is larger than "real"
Intuitively, you might think that "real >= user + sys" will always hold, but in reality, there can be cases where the value of "user + sys" is slightly larger than that of "real". This is due to the different methods used to measure each time and the fact that the precision of the measurement is not that high. There's no need to worry too much about this; it's enough to be aware that such things can happen.
Furthermore, there are cases where "user + sys" can be significantly larger than "real". For example, this occurs when you run the "multiload.py" program with the "-m" option and set the number of processes to 2 or more. Now, let's try running "./multiload -m 2" through the time command.
$ time ./multiload -m 2
real 0m2.510s
user 0m2.502s
sys 0m0.008s
real 0m2.725s
user 0m2.716s
sys 0m0.008s
real 0m2.728s
user 0m5.222s
sys 0m0.016s
The first and second entries are data about the load processing processes of the "multiload.py" program. The third entry is data about the "multiload.py" program itself. As you can see, the value of user is about twice that of "real". In fact, the "user" and "sys" values obtained by the "time" command are the sums of the values for the target process and its terminated child processes. Therefore, if a process generates child processes and they each run on a different logical CPU, the value of "user+sys" could be greater than real. The "multiload.py" program fits exactly this condition.
NOTE
This article is based on my book written in Japanese. Please contact me via satoru.takeuchi@gmail.com if you're interested in publishing this book's English version.
Top comments (0)