Why Does Any of This Matter?
Imagine you are writing a Linux program that opens many files at once, or creates filenames dynamically, or reads environment variables. You have a question: how many files can I actually open at the same time on this machine? How long can a filename be?
The wrong answer is to guess — for example, hardcoding 1024 as the max open files or 255 as the max filename length. On a different Linux kernel config, an embedded system, or a BSD machine, those numbers could be completely wrong. Your program silently misbehaves or crashes.
The right answer is what this tutorial is about: ask the OS itself at compile time or at runtime what the actual limits are. POSIX gives us a clean set of functions and constants to do exactly that.
Part 1 — The Problem With Hard-Coding Limits
Every UNIX/Linux system has internal limits. How many processes can run at once? How big can an argument list be when you call exec()? What is the largest integer you can store in an int? These are not universal — they depend on the hardware, the kernel version, the filesystem, and the compiler.
Here is a concrete problem. Suppose you write code like this:
Limits can vary in two ways:
| Source of Variation | Example | Consequence |
|---|---|---|
| Different UNIX implementations | Max int size on 32-bit vs 64-bit | Same code gives different results on different machines |
| Same OS, different kernel config or runtime | Admin raises OPEN_MAX via ulimit | Binary compiled yesterday now uses wrong limit |
| Same OS, different filesystem | FAT32 max filename = 8.3, ext4 = 255 bytes | File creation fails when crossing filesystem boundary |
The POSIX standard (SUSv3 and later SUSv4) gave us a proper solution: a set of standard constants, header files, and functions so a program can discover limits without guessing.
Part 2 — Two Ways to Get Limit Information
POSIX provides two complementary approaches depending on when the limit is known.
| Approach | When to Use | How | Example |
|---|---|---|---|
| Compile-Time | Limit is fixed by hardware/compiler design | Constants in <limits.h> |
INT_MAX, CHAR_BIT |
| Runtime | Limit can change per kernel/filesystem/config | sysconf(), pathconf(), fpathconf() |
OPEN_MAX, NAME_MAX |
Compile-Time: limits.h and the _POSIX_ minimum values
The header <limits.h> defines two layers of constants for each limit:
- _POSIX_XXX_MAX — the minimum value any conforming implementation must support. Think of it as the floor POSIX guarantees.
- XXX_MAX — the actual value this implementation supports. It will be equal to or larger than the POSIX minimum.
| Constant | What It Means | Mental Model |
|---|---|---|
_POSIX_NAME_MAX = 14 |
Every POSIX system must support filenames of at least 14 bytes | Guaranteed floor — your safe lower bound |
NAME_MAX = 255 (Linux ext4) |
On this specific system, filenames up to 255 bytes work | Actual ceiling on this machine |
FLT_MIN = 1E-37 |
The largest smallest float an impl may use (note: _MIN here = lower limit) | Every system must handle floats at least this small |
Part 3 — Three Categories of Runtime Limits
Not all limits behave the same way at runtime. SUSv3 defines three distinct categories. Understanding which category a limit falls into tells you how to query it and whether to expect it to change.
| Category | Fixed At? | How To Query | Can Be Indeterminate? | Example |
|---|---|---|---|---|
| Runtime Invariant Value | Fixed per implementation | sysconf() |
Yes — may depend on available RAM | MQ_PRIO_MAX |
| Pathname Variable Value | Can vary per filesystem | pathconf() / fpathconf() |
Yes | NAME_MAX, PATH_MAX |
| Runtime Increasable Value | Fixed floor, system may raise it | sysconf() |
No | NGROUPS_MAX |
Category 1: Runtime Invariant Value — MQ_PRIO_MAX Example
Take POSIX message queue priority (MQ_PRIO_MAX). POSIX says every implementation must allow at least 32 priority levels (0 to 31). Linux goes far beyond that — it allows 32,768 priority levels. The actual number is fixed for a given kernel build, but you must query it to know what it is:
Category 2: Pathname Variable Value — NAME_MAX Example
This one is filesystem-dependent. If your program mounts an ext4 partition and a FAT32 USB drive, the max filename length is different on each. You must pass the actual path to pathconf() so the kernel can look at which filesystem that path lives on:
Category 3: Runtime Increasable Value — NGROUPS_MAX Example
NGROUPS_MAX defines how many supplementary groups a process can belong to simultaneously. POSIX guarantees at least 8. Linux default is 65536. The floor is fixed but a running system can increase it. You check with sysconf():
Part 4 — sysconf() Deep Dive
sysconf() is your main tool for querying system-wide limits at runtime. Its signature is simple:
| Return Value | errno After Call | Meaning | What To Do |
|---|---|---|---|
| Positive value | unchanged | Success — this is the actual limit | Use the value |
| -1 | still 0 | Limit is indeterminate (genuinely unknown) | Use a safe fallback or POSIX minimum |
| -1 | EINVAL | The name argument is invalid | Fix the name constant you passed |
The critical thing to notice: -1 has two different meanings. That is why you must set errno = 0 before the call, then check errno after if -1 is returned. Here is a reusable wrapper function you should always use:
Important: sysconf() values can change during a process lifetime (on Linux)
POSIX says sysconf() values should be constant for the life of a process. But Linux has three legitimate exceptions — cases where a process can change its own resource limits using setrlimit(), which then affects what sysconf() reports:
| setrlimit() Resource | Affects sysconf() Name | What Changes |
|---|---|---|
RLIMIT_NOFILE |
_SC_OPEN_MAX |
Max files this process can open |
RLIMIT_NPROC |
_SC_CHILD_MAX |
Per-user process creation limit |
RLIMIT_STACK |
_SC_ARG_MAX |
Size allowed for argv + environ in exec() |
Part 5 — pathconf() and fpathconf()
While sysconf() handles system-wide limits, some limits are filesystem-specific. A filename limit on ext4 is different from one on FAT32 or NFS. For these, POSIX gives us pathconf() and fpathconf().
The Only Difference Between the Two
pathconf() takes a string path. fpathconf() takes an already-open file descriptor. Use fpathconf() when you have already opened the file (avoids a second path resolution), and pathconf() when you have only the path string.
| Do you have an open fd? | ||
|
Practical Example: Allocating the Right Buffer for a Pathname
A very common real-world use case: you need to allocate a buffer large enough to hold the longest possible path on the filesystem where a given directory lives.
How _PC_PIPE_BUF Works — A Subtlety
_PC_PIPE_BUF is interesting because its meaning depends on what type of file you pass:
| File Type Passed | Value Returned |
|---|---|
| A pipe file descriptor | Atomic write limit for that specific pipe |
| A FIFO (named pipe) path | Atomic write limit for that FIFO |
| A directory path | Limit that applies to FIFOs created in that directory |
Part 6 — The Key Limits Reference Table
Here is a combined reference of the most important limits you will encounter in daily Linux/embedded programming, their POSIX minimums, and how to query them.
| Limit Name (limits.h) |
POSIX Min | Linux Typical | Query With | What It Controls |
|---|---|---|---|---|
| ARG_MAX | 4096 bytes | ~2 MB | _SC_ARG_MAX |
Total bytes for argv[] + environ[] passed to exec() |
| OPEN_MAX | 20 | 1024 (soft), 1M+ hard | _SC_OPEN_MAX |
Max simultaneously open file descriptors per process |
| NGROUPS_MAX | 8 | 65536 | _SC_NGROUPS_MAX |
Max supplementary group IDs a process can belong to |
| LOGIN_NAME_MAX | 9 bytes | 256 bytes | _SC_LOGIN_NAME_MAX |
Max login/username length including null terminator |
| RTSIG_MAX | 8 | 32 | _SC_RTSIG_MAX |
Distinct realtime signal numbers available (SIGRTMIN to SIGRTMAX) |
| STREAM_MAX | 8 | 16 (= FOPEN_MAX) | _SC_STREAM_MAX |
Max stdio FILE* streams open simultaneously |
| NAME_MAX | 14 bytes | 255 (ext4) | _PC_NAME_MAX |
Max filename bytes (excludes null terminator) |
| PATH_MAX | 256 bytes | 4096 (ext4) | _PC_PATH_MAX |
Max pathname bytes (includes null terminator) |
| PIPE_BUF | 512 bytes | 4096 bytes | _PC_PIPE_BUF |
Max bytes written atomically to a pipe/FIFO |
NAME_MAX does not include the terminating null byte — so you need NAME_MAX + 1 bytes for a filename buffer. PATH_MAX does include it — so allocating exactly PATH_MAX bytes is safe for a full path buffer. This is a deliberate POSIX inconsistency correction; older standards were ambiguous about PATH_MAX.
Part 7 — The getconf Shell Command
You do not always need a C program to check limits. The getconf command lets you query the same values from the shell. This is useful for shell scripting, quick checks, and debugging.
In a shell script, you might use getconf to safely compute a buffer or loop limit:
Part 8 — System Options (Feature Detection)
Beyond numeric limits, POSIX also defines a set of optional features — things like realtime signals, POSIX threads, job control, shared memory, etc. Not every UNIX/Linux system supports all of them (especially true for embedded systems or stripped-down configs).
POSIX options are advertised through constants in <unistd.h> that begin with _POSIX_ or _XOPEN_. Each constant has a specific value meaning:
| Value | Meaning | What Your Code Must Do |
|---|---|---|
| -1 | Feature is definitely NOT supported | Use #if to exclude code that depends on this feature |
| 0 | Support is uncertain at compile time — check at runtime | Call sysconf(_SC_...) at runtime to confirm |
| > 0 (e.g. 200112L) | Feature is guaranteed supported | Safe to use unconditionally — all APIs are available |
The value 200112L is the year-month the SUSv3 standard was approved (2001 December). SUSv4 uses 200809L.
Important Options You Will Encounter
| Option Constant | sysconf() Name | Feature | Required by SUSv3? |
|---|---|---|---|
_POSIX_JOB_CONTROL |
_SC_JOB_CONTROL |
Shell job control (bg, fg, Ctrl+Z) | Yes (+) |
_POSIX_THREADS |
_SC_THREADS |
POSIX thread support (pthreads) | Optional |
_POSIX_REALTIME_SIGNALS |
_SC_REALTIME_SIGNALS |
sigqueue(), sigwaitinfo() etc. | Optional |
_POSIX_SHARED_MEMORY_OBJECTS |
_SC_SHARED_MEMORY_OBJECTS |
shm_open(), mmap() shared memory | Optional |
_POSIX_SEMAPHORES |
_SC_SEMAPHORES |
sem_open(), sem_wait() etc. | Optional |
_POSIX_SAVED_IDS |
none | Processes have saved set-user-ID/group-ID | Yes (+) |
_POSIX_CHOWN_RESTRICTED |
_PC_CHOWN_RESTRICTED |
Only root can chown files to arbitrary UIDs | Must be defined (non -1) (*) |
_POSIX_MESSAGE_PASSING |
_SC_MESSAGE_PASSING |
POSIX message queues (mq_open etc.) | Optional |
Part 9 — Dealing With Indeterminate Limits
Sometimes both the header constant is missing and sysconf()/pathconf() returns -1 with errno = 0. This means the system genuinely cannot tell you the limit. You have four strategies to handle this:
| # | Strategy | When Good | Risk |
|---|---|---|---|
| 1 | Use the POSIX minimum (_POSIX_XXX_MAX) | Maximum portability required | Very conservative; may limit functionality unnecessarily |
| 2 | Try the operation and handle the error | The operation itself fails cleanly with EAGAIN/ENAMETOOLONG etc. | Adds retry logic complexity |
| 3 | Write a probe function to estimate the limit | When a reasonable “good guess” is acceptable | Guess may still be wrong on exotic systems |
| 4 | Use GNU Autoconf at build time | Cross-platform library or tool distribution | Build system complexity; not suitable for runtime-only decisions |
Strategy 2 in Action — Try and Recover
For realtime signal queuing, the system imposes a limit. When you hit it, sigqueue() returns EAGAIN. Rather than trying to predict the limit, you handle the failure:
Strategy 3 — Probe Function for PATH_MAX
PATH_MAX is the classic case where both the header may be absent and pathconf() may return -1. Here is a robust probe function:
Part 10 — Complete Worked Example: Portable File Lister
Let us tie everything together. Here is a small program that lists files in a directory, properly using system limit queries instead of any hard-coded values. This is the kind of portable code you should write in production:
Part 11 — Summary: The Full Picture
| Question | Answer | Tool to Use | Header |
|---|---|---|---|
| Fixed by architecture/compiler? | Yes (e.g. INT_MAX) | Compile-time constant | <limits.h> |
| System-wide, may vary per kernel? | Yes (OPEN_MAX, NGROUPS_MAX) | sysconf(_SC_...) |
<unistd.h> |
| Varies per filesystem/path? | Yes (NAME_MAX, PATH_MAX) | pathconf(path, _PC_...) |
<unistd.h> |
| Have an open fd, want fs limit? | Yes (fd already open) | fpathconf(fd, _PC_...) |
<unistd.h> |
| Optional POSIX feature present? | Check compile + runtime | #if _POSIX_THREADS > 0 or sysconf(_SC_THREADS) |
<unistd.h> |
| From the shell quickly? | Yes | getconf ARG_MAX |
CLI only |
Five Rules to Remember
- Never hard-code limits — they change across kernels, filesystems, and hardware.
- Set errno = 0 before every sysconf/pathconf call — the only way to tell indeterminate from error.
- Use _PC_* with pathconf, _SC_* with sysconf — wrong prefix = EINVAL.
- NAME_MAX excludes the null byte, PATH_MAX includes it — add 1 to NAME_MAX for your buffer.
- POSIX option constant = 0 means “ask at runtime”; undefined or -1 means not supported.
Keep Learning Linux Systems Programming
This is part of the free Linux Systems Programming course on EmbeddedPathashala. Next up: process resource limits with setrlimit() and getrlimit().
