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.