1. Introduction
The Linux kernel handles system resources with the help of related limits. One such resource is the memory stack available to each process. We should take this into account, especially when we’re using recursion.
In this tutorial, we’ll learn how to manage this limit. In addition, we’ll study a recursion example in Bash.
2. The ulimit
The ulimit command reports and sets resource limits related to a given shell. We can adjust both soft and hard limits. To obtain the stack size limit, we use the -s option:
$ ulimit -s
8192
Here, 8192 is the Linux default stack size soft limit in kilobytes. Now, let’s make out that there is no hard limit imposed by default:
$ ulimit -s -H
unlimited
To raise the soft cap to around 16 MB, let’s issue:
$ ulimit -S -s 16000
We can adjust the hard limit by employing the -H option. However, the unprivileged user can only lower its value. Finally, note that if we don’t provide either the -H or -S option, both limits will be affected.
3. Changing Process Limits With prlimit
With the prlimit command, we can manage the resource limits per process. To check the stack size limit of a process identified with PID, let’s issue:
$ sudo prlimit --stack --pid=<PID>
Then, to change both hard and soft limits, let’s specify the new value in bytes after the stack switch:
$ sudo prlimit --stack=<value> --pid=<PID>
As an example, let’s set the limits of the process related to the current terminal and all processes originating from therein to around 16 MB:
$ sudo prlimit --stack=1600000 --pid=$$
We use $$ to obtain the PID of the current shell. This trick is useful because we can’t use ulimit with sudo to increase the limit. Let’s check the result:
$ sudo prlimit --stack --pid=$$
RESOURCE DESCRIPTION SOFT HARD UNITS
STACK max stack size 1600000 1600000 bytes
4. The pidstat Command
Let’s use the pidstat command with the -s switch to report the stack size. We’ll check the current shell process, passing its PID with the -p option:
$ pidstat -s -p $$
Linux 6.5.0-35-generic (ubuntu) 16.06.2024 _x86_64_ (4 CPU)
11:30:09 UID PID StkSize StkRef Command
11:30:09 1001 7543 132 112 bash
It covers the period from the last boot. StkSize provides the amount of memory reserved for the process, though not necessarily in use. StkRef tells us the used amount of memory as referenced by the process. Both values are in kilobytes.
Let’s receive a rolling report by passing the interval in seconds. Here we set it to 2 s:
$ pidstat 2 -s -p $$
Linux 6.5.0-35-generic (ubuntu) 16.06.2024 _x86_64_ (4 CPU)
11:38:06 UID PID StkSize StkRef Command
11:38:08 1001 7543 132 112 bash
11:38:10 1001 7543 132 112 bash
11:38:12 1001 7543 132 112 bash
# ...
To restrict the number of readings, let’s add the second argument with the number of records, here set to three:
$ pidstat 2 3 -s -p $$
Linux 6.5.0-35-generic (ubuntu) 16.06.2024 _x86_64_ (4 CPU)
13:38:09 UID PID StkSize StkRef Command
13:38:11 1001 10475 132 112 bash
13:38:13 1001 10475 132 112 bash
13:38:15 1001 10475 132 112 bash
Average: 1001 10475 132 112 bash
Finally, instead of using the PID, we can query with the -C option by the command name. Then, by default, pidstat reports only active processes. We can change this behavior by adding the -p ALL option:
$ pidstat -s -C firefox -p ALL
Linux 6.5.0-35-generic (ubuntu) 16.06.2024 _x86_64_ (4 CPU)
12:13:09 UID PID StkSize StkRef Command
12:13:09 1001 2285 132 128 firefox
5. Recurrence In Bash
Let’s examine a recurrent implementation of the factorial function in the rec_fact script:
#!/bin/bash
result=1
factorial()
{
sleep 0.1
if (( $1 <= 1 )); then
echo $result
else
result=$(( $1 * $result ))
factorial $(( $1 - 1 ))
fi
}
factorial $1
The function returns by echo when the argument falls below one. Additionally, we slowed down the script a bit with the sleep command.
Note that we accumulate the outcome in the global variable result. In this way, we evade taking the function return value, eg previous=$(factorial $(( $1 – 1 ))):
factorial()
{
if (( $1 <= 1 )); then
echo 1
else
previous=$(factorial $(( $1 - 1 )))
echo $(( $1 * previous ))
fi
}
The () operator would create a new subshell and dramatically increase the overall memory usage.
Now let’s start the script with some ridiculously huge number:
$ ./rec_fact 10000
Then, we’ll observe the process stack size with pidstat:
$ pidstat 1 -s -C rec_fact -p ALL
# ...
14:17:07 UID PID StkSize StkRef Command
14:17:08 1001 17338 132 116 rec_fact
# ...
14:23:52 UID PID StkSize StkRef Command
14:23:53 1001 17338 8188 8188 rec_fact
# ...
So, after some time, the process stack size hits a limit of 8 MB, raising the segmentation fault error.
6. Conclusion
In this article, we learned about stack size limits in Linux. For the current shell, we managed them with ulimit. Then, we applied the prlimit command to read and set the limit for a running process. Next, with the pidstat command, we reported the process stack utilization in real-time.
Finally, we analyzed the recurrent implementation of the well-known factorial function in Bash.