ftruncate() & fstat() โ€” Sizing & Inspecting Shared Memory

ftruncate() & fstat() โ€” Sizing & Inspecting Shared Memory
Chapter 54 โ€” Part 3 of 5 | The Linux Programming Interface
0
Initial Size of New Object
st_size
Size Field in stat
SIGBUS
Access Beyond Size

๐Ÿ“š Chapter 54 Series
Part 1: Introduction & Overview Part 2: shm_open() Part 3: ftruncate() & fstat() Part 4: mmap() with POSIX shm Part 5: shm_unlink() & Examples

๐Ÿ”‘ Key Concepts in This Part
ftruncate() fstat() Zero Initialization st_size st_mode st_uid / st_gid SIGBUS fchmod() fchown() Dynamic Resize

Why Do You Need ftruncate()?

When shm_open() creates a brand new shared memory object, that object has zero length. You cannot map zero bytes, and even if you could, there would be nothing useful to work with.

You must call ftruncate() to set the object to your desired size before calling mmap(). Think of it as “allocating” the shared memory region.

After shm_open()
0 bytes
Not usable yet
โ†’
ftruncate(fd, N)
N bytes
Zero-initialized
โ†’
mmap() now OK
Ready
Can read/write

๐Ÿ“‹ ftruncate() โ€” Function Signature
#include <unistd.h>

int ftruncate(int fd, off_t length);
/* Returns: 0 on success, -1 on error */

fd is the file descriptor from shm_open(). length is the desired size in bytes.

Growing the object

If length is larger than the current size, the object is extended. New bytes are automatically initialized to zero. No garbage data.

Shrinking the object

If length is smaller than the current size, the object is truncated. Any process that still has a mapping beyond the new end will receive SIGBUS if it accesses the truncated region.

๐Ÿ’ป Code Example: ftruncate() โ€” Create, Size, Grow, Shrink
/*
 * shm_ftruncate.c
 * Demonstrates ftruncate() on a POSIX shared memory object:
 *   - Initial creation (size 0)
 *   - Setting initial size
 *   - Growing the object
 *   - Observing zero-initialization
 *
 * Compile: gcc shm_ftruncate.c -o shm_ftruncate -lrt
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_NAME "/ep_truncate_demo"

int main(void)
{
    int fd;
    struct stat sb;
    char *addr;

    /* Step 1: Create โ€” starts at size 0 */
    fd = shm_open(SHM_NAME, O_CREAT | O_EXCL | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }

    /* Verify size is 0 immediately after creation */
    if (fstat(fd, &sb) == -1) { perror("fstat"); exit(EXIT_FAILURE); }
    printf("After shm_open(): size = %lld bytes\n", (long long)sb.st_size);

    /* Step 2: Set size to 1024 bytes */
    if (ftruncate(fd, 1024) == -1) { perror("ftruncate"); exit(EXIT_FAILURE); }
    fstat(fd, &sb);
    printf("After ftruncate(1024): size = %lld bytes\n", (long long)sb.st_size);

    /* Step 3: Map it and verify zero-initialization */
    addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    /* Check first 8 bytes are zero */
    int all_zero = 1;
    for (int i = 0; i < 8; i++) {
        if (addr[i] != 0) { all_zero = 0; break; }
    }
    printf("First 8 bytes zero-initialized: %s\n", all_zero ? "YES" : "NO");

    /* Write some data */
    strcpy(addr, "EmbeddedPathashala");

    /* Step 4: Grow the object to 2048 bytes */
    if (ftruncate(fd, 2048) == -1) { perror("ftruncate grow"); exit(EXIT_FAILURE); }
    fstat(fd, &sb);
    printf("After ftruncate(2048): size = %lld bytes\n", (long long)sb.st_size);

    /* The new region (bytes 1024โ€“2047) is also zero */
    printf("Byte at offset 1024 (new region): %d\n", (unsigned char)addr[1024]);

    /* Old data still intact after grow */
    printf("Data after grow: %s\n", addr);

    /* Cleanup */
    munmap(addr, 2048);
    close(fd);
    shm_unlink(SHM_NAME);
    return 0;
}
Expected output:

After shm_open(): size = 0 bytes
After ftruncate(1024): size = 1024 bytes
First 8 bytes zero-initialized: YES
After ftruncate(2048): size = 2048 bytes
Byte at offset 1024 (new region): 0
Data after grow: EmbeddedPathashala

๐Ÿ“Š fstat() โ€” Inspecting a Shared Memory Object

You can call fstat() on the file descriptor from shm_open() to query metadata about the shared memory object. POSIX requires only certain fields to be meaningful; on Linux most fields work as you would expect for a file.

#include <sys/stat.h>

int fstat(int fd, struct stat *statbuf);
/* Returns: 0 on success, -1 on error */

/* Relevant fields in struct stat for shared memory: */
struct stat {
    off_t   st_size;   /* Current size of the shared memory object in bytes */
    mode_t  st_mode;   /* Permissions (e.g., 0600) โ€” changeable with fchmod() */
    uid_t   st_uid;    /* Owner user ID                                        */
    gid_t   st_gid;    /* Owner group ID                                       */
    /* Linux also fills in time fields and other info */
};
SUSv3 Note: The standard only requires fstat() to fill in st_size, st_mode, st_uid, and st_gid for shared memory objects. Linux fills in more fields (timestamps, etc.) but do not depend on those in portable code.

๐Ÿ’ป Code Example: fstat() โ€” Inspect Shared Memory Metadata
/*
 * shm_fstat.c
 * Open an existing shared memory object and print its metadata.
 * Compile: gcc shm_fstat.c -o shm_fstat -lrt
 * Run after creating a shm object with shm_creator (Part 2 example).
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>

#define SHM_NAME "/ep_inspect"

int main(void)
{
    int fd;
    struct stat sb;

    /* Create a 4096-byte object to inspect */
    fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0640);
    if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }
    if (ftruncate(fd, 4096) == -1) { perror("ftruncate"); exit(EXIT_FAILURE); }

    /* Now inspect it with fstat */
    if (fstat(fd, &sb) == -1) { perror("fstat"); exit(EXIT_FAILURE); }

    printf("=== POSIX Shared Memory Object Info ===\n");
    printf("  Name           : %s\n", SHM_NAME);
    printf("  Linux path     : /dev/shm%s\n", SHM_NAME);
    printf("  Size           : %lld bytes\n", (long long)sb.st_size);
    printf("  Permissions    : %o (octal)\n", sb.st_mode & 0777);
    printf("  Owner UID      : %d\n", (int)sb.st_uid);
    printf("  Owner GID      : %d\n", (int)sb.st_gid);

    /* Check if owner-readable/writable */
    printf("  Owner can read : %s\n",
           (sb.st_mode & S_IRUSR) ? "yes" : "no");
    printf("  Owner can write: %s\n",
           (sb.st_mode & S_IWUSR) ? "yes" : "no");

    /* Change permissions with fchmod */
    if (fchmod(fd, 0600) == -1) {
        perror("fchmod");
    } else {
        printf("  fchmod(0600) applied.\n");
        fstat(fd, &sb);
        printf("  New permissions: %o\n", sb.st_mode & 0777);
    }

    close(fd);
    shm_unlink(SHM_NAME);
    return 0;
}
Expected output:

=== POSIX Shared Memory Object Info ===
  Name           : /ep_inspect
  Linux path     : /dev/shm/ep_inspect
  Size           : 4096 bytes
  Permissions    : 640 (octal)
  Owner UID      : 1000
  Owner GID      : 1000
  Owner can read : yes
  Owner can write: yes
  fchmod(0600) applied.
  New permissions: 600

๐Ÿ”„ Dynamic Resize Pattern

In some designs, the creator grows the shared memory object over time as more data is needed. Here is the safe pattern for doing that:

/*
 * Safe pattern for growing a shared memory object after initial mmap().
 *
 * Key rules:
 *   1. Call ftruncate() to grow the object BEFORE accessing the new region.
 *   2. If you already have a mmap() mapping, you must either:
 *      a. munmap() the old region and mmap() again with the new size, OR
 *      b. Use mremap() (Linux-specific) to extend the existing mapping.
 *   3. Never access bytes beyond the current st_size โ€” that causes SIGBUS.
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_NAME "/ep_dynamic"
#define INITIAL_SIZE 1024
#define GROWN_SIZE   4096

int main(void)
{
    int fd;
    char *addr;

    fd = shm_open(SHM_NAME, O_CREAT | O_EXCL | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }

    /* Set initial size and map */
    ftruncate(fd, INITIAL_SIZE);
    addr = mmap(NULL, INITIAL_SIZE, PROT_READ | PROT_WRITE,
                MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    addr[0] = 'A';  /* Use initial region */
    printf("Initial write OK. addr[0] = %c\n", addr[0]);

    /* Now grow the object */
    ftruncate(fd, GROWN_SIZE);

    /* munmap old region and remap with new size */
    munmap(addr, INITIAL_SIZE);
    addr = mmap(NULL, GROWN_SIZE, PROT_READ | PROT_WRITE,
                MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap (grown)"); exit(EXIT_FAILURE); }

    /* Old data preserved, new region is zero */
    printf("After grow: addr[0] = %c (preserved)\n", addr[0]);
    printf("After grow: addr[2048] = %d (zero)\n", (unsigned char)addr[2048]);

    munmap(addr, GROWN_SIZE);
    close(fd);
    shm_unlink(SHM_NAME);
    return 0;
}

โ˜ ๏ธ SIGBUS โ€” The Danger of Accessing Beyond Object Size

If you map N bytes but then shrink the underlying object with ftruncate() to fewer than N bytes, any access to the “orphaned” pages beyond the new end causes a SIGBUS signal. By default this kills the process.

/* Memory layout after shrink */
Bytes 0โ€“511
Valid (in object)
Bytes 512โ€“1023
SIGBUS zone
/* ftruncate(fd, 512) was called after mmap(NULL, 1024, …) */
addr[600] = ‘X’; /* SIGBUS! Process killed */
Best Practice: Always munmap() before shrinking the object, or ensure all processes using the mapping are aware of the new boundary. Use fstat() to query the current size before accessing.

๐ŸŽฏ Interview Questions โ€” ftruncate() & fstat()
Q1. Why must ftruncate() be called after shm_open() for a new object?
Answer: A newly created shared memory object has a size of zero bytes. mmap() cannot create a useful mapping of a zero-length object. ftruncate() sets the size to the desired number of bytes before mmap() is called.
Q2. What happens to newly added bytes when you grow a shared memory object with ftruncate()?
Answer: They are automatically initialized to zero. This is guaranteed by POSIX โ€” you never get garbage data in the extended region.
Q3. What signal is sent when a process accesses memory beyond the end of a shared memory object?
Answer: SIGBUS (bus error). This happens if you map N bytes but the underlying object is shrunk to fewer than N bytes by ftruncate(), and then a process accesses the now-invalid region.
Q4. Which fields does SUSv3 require fstat() to return for a shared memory object?
Answer: SUSv3 only mandates st_size (current size), st_mode (permissions), st_uid (owner user ID), and st_gid (owner group ID). Linux fills in additional fields (timestamps, etc.) but these are non-portable.
Q5. How do you change the permissions of an existing POSIX shared memory object?
Answer: Use fchmod(fd, new_mode) with the file descriptor from shm_open(). Similarly, fchown(fd, uid, gid) changes ownership.
Q6. After calling ftruncate() to grow a shared memory object, does the existing mmap() mapping automatically reflect the new size?
Answer: No, not automatically in the portable sense. The mmap() call specifies a fixed length. To access the new region, you must either munmap() and call mmap() again with the larger size, or use the Linux-specific mremap() to extend the existing mapping.
Q7. Is it safe to call ftruncate() while other processes have the object mapped?
Answer: It depends on the direction. Growing is generally safe โ€” other processes simply cannot access the new region without remapping. Shrinking is dangerous โ€” any process that accesses bytes beyond the new end will receive SIGBUS. Coordinate shrinks carefully using synchronization.

Continue the Series
Next: mmap() with POSIX shared memory โ€” MAP_SHARED, protection flags, and mappings

โ† Part 2: shm_open() Part 4: mmap() โ†’

Leave a Reply

Your email address will not be published. Required fields are marked *