Project 22: Signal-Safe Printf (Async-Signal-Safe Logging)

Build a tiny printf-like facility that is safe to call from a signal handler by using only async-signal-safe operations, mastering the critical constraints of signal handler context.


Quick Reference

Attribute Value
Language C (alt: Rust)
Difficulty Advanced
Time Weekend
Chapters 8, 12
Coolness High (Handler Safety)
Portfolio Value Systems Programming

Learning Objectives

By completing this project, you will:

  1. Master async-signal-safety concepts: Understand exactly which functions can be safely called from signal handlers and why the list is so restricted
  2. Understand reentrancy vs thread-safety: Distinguish between these related but distinct concepts and see why reentrancy is the stricter requirement
  3. Implement signal-safe output: Build formatting functions using only write(2) and manual conversion, avoiding all stdio and allocation
  4. Preserve errno correctly: Learn why signal handlers must save and restore errno to avoid corrupting main program state
  5. Analyze deadlock scenarios: Trace through exactly how calling printf() from a handler leads to deadlock when the main thread holds the stdio lock
  6. Design lock-free algorithms: Understand why mutexes in signal handlers cause deadlocks and explore alternative synchronization patterns
  7. Handle partial writes robustly: Implement retry logic for write(2) when interrupted by signals or when buffers are full
  8. Test async-signal-safety: Design stress tests that expose handler bugs through rapid signal delivery
  9. Apply knowledge to production code: Use your safe logger in shell, proxy, and other signal-handling projects
  10. Debug subtle concurrency bugs: Develop intuition for the kinds of bugs that only appear under heavy signal load

Deep Theoretical Foundation

What is Async-Signal-Safety?

A function is async-signal-safe if it can be safely called from within a signal handler, even if the signal interrupted the main program in the middle of calling the same function (or any other non-async-signal-safe function).

+------------------------------------------------------------------------+
|                    THE SIGNAL HANDLER CONTEXT                           |
+------------------------------------------------------------------------+
|                                                                         |
|  NORMAL EXECUTION:                                                      |
|  ----------------                                                       |
|                                                                         |
|    main()                                                               |
|      |                                                                  |
|      v                                                                  |
|    printf("Hello")   <---- Signal arrives HERE, in the middle of printf |
|      |                                                                  |
|      v                     +---------------------------+               |
|    ... continue ...        | Signal Handler            |               |
|                            |   printf("Interrupted!"); | <-- DEADLOCK! |
|                            +---------------------------+               |
|                                                                         |
|  WHY DEADLOCK OCCURS:                                                  |
|  -------------------                                                   |
|                                                                         |
|    printf() internally:                                                |
|    1. Acquires stdio lock                                              |
|    2. Formats string                                                   |
|    3. Writes to buffer/file                                            |
|    4. Releases lock                                                    |
|                                                                         |
|    If signal arrives between steps 1 and 4:                            |
|    - Main thread holds lock                                            |
|    - Handler calls printf()                                            |
|    - printf() tries to acquire lock                                    |
|    - Lock already held by main thread                                  |
|    - Handler blocks waiting for lock                                   |
|    - Main thread can't run (waiting for handler to return)            |
|    - DEADLOCK: Both waiting forever                                    |
|                                                                         |
+------------------------------------------------------------------------+

The POSIX Async-Signal-Safe Function List

POSIX defines exactly which functions are safe to call from signal handlers. The list is surprisingly short:

+------------------------------------------------------------------------+
|                    ASYNC-SIGNAL-SAFE FUNCTIONS                          |
+------------------------------------------------------------------------+
|                                                                         |
|  SYSTEM CALLS (kernel operations, inherently safe):                    |
|  ------------------------------------------------                      |
|  _exit()      close()      dup2()       fstat()                       |
|  getpid()     kill()       open()       read()                        |
|  sigaction()  signal()     stat()       write()                       |
|  fork()       pipe()       wait()       waitpid()                     |
|  alarm()      pause()      sleep()      nanosleep()                   |
|                                                                         |
|  SIGNAL OPERATIONS:                                                    |
|  -----------------                                                     |
|  sigaddset()    sigdelset()    sigemptyset()   sigfillset()          |
|  sigismember()  sigpending()   sigprocmask()   sigsuspend()          |
|  raise()                                                               |
|                                                                         |
|  OTHER SAFE FUNCTIONS:                                                 |
|  --------------------                                                  |
|  abort()      access()     chdir()      chmod()                       |
|  chown()      creat()      execle()     execve()                      |
|  fcntl()      fdatasync()  fsync()      getegid()                     |
|  geteuid()    getgid()     getgroups()  getpgrp()                     |
|  getppid()    getuid()     link()       lseek()                       |
|  mkdir()      mkfifo()     rename()     rmdir()                       |
|  setgid()     setpgid()    setsid()     setuid()                      |
|  umask()      uname()      unlink()     utime()                       |
|                                                                         |
|  NOTE: strlen() is NOT in this list!                                   |
|        (Though often safe in practice, not guaranteed)                 |
|                                                                         |
+------------------------------------------------------------------------+
|                                                                         |
|  UNSAFE FUNCTIONS (NEVER call from signal handlers):                   |
|  --------------------------------------------------                    |
|                                                                         |
|  STDIO (use internal locks and buffers):                               |
|    printf()    fprintf()    sprintf()    snprintf()                   |
|    puts()      fputs()      fgets()      scanf()                      |
|    fread()     fwrite()     fflush()     fopen()                      |
|    fclose()    perror()                                                |
|                                                                         |
|  MEMORY ALLOCATION (use internal locks and global state):             |
|    malloc()    calloc()     realloc()    free()                       |
|    new         delete       (C++)                                      |
|                                                                         |
|  STRING FUNCTIONS (may use static buffers or locks):                  |
|    strtok()    localtime()  ctime()      asctime()                    |
|    gethostbyname()  getpwnam()  getservbyname()                       |
|                                                                         |
|  THREADING (can deadlock):                                             |
|    pthread_mutex_lock()     pthread_cond_wait()                       |
|    sem_wait()               pthread_cancel()                           |
|                                                                         |
+------------------------------------------------------------------------+

Reentrancy vs Thread-Safety

These concepts are related but distinct:

+------------------------------------------------------------------------+
|               REENTRANCY VS THREAD-SAFETY                               |
+------------------------------------------------------------------------+
|                                                                         |
|  THREAD-SAFE:                                                          |
|  -----------                                                           |
|  Can be called safely from multiple threads concurrently.              |
|  Usually achieved with LOCKS.                                          |
|                                                                         |
|    Thread 1: malloc(100)  ---+                                         |
|                              |--- Lock protects shared heap            |
|    Thread 2: malloc(200)  ---+                                         |
|                                                                         |
|  Result: Both calls succeed, no corruption                             |
|                                                                         |
|                                                                         |
|  REENTRANT:                                                            |
|  ---------                                                             |
|  Can be interrupted at any point and safely called again               |
|  BEFORE the first call completes.                                      |
|  Requires NO LOCKS and NO GLOBAL/STATIC STATE.                         |
|                                                                         |
|    strtok("a,b", ",")  <-- Signal interrupts here                     |
|         |                                                              |
|         +-- Handler calls strtok("x,y", ",")                          |
|              |                                                         |
|              +-- Returns "x"                                           |
|         |                                                              |
|         +-- Returns to main... but static buffer corrupted!           |
|              strtok(NULL, ",") returns garbage                         |
|                                                                         |
|  strtok is thread-safe (with locks) but NOT reentrant!                |
|                                                                         |
|                                                                         |
|  THE RELATIONSHIP:                                                     |
|  ----------------                                                      |
|                                                                         |
|  +-----------------------------+                                       |
|  |        Reentrant           |                                       |
|  |  (No locks, no globals)    |                                       |
|  |  +---------------------+   |                                       |
|  |  | Async-signal-safe  |   |                                       |
|  |  | (Safe in handlers) |   |                                       |
|  |  +---------------------+   |                                       |
|  +-----------------------------+                                       |
|           ^                                                            |
|           |                                                            |
|           | (subset)                                                   |
|           |                                                            |
|  +-----------------------------+                                       |
|  |       Thread-safe          |                                       |
|  |  (May use locks)           |                                       |
|  +-----------------------------+                                       |
|                                                                         |
|  All reentrant functions are thread-safe.                              |
|  NOT all thread-safe functions are reentrant.                          |
|  Async-signal-safe requires reentrancy.                                |
|                                                                         |
+------------------------------------------------------------------------+

Why is printf() Not Async-Signal-Safe?

Let’s trace through exactly what printf() does internally:

+------------------------------------------------------------------------+
|                    INSIDE printf("Hello %d", 42)                        |
+------------------------------------------------------------------------+
|                                                                         |
|  1. ACQUIRE LOCK                                                       |
|     ============                                                       |
|     flockfile(stdout);   // Acquire stdio stream lock                  |
|                          // This is where deadlock can occur!          |
|                                                                         |
|  2. FORMAT STRING                                                      |
|     =============                                                      |
|     // Parse format string, extract arguments                          |
|     // May call malloc() for large formats                             |
|     // Uses internal static buffers in some implementations            |
|                                                                         |
|  3. BUFFER OUTPUT                                                      |
|     =============                                                      |
|     // Copy formatted data to stream buffer                            |
|     // Buffer is part of FILE structure (global state)                 |
|                                                                         |
|     +------------------+                                               |
|     | FILE *stdout     |                                               |
|     |  +-----------+   |                                               |
|     |  | buffer    |<--|-- Characters accumulate here                  |
|     |  +-----------+   |                                               |
|     |  | buf_ptr   |   |                                               |
|     |  | buf_end   |   |                                               |
|     |  | lock      |<--|-- This lock causes deadlock!                  |
|     |  +-----------+   |                                               |
|     +------------------+                                               |
|                                                                         |
|  4. FLUSH IF NEEDED                                                    |
|     ===============                                                    |
|     if (line_buffered && contains_newline) {                           |
|         write(stdout->fd, buffer, count);                              |
|         // Actual system call happens here                             |
|     }                                                                  |
|                                                                         |
|  5. RELEASE LOCK                                                       |
|     ============                                                       |
|     funlockfile(stdout);                                               |
|                                                                         |
|                                                                         |
|  SIGNAL CAN ARRIVE AT ANY POINT IN STEPS 1-5!                         |
|  ============================================                          |
|                                                                         |
|  If signal arrives between steps 1 and 5:                              |
|  - Lock is held                                                        |
|  - Handler calls printf()                                              |
|  - printf() tries to acquire lock                                      |
|  - DEADLOCK                                                            |
|                                                                         |
|  If signal arrives during step 2 or 3:                                 |
|  - Internal state is inconsistent                                      |
|  - Handler's printf() corrupts state                                   |
|  - Original printf() continues with corrupted state                    |
|  - UNDEFINED BEHAVIOR                                                  |
|                                                                         |
+------------------------------------------------------------------------+

Why is malloc() Not Async-Signal-Safe?

+------------------------------------------------------------------------+
|                    INSIDE malloc(100)                                   |
+------------------------------------------------------------------------+
|                                                                         |
|  HEAP STATE:                                                           |
|  ----------                                                            |
|                                                                         |
|  +------------------------------------------------------------------+ |
|  |  FREE LIST: [HDR|64]-->[HDR|128]-->[HDR|256]-->NULL              | |
|  |                                                                   | |
|  |  Heap metadata is GLOBAL STATE protected by LOCK                 | |
|  +------------------------------------------------------------------+ |
|                                                                         |
|  malloc(100) STEPS:                                                    |
|  -----------------                                                     |
|                                                                         |
|  1. pthread_mutex_lock(&heap_lock);  // <-- Signal arrives here       |
|                                                                         |
|  2. Search free list for block >= 112 bytes                           |
|     // Free list pointers being traversed...                          |
|     // <-- Signal arrives here, pointers in inconsistent state       |
|                                                                         |
|  3. Remove block from free list                                        |
|     prev->next = current->next;                                        |
|     // <-- Signal arrives here, list is broken!                       |
|                                                                         |
|  4. Split block if too large                                           |
|     // Modifying multiple pointers...                                  |
|                                                                         |
|  5. pthread_mutex_unlock(&heap_lock);                                  |
|                                                                         |
|                                                                         |
|  SCENARIO: Signal during step 2 or 3                                   |
|  -----------------------------------                                   |
|                                                                         |
|  Main:     malloc(100) - in step 2, traversing list                   |
|  Signal:   handler() called                                            |
|  Handler:  malloc(50) - tries to acquire lock                          |
|            DEADLOCK (or corruption if lock not held)                   |
|                                                                         |
|                                                                         |
|  SCENARIO: Signal during step 3                                        |
|  ------------------------------                                        |
|                                                                         |
|  Free list: A --> B --> C --> NULL                                    |
|  Main removing B: A.next = C (about to write)                         |
|  Signal arrives after reading B.next but before writing A.next        |
|  Handler calls malloc, sees: A --> B --> C                            |
|  Handler removes C: B.next = NULL                                     |
|  Handler returns                                                       |
|  Main continues: A.next = C (old value!)                              |
|  Result: A --> C --> NULL, but C.next = NULL now                      |
|  CORRUPTED HEAP!                                                       |
|                                                                         |
+------------------------------------------------------------------------+

The write(2) System Call: Your Only Safe Output

The write(2) system call is async-signal-safe because:

  1. It’s a direct kernel system call (no userspace buffering)
  2. It has no internal locks
  3. It doesn’t use any global state
  4. It’s atomic at the kernel level
+------------------------------------------------------------------------+
|                    THE write(2) SYSTEM CALL                             |
+------------------------------------------------------------------------+
|                                                                         |
|  PROTOTYPE:                                                            |
|  ---------                                                             |
|  ssize_t write(int fd, const void *buf, size_t count);                 |
|                                                                         |
|                                                                         |
|  WHAT IT DOES:                                                         |
|  ------------                                                          |
|                                                                         |
|  User space           |  Kernel space                                  |
|  -------------------  |  ------------------                            |
|                       |                                                |
|  write(fd, buf, n)    |                                                |
|       |               |                                                |
|       | syscall       |                                                |
|       v               |                                                |
|  +-----------+        |  +-----------+                                 |
|  | buf[0..n] | -------|->| Copy to   |                                 |
|  +-----------+        |  | kernel    |                                 |
|                       |  | buffer    |                                 |
|                       |  +-----------+                                 |
|                       |       |                                        |
|                       |       v                                        |
|                       |  +-----------+                                 |
|                       |  | Write to  |                                 |
|                       |  | device    |                                 |
|                       |  +-----------+                                 |
|                       |                                                |
|  No userspace locks!  |  Kernel handles concurrency                   |
|  No userspace state!  |  internally                                    |
|                                                                         |
|                                                                         |
|  RETURN VALUE:                                                         |
|  ------------                                                          |
|  - On success: Number of bytes written (may be < count!)               |
|  - On error: -1, errno set                                             |
|                                                                         |
|                                                                         |
|  PARTIAL WRITES:                                                       |
|  --------------                                                        |
|  write() may return LESS than requested if:                            |
|  - Interrupted by signal (EINTR)                                       |
|  - Output buffer full (non-blocking mode)                              |
|  - Writing to a pipe/socket with limited buffer                        |
|                                                                         |
|  YOUR CODE MUST HANDLE THIS:                                           |
|                                                                         |
|  ssize_t write_all(int fd, const char *buf, size_t n) {               |
|      size_t remaining = n;                                             |
|      while (remaining > 0) {                                           |
|          ssize_t written = write(fd, buf, remaining);                  |
|          if (written < 0) {                                            |
|              if (errno == EINTR)                                       |
|                  continue;  // Interrupted, retry                      |
|              return -1;     // Real error                              |
|          }                                                             |
|          buf += written;                                               |
|          remaining -= written;                                         |
|      }                                                                 |
|      return n;                                                         |
|  }                                                                     |
|                                                                         |
+------------------------------------------------------------------------+

Errno Preservation in Signal Handlers

Signal handlers can modify errno, which can corrupt the main program’s error checking:

+------------------------------------------------------------------------+
|                    THE ERRNO PROBLEM                                    |
+------------------------------------------------------------------------+
|                                                                         |
|  SCENARIO WITHOUT ERRNO PRESERVATION:                                  |
|  -----------------------------------                                   |
|                                                                         |
|  Main program:                                                         |
|  ------------                                                          |
|    fd = open("/tmp/data", O_RDONLY);                                   |
|    // open() fails, sets errno = ENOENT (2)                           |
|    // SIGNAL ARRIVES HERE!                                             |
|                                                                         |
|  Signal handler:                                                       |
|  --------------                                                        |
|    write(STDOUT_FILENO, "signal\n", 7);                                |
|    // write() succeeds, but some internal call sets errno = 0         |
|    // OR write() fails, sets errno = EBADF (9)                        |
|    return;                                                             |
|                                                                         |
|  Back in main:                                                         |
|  ------------                                                          |
|    if (fd < 0) {                                                       |
|        perror("open");   // errno is now 0 or EBADF, not ENOENT!      |
|        // Prints wrong error message!                                  |
|    }                                                                   |
|                                                                         |
|                                                                         |
|  THE FIX: Save and restore errno                                       |
|  --------------------------------                                      |
|                                                                         |
|  void handler(int sig) {                                               |
|      int saved_errno = errno;    // SAVE at entry                     |
|                                                                         |
|      // ... handler code ...                                           |
|      write(STDOUT_FILENO, "signal\n", 7);                              |
|                                                                         |
|      errno = saved_errno;        // RESTORE before return             |
|  }                                                                     |
|                                                                         |
|                                                                         |
|  RULE: EVERY signal handler should:                                    |
|        1. Save errno at the VERY BEGINNING                             |
|        2. Restore errno at the VERY END                                |
|        (Even if you don't think you modify it!)                       |
|                                                                         |
+------------------------------------------------------------------------+

Signal Delivery and Handler Execution

Understanding when and how signals interrupt execution:

+------------------------------------------------------------------------+
|                    SIGNAL DELIVERY MECHANICS                            |
+------------------------------------------------------------------------+
|                                                                         |
|  SIGNAL PENDING AND DELIVERY:                                          |
|  ---------------------------                                           |
|                                                                         |
|  1. Signal generated (kill(), timer, hardware exception)               |
|  2. Signal marked PENDING for process                                  |
|  3. Process returns to user mode (syscall return, interrupt return)   |
|  4. Kernel checks pending signals                                      |
|  5. If signal not blocked, kernel delivers it                          |
|  6. Handler runs (or default action taken)                             |
|                                                                         |
|                                                                         |
|  WHEN CAN SIGNALS INTERRUPT?                                           |
|  --------------------------                                            |
|                                                                         |
|  +--------------------+  +--------------------+                        |
|  | User-space code    |  | Signal can arrive  |                        |
|  | instruction 1      |  | BETWEEN any two    |                        |
|  | instruction 2      |  | instructions!      |                        |
|  | instruction 3  <---+--+                    |                        |
|  | instruction 4      |                       |                        |
|  +--------------------+                       |                        |
|                                                                         |
|  EVEN in the middle of a "simple" C statement:                         |
|                                                                         |
|  x = y + z;                                                            |
|  ^^^^^^^^^^^^^                                                          |
|  Compiles to:                                                          |
|    load y into register    <-- Signal can arrive here                  |
|    load z into register    <-- Or here                                 |
|    add registers           <-- Or here                                 |
|    store to x              <-- Or here                                 |
|                                                                         |
|                                                                         |
|  WHAT HAPPENS WHEN SIGNAL DELIVERED:                                   |
|  ----------------------------------                                    |
|                                                                         |
|  1. Kernel saves process state (registers, flags, PC)                 |
|  2. Kernel sets up handler stack frame                                 |
|  3. Handler executes (completely in user space)                       |
|  4. Handler returns (sigreturn syscall)                                |
|  5. Kernel restores saved state                                        |
|  6. Process continues from interrupted point                           |
|                                                                         |
|     Main program      Signal delivery       Handler                    |
|     ============      ==============        =======                    |
|                                                                         |
|     instruction N                                                      |
|          |                                                             |
|          v                                                             |
|     instruction N+1   <-- interrupted                                  |
|          |                 |                                           |
|          |                 +-> save state                              |
|          |                 |                                           |
|          |                 +-> handler()                               |
|          |                 |       |                                   |
|          |                 |       v                                   |
|          |                 |   sio_puts("signal")                      |
|          |                 |       |                                   |
|          |                 |       v                                   |
|          |                 |   return                                  |
|          |                 |                                           |
|          |                 +-> restore state                           |
|          |                 |                                           |
|          v            <----+                                           |
|     instruction N+2   (continues as if nothing happened)              |
|                                                                         |
+------------------------------------------------------------------------+

The sig_atomic_t Type

For safe communication between handler and main program:

+------------------------------------------------------------------------+
|                    ATOMIC OPERATIONS IN HANDLERS                        |
+------------------------------------------------------------------------+
|                                                                         |
|  THE PROBLEM WITH REGULAR VARIABLES:                                   |
|  ----------------------------------                                    |
|                                                                         |
|  int flag = 0;                                                         |
|                                                                         |
|  void handler(int sig) {                                               |
|      flag = 1;    // May not be atomic on all architectures!          |
|  }                                                                     |
|                                                                         |
|  int main() {                                                          |
|      while (!flag) {                                                   |
|          // Compiler might optimize this to: while(1) { ... }         |
|          // Because it doesn't know flag can change asynchronously    |
|      }                                                                 |
|  }                                                                     |
|                                                                         |
|                                                                         |
|  THE SOLUTION: volatile sig_atomic_t                                   |
|  -----------------------------------                                   |
|                                                                         |
|  volatile sig_atomic_t flag = 0;                                       |
|                                                                         |
|  - sig_atomic_t: Type that can be read/written atomically             |
|                  (always at least as large as int)                     |
|                                                                         |
|  - volatile: Tells compiler not to optimize away reads/writes         |
|              Forces re-read from memory each time                      |
|                                                                         |
|                                                                         |
|  WHAT sig_atomic_t GUARANTEES:                                         |
|  ----------------------------                                          |
|                                                                         |
|  1. Single read or write is atomic (won't be split)                   |
|  2. No torn reads/writes between handler and main                     |
|                                                                         |
|  WHAT sig_atomic_t DOES NOT GUARANTEE:                                 |
|  ------------------------------------                                  |
|                                                                         |
|  1. Read-modify-write is NOT atomic:                                   |
|                                                                         |
|     flag++;   // NOT SAFE! (read, add, write - three operations)      |
|                                                                         |
|  2. Memory ordering with other variables:                              |
|                                                                         |
|     data[index] = value;                                               |
|     flag = 1;  // Other thread might see flag=1 before data written   |
|                                                                         |
|                                                                         |
|  SAFE PATTERNS:                                                        |
|  -------------                                                         |
|                                                                         |
|  // Handler sets flag                                                  |
|  void handler(int sig) {                                               |
|      flag = 1;           // Simple assignment - SAFE                   |
|  }                                                                     |
|                                                                         |
|  // Main checks and clears flag                                        |
|  if (flag) {                                                           |
|      flag = 0;           // Simple assignment - SAFE                   |
|      // Process signal                                                 |
|  }                                                                     |
|                                                                         |
|                                                                         |
|  // Handler increments counter (UNSAFE!)                               |
|  void handler(int sig) {                                               |
|      count++;            // NOT ATOMIC - can lose counts              |
|  }                                                                     |
|                                                                         |
+------------------------------------------------------------------------+

Project Specification

What You Will Build

A small library (sio.c/sio.h) providing signal-safe I/O functions:

/* Core output primitives */
ssize_t sio_write(const char *buf, size_t len);  // Direct write() wrapper
ssize_t sio_puts(const char *s);                  // String output (no strlen!)
ssize_t sio_putl(long v);                         // Integer output
ssize_t sio_puthex(unsigned long v);              // Hexadecimal output

/* Optional: printf-like formatting */
void sio_printf(const char *fmt, ...);            // Minimal format: %s %d %x %p %%

Functional Requirements

+------------------------------------------------------------------------+
|                    FUNCTIONAL REQUIREMENTS                              |
+------------------------------------------------------------------------+
|                                                                         |
|  CORE API:                                                             |
|  --------                                                              |
|                                                                         |
|  ssize_t sio_write(const char *buf, size_t len);                       |
|    - Write exactly len bytes to stdout                                 |
|    - Handle partial writes (retry until all bytes written)            |
|    - Handle EINTR (retry on signal interruption)                       |
|    - Return total bytes written or -1 on error                         |
|                                                                         |
|  ssize_t sio_puts(const char *s);                                      |
|    - Write null-terminated string to stdout                            |
|    - DO NOT use strlen() - compute length with bounded loop           |
|    - Return bytes written or -1 on error                               |
|                                                                         |
|  ssize_t sio_putl(long v);                                             |
|    - Convert integer to string and write                               |
|    - Handle negative numbers                                           |
|    - NO sprintf, NO malloc - manual conversion only                    |
|    - Use stack-allocated buffer                                        |
|                                                                         |
|  ssize_t sio_puthex(unsigned long v);                                  |
|    - Convert to hexadecimal and write with "0x" prefix                |
|    - Handle zero specially                                             |
|                                                                         |
|  void sio_printf(const char *fmt, ...);                                |
|    - Parse format string character by character                        |
|    - Support: %d, %ld, %u, %lu, %x, %lx, %p, %s, %%                   |
|    - NO dynamic allocation                                             |
|    - NO stdio functions                                                |
|                                                                         |
|                                                                         |
|  CONSTRAINTS:                                                          |
|  -----------                                                           |
|                                                                         |
|  - ONLY use async-signal-safe functions                                |
|  - NO malloc, calloc, realloc, free                                    |
|  - NO printf, sprintf, snprintf, fprintf                               |
|  - NO strlen (implement bounded version)                               |
|  - ALL buffers must be stack-allocated                                 |
|  - Signal handlers using sio MUST save/restore errno                   |
|                                                                         |
+------------------------------------------------------------------------+

Non-Functional Requirements

  • Async-signal-safety: Library functions must be safe to call from any signal handler
  • Robustness: Must handle partial writes and EINTR correctly
  • Correctness: Output must match equivalent printf for all supported format specifiers
  • Performance: Direct write() calls, minimal overhead
  • Portability: Work on Linux and macOS (POSIX-compliant)

Real World Outcome

When you complete this project, here’s exactly what you’ll see when running your demo:

$ ./sio_demo
================================================================================
                    SIGNAL-SAFE I/O (SIO) DEMONSTRATION
================================================================================
[TEST 1] Basic output from main()
sio_puts: Hello from signal-safe I/O!
sio_putl: The answer is 42
sio_puthex: Address = 0x7fff5fbff8c0

[TEST 2] Signal handler output (SIGUSR1)
$ kill -USR1 $(pgrep sio_demo)
[HANDLER] Caught signal 10 (SIGUSR1)
[HANDLER] Handler invoked 1 time(s)
[HANDLER] Current errno preserved: 0

[TEST 3] Rapid signal delivery stress test
Sending 10000 SIGALRM signals at 10000 Hz...
[HANDLER] Signal count: 1000
[HANDLER] Signal count: 2000
[HANDLER] Signal count: 5000
[HANDLER] Signal count: 10000

[RESULT] All 10000 signals handled
[RESULT] No crashes, no corruption, no deadlocks
[RESULT] Printf equivalent calls in handler: 0 (verified safe)

$ ./sio_demo --compare-with-printf
================================================================================
                    SAFETY COMPARISON: SIO vs PRINTF
================================================================================
[SETUP] Installing SIGALRM handler that prints a message
[SETUP] Handler will fire every 100 microseconds

[TEST] Main thread calling malloc() in a loop...

--- Using printf() in handler (UNSAFE) ---
[MAIN] Iteration 1000...
[MAIN] Iteration 2000...
[DEADLOCK DETECTED] Program hung after 2847 iterations
[CAUSE] printf() called from handler while main held stdio lock

--- Using sio_puts() in handler (SAFE) ---
[MAIN] Iteration 1000...
[HANDLER] tick 50
[MAIN] Iteration 2000...
[HANDLER] tick 100
[MAIN] Iteration 10000...
[HANDLER] tick 500

[RESULT] Completed 10000 iterations with 500 handler invocations
[RESULT] No deadlocks with async-signal-safe sio functions

$ ./sio_demo --format-test
================================================================================
                    FORMAT SPECIFIER TESTS
================================================================================
Testing sio_printf() format specifiers:

sio_printf("Integer: %d\n", -42)     -> Integer: -42
sio_printf("Unsigned: %u\n", 42)     -> Unsigned: 42
sio_printf("Hex: 0x%x\n", 255)       -> Hex: 0xff
sio_printf("Long: %ld\n", 1234567890123) -> Long: 1234567890123
sio_printf("String: %s\n", "hello")  -> String: hello
sio_printf("Pointer: %p\n", ptr)     -> Pointer: 0x7fff5fbff8c0
sio_printf("Percent: %%\n")          -> Percent: %
sio_printf("Width: %10d\n", 42)      -> Width:         42
sio_printf("Multiple: %s=%d\n", "x", 5) -> Multiple: x=5

[RESULT] All format specifiers working correctly
[RESULT] No malloc, no stdio, only write(2) syscalls

Solution Architecture

High-Level Design

+------------------------------------------------------------------------+
|                    SIO LIBRARY ARCHITECTURE                             |
+------------------------------------------------------------------------+
|                                                                         |
|                     APPLICATION CODE                                    |
|                           |                                             |
|         +----------------+|+----------------+                          |
|         |   Main code    |||  Signal Handler|                          |
|         +----------------+|+----------------+                          |
|                |          ||        |                                  |
|                v          ||        v                                  |
|         +----------------+||+----------------+                          |
|         | sio_printf()   ||| sio_puts()     |                          |
|         | sio_puts()     ||| sio_putl()     |                          |
|         | sio_putl()     ||| sio_puthex()   |                          |
|         +----------------+||+----------------+                          |
|                |          ||        |                                  |
|                +----------++--------+                                  |
|                           |                                             |
|                           v                                             |
|                  +------------------+                                   |
|                  |    sio_write()   |                                   |
|                  | (handles partial |                                   |
|                  |  writes, EINTR)  |                                   |
|                  +------------------+                                   |
|                           |                                             |
|                           v                                             |
|                  +------------------+                                   |
|                  |    write(2)      |                                   |
|                  | (kernel syscall) |                                   |
|                  +------------------+                                   |
|                           |                                             |
|                           v                                             |
|                  +------------------+                                   |
|                  |  File Descriptor |                                   |
|                  |  (stdout = 1)    |                                   |
|                  +------------------+                                   |
|                                                                         |
+------------------------------------------------------------------------+

Unsafe vs Safe Handler Patterns

+------------------------------------------------------------------------+
|                    UNSAFE HANDLER PATTERN                               |
+------------------------------------------------------------------------+
|                                                                         |
|  void unsafe_handler(int sig) {                                        |
|      // BAD: Using printf - has internal locks                         |
|      printf("Received signal %d\n", sig);                              |
|                                                                         |
|      // BAD: Using malloc - has internal locks                         |
|      char *buf = malloc(100);                                          |
|      sprintf(buf, "Signal at %ld", time(NULL));                        |
|      free(buf);                                                        |
|                                                                         |
|      // BAD: Using strtok - uses static buffer                         |
|      strtok(some_string, ",");                                         |
|                                                                         |
|      // BAD: Not saving/restoring errno                                |
|      // (errno might be corrupted for main program)                    |
|  }                                                                     |
|                                                                         |
|  PROBLEMS:                                                             |
|  - Deadlock if main thread holds stdio/heap locks                     |
|  - Data corruption if handler interrupts malloc/printf                |
|  - errno corruption affects main program error handling               |
|                                                                         |
+------------------------------------------------------------------------+

+------------------------------------------------------------------------+
|                    SAFE HANDLER PATTERN                                 |
+------------------------------------------------------------------------+
|                                                                         |
|  volatile sig_atomic_t signal_count = 0;                               |
|                                                                         |
|  void safe_handler(int sig) {                                          |
|      // CRITICAL: Save errno first                                     |
|      int saved_errno = errno;                                          |
|                                                                         |
|      // SAFE: Increment atomic counter                                 |
|      signal_count++;                                                   |
|                                                                         |
|      // SAFE: Using sio functions (only write syscall)                |
|      sio_puts("[HANDLER] Caught signal ");                             |
|      sio_putl(sig);                                                    |
|      sio_puts("\n");                                                   |
|                                                                         |
|      // CRITICAL: Restore errno before return                          |
|      errno = saved_errno;                                              |
|  }                                                                     |
|                                                                         |
|  GUARANTEES:                                                           |
|  - No locks held, can't deadlock                                       |
|  - No global state modified (except sig_atomic_t)                     |
|  - errno preserved for main program                                    |
|  - Works even if signal interrupts another signal handler             |
|                                                                         |
+------------------------------------------------------------------------+

Integer to String Conversion (No malloc!)

+------------------------------------------------------------------------+
|                INTEGER TO STRING CONVERSION                             |
+------------------------------------------------------------------------+
|                                                                         |
|  PROBLEM: Convert 12345 to "12345" without sprintf or malloc           |
|                                                                         |
|  ALGORITHM: Repeated division, build string backwards                  |
|                                                                         |
|  12345 % 10 = 5    (rightmost digit)                                   |
|  12345 / 10 = 1234                                                     |
|  1234 % 10 = 4                                                         |
|  1234 / 10 = 123                                                       |
|  123 % 10 = 3                                                          |
|  123 / 10 = 12                                                         |
|  12 % 10 = 2                                                           |
|  12 / 10 = 1                                                           |
|  1 % 10 = 1                                                            |
|  1 / 10 = 0       (done)                                               |
|                                                                         |
|  Digits extracted: 5, 4, 3, 2, 1 (reverse order)                       |
|  Build string backwards: "54321" then read from end = "12345"          |
|                                                                         |
|                                                                         |
|  IMPLEMENTATION:                                                       |
|  --------------                                                        |
|                                                                         |
|  char buf[32];        // Stack buffer (enough for 64-bit long)        |
|  char *p = buf + 31;  // Start at end                                 |
|  *p = '\0';           // Null terminator                              |
|                                                                         |
|  unsigned long uval = (value < 0) ? -value : value;                   |
|                                                                         |
|  do {                                                                  |
|      *--p = '0' + (uval % 10);  // Convert digit to char              |
|      uval /= 10;                                                       |
|  } while (uval > 0);                                                   |
|                                                                         |
|  if (value < 0) {                                                      |
|      *--p = '-';               // Add negative sign                   |
|  }                                                                     |
|                                                                         |
|  // p now points to start of number string                             |
|  sio_puts(p);                                                          |
|                                                                         |
|                                                                         |
|  BUFFER VISUALIZATION:                                                 |
|  --------------------                                                  |
|                                                                         |
|  Initial:  buf = [?][?][?][?][?][?][?][?][\0]                         |
|                                           ^                            |
|                                           p                            |
|                                                                         |
|  After 5:  buf = [?][?][?][?][?][?][?][5][\0]                         |
|                                       ^                                |
|                                       p                                |
|                                                                         |
|  After 4:  buf = [?][?][?][?][?][?][4][5][\0]                         |
|                                    ^                                   |
|                                    p                                   |
|                                                                         |
|  Final:    buf = [?][?][?][1][2][3][4][5][\0]                         |
|                          ^                                             |
|                          p (return this)                               |
|                                                                         |
+------------------------------------------------------------------------+

Implementation Guide

The Core Question You’re Answering

“Why can’t you call printf() from a signal handler, and how do you build output functions that are safe to call from any context?”

This project forces you to understand the deep reasons why most standard library functions are unsafe in signal handlers. You’ll learn that it’s not about the functions being “buggy” - they work perfectly in normal contexts. The problem is that signals interrupt at arbitrary points, potentially in the middle of operations that rely on consistent internal state or held locks.

Concepts You Must Understand First

Before starting, ensure you understand these concepts:

Concept Why It Matters Where to Learn
Async-signal-safety Which functions can be safely called from signal handlers and why most cannot CS:APP 8.5.5, TLPI 21.1.2
Reentrancy What happens when a function is interrupted and called again before completing TLPI 21.1.2
Signal delivery semantics How signals interrupt execution at arbitrary points CS:APP 8.5
The write(2) syscall The only safe way to output from a signal handler TLPI 4.3
Errno preservation Why handlers must save and restore errno TLPI 21.1.3
Lock-free programming basics Why mutexes in handlers cause deadlocks TLPI 21.1.2

Questions to Guide Your Design

Work through these questions BEFORE writing code:

  1. Why is printf() not async-signal-safe? What specific resources does it use that cause problems?

  2. How do you convert an integer to a string without calling sprintf(), snprintf(), or any memory allocation?

  3. What buffer should you use for formatting? Stack-allocated? Static? What are the tradeoffs?

  4. How do you handle negative numbers in your integer-to-string conversion?

  5. Should sio functions buffer output or write immediately? What does buffering require that makes it unsafe?

  6. How do you implement hexadecimal output without using lookup tables that might not be in cache?

  7. What happens if write(2) is interrupted by another signal? How do you handle partial writes?

  8. How do you test that your implementation is truly async-signal-safe?

Thinking Exercise

Before coding, analyze why this handler deadlocks:

pthread_mutex_t stdio_lock = PTHREAD_MUTEX_INITIALIZER;
char buffer[1024];

void safe_looking_print(const char *msg) {
    pthread_mutex_lock(&stdio_lock);
    strcpy(buffer, msg);
    printf("%s\n", buffer);
    pthread_mutex_unlock(&stdio_lock);
}

void handler(int sig) {
    safe_looking_print("Signal received!");  // <-- Why does this deadlock?
}

int main() {
    signal(SIGINT, handler);
    while (1) {
        safe_looking_print("Main loop iteration");
    }
}

Trace through: What happens if SIGINT arrives while main() is between pthread_mutex_lock and pthread_mutex_unlock?

Now consider: Would making the mutex recursive solve the problem? (Hint: What about printf’s internal locks?)

Development Environment Setup

# Required tools
gcc --version      # Need GCC for compilation
make --version     # Build automation

# Create project structure
mkdir -p signal-safe-printf/{src,tests}
cd signal-safe-printf

# Create initial files
touch src/sio.c src/sio.h tests/sio_demo.c

Project Structure

signal-safe-printf/
├── src/
│   ├── sio.c              # Signal-safe I/O implementation
│   └── sio.h              # Public interface
├── tests/
│   ├── sio_demo.c         # Basic demonstration
│   ├── stress_test.c      # Rapid signal delivery test
│   └── deadlock_test.c    # Comparison with printf
├── Makefile
└── README.md

Implementation Phases

Phase 1: Core Write Primitive (Day 1, Morning)

Goals:

  • Implement sio_write() with partial write handling
  • Handle EINTR correctly

Layer 1 - Core Output Primitive:

// The ONLY function we can use for output in a signal handler
ssize_t sio_write(const char *s, size_t n) {
    size_t remaining = n;
    const char *p = s;

    while (remaining > 0) {
        ssize_t written = write(STDOUT_FILENO, p, remaining);
        if (written < 0) {
            if (errno == EINTR) continue;  // Interrupted, retry
            return -1;  // Real error
        }
        remaining -= written;
        p += written;
    }
    return n;
}

// Wrapper for null-terminated strings
ssize_t sio_puts(const char *s) {
    return sio_write(s, strlen(s));
}

Note: The strlen() above is technically not guaranteed async-signal-safe. In production, implement a bounded version:

static size_t sio_strlen(const char *s) {
    size_t len = 0;
    while (s[len] != '\0' && len < 4096) {  // Bounded loop
        len++;
    }
    return len;
}

Checkpoint: Can write strings from main() and see output.

Phase 2: Integer Formatting (Day 1, Afternoon)

Goals:

  • Implement integer-to-string conversion
  • Handle negative numbers
  • Use only stack buffers

Layer 2 - Integer to String (No malloc!):

// Convert integer to string in caller-provided buffer
// Returns pointer to start of number within buffer
char *sio_itoa(long value, char *buf, size_t bufsize) {
    char *p = buf + bufsize - 1;
    *p = '\0';

    int negative = (value < 0);
    unsigned long uval = negative ? -value : value;

    // Build string backwards
    do {
        *--p = '0' + (uval % 10);
        uval /= 10;
    } while (uval > 0 && p > buf);

    if (negative && p > buf) {
        *--p = '-';
    }

    return p;  // Start of the number string
}

// Output a long integer
ssize_t sio_putl(long value) {
    char buf[32];  // Stack allocated!
    char *s = sio_itoa(value, buf, sizeof(buf));
    return sio_puts(s);
}

Checkpoint: sio_putl(12345) outputs “12345”, sio_putl(-42) outputs “-42”.

Phase 3: Hexadecimal Output (Day 1, Evening)

Goals:

  • Implement hex formatting
  • Handle zero case
  • Add “0x” prefix

Layer 3 - Hexadecimal Output:

ssize_t sio_puthex(unsigned long value) {
    char buf[20];
    char *p = buf + sizeof(buf) - 1;
    *p = '\0';

    if (value == 0) {
        *--p = '0';
    } else {
        while (value > 0 && p > buf) {
            int digit = value & 0xF;
            *--p = (digit < 10) ? ('0' + digit) : ('a' + digit - 10);
            value >>= 4;
        }
    }

    // Add "0x" prefix
    *--p = 'x';
    *--p = '0';

    return sio_puts(p);
}

Checkpoint: sio_puthex(255) outputs “0xff”, sio_puthex(0) outputs “0x0”.

Phase 4: Signal Handler Pattern (Day 2, Morning)

Goals:

  • Create safe handler template
  • Implement errno preservation
  • Test with actual signals

Layer 4 - Signal Handler Pattern:

volatile sig_atomic_t signal_count = 0;

void handler(int sig) {
    // CRITICAL: Save and restore errno
    int saved_errno = errno;

    signal_count++;  // sig_atomic_t is safe to modify

    // Safe output
    sio_puts("[HANDLER] Signal ");
    sio_putl(sig);
    sio_puts(" received (count: ");
    sio_putl(signal_count);
    sio_puts(")\n");

    errno = saved_errno;  // Restore before return
}

Checkpoint: Handler runs without deadlock when signals delivered rapidly.

Phase 5: Format String Parser (Day 2, Afternoon)

Goals:

  • Implement minimal printf subset
  • Support %d, %ld, %s, %x, %p, %%
  • Parse format string character by character

Layer 5 - Simple Format String Parser:

// Minimal printf subset: %s, %d, %ld, %x, %p, %%
void sio_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    char buf[32];
    const char *p = fmt;

    while (*p) {
        if (*p != '%') {
            sio_write(p, 1);
            p++;
            continue;
        }

        p++;  // Skip '%'
        switch (*p) {
            case 'd': {
                int val = va_arg(ap, int);
                sio_puts(sio_itoa(val, buf, sizeof(buf)));
                break;
            }
            case 'l':
                p++;
                if (*p == 'd') {
                    long val = va_arg(ap, long);
                    sio_puts(sio_itoa(val, buf, sizeof(buf)));
                }
                break;
            case 's': {
                char *s = va_arg(ap, char*);
                sio_puts(s ? s : "(null)");
                break;
            }
            case 'x': {
                unsigned val = va_arg(ap, unsigned);
                sio_puthex(val);
                break;
            }
            case 'p': {
                void *ptr = va_arg(ap, void*);
                sio_puthex((unsigned long)ptr);
                break;
            }
            case '%':
                sio_write("%", 1);
                break;
        }
        p++;
    }

    va_end(ap);
}

Checkpoint: sio_printf(“x=%d, s=%s\n”, 42, “hello”) outputs “x=42, s=hello”.

Phase 6: Integration and Testing (Day 2, Evening)

Goals:

  • Create comprehensive test suite
  • Stress test with rapid signals
  • Compare with unsafe printf version

Testing Strategy

Basic Functionality Tests

void test_basic_output(void) {
    printf("=== Basic Output Tests ===\n");

    // Test sio_puts
    sio_puts("Testing sio_puts: Hello, World!\n");

    // Test sio_putl
    sio_puts("Testing sio_putl: ");
    sio_putl(12345);
    sio_puts("\n");

    sio_puts("Testing negative: ");
    sio_putl(-67890);
    sio_puts("\n");

    sio_puts("Testing zero: ");
    sio_putl(0);
    sio_puts("\n");

    sio_puts("Testing LONG_MAX: ");
    sio_putl(LONG_MAX);
    sio_puts("\n");

    sio_puts("Testing LONG_MIN: ");
    sio_putl(LONG_MIN);
    sio_puts("\n");

    // Test sio_puthex
    sio_puts("Testing sio_puthex(255): ");
    sio_puthex(255);
    sio_puts("\n");

    sio_puts("Testing sio_puthex(0): ");
    sio_puthex(0);
    sio_puts("\n");

    printf("=== Basic tests completed ===\n");
}

Signal Handler Test

volatile sig_atomic_t handler_called = 0;

void test_handler(int sig) {
    int saved_errno = errno;

    handler_called++;
    sio_puts("[HANDLER] Signal ");
    sio_putl(sig);
    sio_puts(" caught, count=");
    sio_putl(handler_called);
    sio_puts("\n");

    errno = saved_errno;
}

void test_signal_safety(void) {
    struct sigaction sa;
    sa.sa_handler = test_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGUSR1, &sa, NULL);

    printf("=== Signal Safety Test ===\n");
    printf("Sending SIGUSR1 to self...\n");

    for (int i = 0; i < 10; i++) {
        kill(getpid(), SIGUSR1);
    }

    printf("Handler called %d times\n", (int)handler_called);
    printf("=== Signal test completed ===\n");
}

Rapid Signal Delivery Stress Test

volatile sig_atomic_t rapid_count = 0;

void rapid_handler(int sig) {
    int saved_errno = errno;
    rapid_count++;

    // Only print every 1000 signals to avoid flooding
    if (rapid_count % 1000 == 0) {
        sio_puts("[HANDLER] Signal count: ");
        sio_putl(rapid_count);
        sio_puts("\n");
    }

    errno = saved_errno;
}

void test_rapid_signals(int count, int interval_us) {
    struct sigaction sa;
    sa.sa_handler = rapid_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, NULL);

    printf("=== Rapid Signal Test ===\n");
    printf("Sending %d signals at %d Hz...\n", count, 1000000 / interval_us);

    rapid_count = 0;

    // Set up interval timer
    struct itimerval timer;
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = interval_us;
    timer.it_interval.tv_sec = 0;
    timer.it_interval.tv_usec = interval_us;

    setitimer(ITIMER_REAL, &timer, NULL);

    // Wait for signals
    while (rapid_count < count) {
        pause();  // Wait for signal
    }

    // Stop timer
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &timer, NULL);

    printf("Completed: %ld signals handled\n", (long)rapid_count);
    printf("=== Rapid test completed ===\n");
}

Deadlock Comparison Test

volatile sig_atomic_t unsafe_count = 0;

void unsafe_handler(int sig) {
    unsafe_count++;
    printf("[UNSAFE] Signal %d caught\n", sig);  // UNSAFE!
}

void safe_handler_for_comparison(int sig) {
    int saved_errno = errno;
    unsafe_count++;
    sio_puts("[SAFE] Signal ");
    sio_putl(sig);
    sio_puts(" caught\n");
    errno = saved_errno;
}

void test_deadlock_comparison(void) {
    printf("=== Deadlock Comparison Test ===\n");

    // Test 1: Safe handler with malloc in main
    struct sigaction sa;
    sa.sa_handler = safe_handler_for_comparison;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, NULL);

    // Set timer for frequent signals
    struct itimerval timer;
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = 100;  // Every 100us
    timer.it_interval = timer.it_value;
    setitimer(ITIMER_REAL, &timer, NULL);

    printf("Testing SAFE handler with malloc loop...\n");
    for (int i = 0; i < 10000; i++) {
        void *p = malloc(100);
        if (p) {
            memset(p, 0, 100);
            free(p);
        }
        if (i % 2000 == 0) {
            printf("[MAIN] Iteration %d...\n", i);
        }
    }

    // Stop timer
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &timer, NULL);

    printf("SAFE handler: Completed without deadlock!\n");
    printf("Handler called %ld times\n", (long)unsafe_count);

    // Note: Testing the UNSAFE version would likely hang,
    // so we skip it in automated tests
    printf("(UNSAFE handler test skipped - would likely deadlock)\n");

    printf("=== Comparison test completed ===\n");
}

Format String Correctness Test

void test_format_strings(void) {
    printf("=== Format String Tests ===\n");

    // Compare sio_printf output with expected values
    printf("Expected: Integer: -42\n");
    sio_printf("sio:      Integer: %d\n", -42);

    printf("Expected: Unsigned: 42\n");
    sio_printf("sio:      Unsigned: %d\n", 42);

    printf("Expected: String: hello\n");
    sio_printf("sio:      String: %s\n", "hello");

    printf("Expected: NULL string: (null)\n");
    sio_printf("sio:      NULL string: %s\n", NULL);

    printf("Expected: Percent: %%\n");
    sio_printf("sio:      Percent: %%\n");

    printf("Expected: Multiple: x=5, y=hello\n");
    sio_printf("sio:      Multiple: x=%d, y=%s\n", 5, "hello");

    printf("=== Format tests completed ===\n");
}

Common Pitfalls & Debugging

Bug 1: Forgetting to Save/Restore errno

void handler(int sig) {
    // WRONG - corrupts errno if main code is checking it
    write(STDOUT_FILENO, "signal\n", 7);  // write() might set errno
}

// RIGHT
void handler(int sig) {
    int saved_errno = errno;
    write(STDOUT_FILENO, "signal\n", 7);
    errno = saved_errno;
}

Symptom: Intermittent incorrect error messages in main program.

Debugging: Add logging around errno checks in main program, look for unexpected values.

Bug 2: Using sprintf() “Because It Doesn’t malloc”

// WRONG - sprintf uses stdio buffers, internal locks
void handler(int sig) {
    char buf[64];
    sprintf(buf, "Signal %d\n", sig);  // NOT async-signal-safe!
    write(STDOUT_FILENO, buf, strlen(buf));
}

// RIGHT - manual conversion
void handler(int sig) {
    char buf[32];
    char *p = sio_itoa(sig, buf, sizeof(buf));
    sio_puts("Signal ");
    sio_puts(p);
    sio_puts("\n");
}

Symptom: Random deadlocks when printf in main coincides with signal.

Debugging: Replace sprintf with manual conversion, test with rapid signal delivery.

Bug 3: Static Buffers Shared Between Handler and Main

// WRONG - handler might corrupt buffer while main is using it
static char shared_buffer[256];

void handler(int sig) {
    strcpy(shared_buffer, "interrupted!");  // Race condition!
}

// RIGHT - use stack-local buffers in handler
void handler(int sig) {
    char local_buf[256];  // Each handler invocation gets its own
    // ...
}

Symptom: Corrupted output, garbled strings.

Debugging: Make all buffers in handler stack-local.

Bug 4: Ignoring Partial Writes

// WRONG - write() might not write everything
void handler(int sig) {
    char msg[] = "Very long message...";
    write(STDOUT_FILENO, msg, sizeof(msg));  // Might only write part!
}

// RIGHT - loop until all bytes written
void sio_write_all(const char *buf, size_t n) {
    while (n > 0) {
        ssize_t written = write(STDOUT_FILENO, buf, n);
        if (written <= 0) {
            if (errno == EINTR) continue;
            return;  // Error
        }
        buf += written;
        n -= written;
    }
}

Symptom: Truncated output, missing characters.

Debugging: Add logging to track bytes written vs requested.

Bug 5: Using strlen() Without Bound

// POTENTIALLY UNSAFE - strlen not guaranteed async-signal-safe
void handler(int sig) {
    const char *msg = "signal";
    write(STDOUT_FILENO, msg, strlen(msg));  // strlen might be unsafe
}

// SAFER - bounded length calculation
size_t safe_strlen(const char *s, size_t max) {
    size_t len = 0;
    while (len < max && s[len] != '\0') {
        len++;
    }
    return len;
}

Symptom: Usually works, but may fail on exotic platforms.

Debugging: Implement bounded strlen, add maximum length checks.

Bug 6: Integer Overflow in itoa

// WRONG - doesn't handle LONG_MIN correctly
char *sio_itoa(long value, char *buf, size_t bufsize) {
    int negative = (value < 0);
    unsigned long uval = negative ? -value : value;  // -LONG_MIN overflows!
    // ...
}

// RIGHT - handle LONG_MIN specially
char *sio_itoa(long value, char *buf, size_t bufsize) {
    // LONG_MIN is a special case: -LONG_MIN overflows
    // Either handle specially or use unsigned arithmetic throughout
    int negative = 0;
    unsigned long uval;

    if (value < 0) {
        negative = 1;
        uval = -(value + 1);  // Avoid overflow
        uval += 1;
    } else {
        uval = value;
    }
    // ...
}

Symptom: Wrong output for LONG_MIN.

Debugging: Test edge cases: 0, 1, -1, LONG_MAX, LONG_MIN.

Bug 7: Buffer Too Small

// WRONG - buffer might overflow for large numbers
void handler(int sig) {
    char buf[8];  // Too small for -2147483648 (11 chars + null)
    char *s = sio_itoa(some_value, buf, sizeof(buf));
    sio_puts(s);
}

// RIGHT - size buffer for worst case
void handler(int sig) {
    char buf[32];  // Enough for any 64-bit integer
    char *s = sio_itoa(some_value, buf, sizeof(buf));
    sio_puts(s);
}

Symptom: Buffer overflow, corrupted output, crashes.

Debugging: Calculate worst-case buffer size: 20 chars for 64-bit integer + sign + null.

Bug 8: Not Handling write() Errors

// WRONG - ignoring write failures
void handler(int sig) {
    sio_puts("signal\n");  // What if stdout is closed?
}

// BETTER - at least check for errors
ssize_t sio_puts(const char *s) {
    ssize_t result = sio_write(s, sio_strlen(s));
    // Can't really report errors from handler, but at least don't loop forever
    return result;
}

Symptom: Handler hangs if stdout closed or redirected.

Debugging: Limit retry attempts, add timeout logic if needed.


Extensions & Challenges

Beginner Extensions

  • Add sio_putd(): Output double/float values without stdio (harder than it sounds!)
  • Add field width: Support %10d for right-aligned numbers
  • Add stderr output: sio_error() that writes to STDERR_FILENO
  • Add newline handling: sio_println() that auto-adds newline

Intermediate Extensions

  • Signal-safe ring buffer: Handler enqueues messages, background thread drains
  • Timestamp support: Output time using only clock_gettime() (async-signal-safe)
  • Color support: Add ANSI escape codes for colored output
  • Log levels: sio_debug(), sio_info(), sio_warn(), sio_error() with filtering

Advanced Extensions

  • Crash reporter: Print registers from ucontext_t (platform-specific)
  • Stack trace: Walk stack frames and print return addresses
  • Symbol resolution: Convert addresses to function names (without malloc!)
  • Thread-safe version: Add per-thread buffers without locks

Ring Buffer Implementation

For high-frequency signals, writing directly might be too slow. Use a ring buffer:

/*
 * Signal-safe ring buffer for deferred logging
 * Handler writes to buffer, background thread drains to stdout
 */

#define RING_SIZE 4096

typedef struct {
    char buffer[RING_SIZE];
    volatile size_t write_pos;  // Only handler writes
    volatile size_t read_pos;   // Only drain thread reads
} ring_buffer_t;

// Handler: add message to ring (signal-safe)
void ring_write(ring_buffer_t *rb, const char *msg, size_t len) {
    // Simple implementation - no wrap handling for brevity
    size_t pos = rb->write_pos;
    size_t space = RING_SIZE - pos;

    if (len > space) len = space;  // Truncate if full

    for (size_t i = 0; i < len; i++) {
        rb->buffer[pos + i] = msg[i];
    }

    // Memory barrier needed here for correctness
    __sync_synchronize();

    rb->write_pos = pos + len;
}

// Drain thread: read and output (normal context)
void *drain_thread(void *arg) {
    ring_buffer_t *rb = (ring_buffer_t *)arg;

    while (1) {
        size_t read = rb->read_pos;
        size_t write = rb->write_pos;

        if (read < write) {
            write(STDOUT_FILENO, rb->buffer + read, write - read);
            rb->read_pos = write;
        }

        usleep(1000);  // Poll every 1ms
    }
}

Books That Will Help

Book Chapters What You’ll Learn
CS:APP 3e 8.5 Signal concepts, async-signal-safety, handler design
TLPI (The Linux Programming Interface) 21-22 Signals, signal handlers, async-signal-safe functions (comprehensive list)
APUE 3e (Advanced Programming in the UNIX Environment) 10 Signals (POSIX perspective)
OSTEP Ch. 5 (Process API) Understanding how signals fit with process model
Secure Coding in C and C++ Ch. 5 Signal handling vulnerabilities

Specific Sections to Read

CS:APP Chapter 8.5:

  • 8.5.1: Signal Terminology
  • 8.5.2: Sending Signals
  • 8.5.3: Receiving Signals
  • 8.5.4: Blocking and Unblocking Signals
  • 8.5.5: Writing Signal Handlers (KEY SECTION)
  • 8.5.6: Synchronizing Flows to Avoid Races

TLPI Chapter 21:

  • 21.1: Signal Handlers - detailed coverage of async-signal-safety
  • 21.1.2: Reentrant and Async-Signal-Safe Functions
  • 21.1.3: Global Variables and the sig_atomic_t Type

Self-Assessment Checklist

Understanding

  • I can explain why printf() is not async-signal-safe
  • I can list at least 10 async-signal-safe functions
  • I can explain the difference between reentrant and thread-safe
  • I understand why errno must be saved/restored in handlers
  • I can trace through a deadlock scenario with printf in handler
  • I understand why write(2) is safe but most stdio functions are not
  • I can explain what sig_atomic_t provides and what it doesn’t

Implementation

  • sio_write() handles partial writes correctly
  • sio_write() handles EINTR correctly
  • sio_putl() handles negative numbers
  • sio_putl() handles LONG_MIN correctly
  • sio_puthex() handles zero correctly
  • sio_printf() parses format string without stdio
  • All buffers are stack-allocated
  • Handlers using sio save/restore errno

Testing

  • Basic output matches expected values
  • Handler runs without deadlock under rapid signals
  • Format specifiers produce correct output
  • Edge cases (0, -1, LONG_MIN, LONG_MAX) work correctly
  • Comparison test shows sio is safe where printf deadlocks

Growth

  • I can use sio in other projects (shell, proxy, etc.)
  • I understand when to use signal-safe logging
  • I can explain handler safety in an interview
  • I can analyze whether a function is async-signal-safe

The Interview Questions They’ll Ask

After completing this project, you’ll be ready for these common interview questions:

1. “What makes a function async-signal-safe? Give examples of safe and unsafe functions.”

Expected answer: A function is async-signal-safe if it can be safely called from a signal handler, even if the main program was interrupted in the middle of the same function. Safe: write(), _exit(), signal(). Unsafe: printf(), malloc(), any function using locks or global state. The key issue is reentrancy and internal locks.

Follow-up to expect: “Why does using locks make a function unsafe in handlers?”

2. “Why is malloc() not async-signal-safe?”

Expected answer: malloc() uses internal locks to protect the heap data structures. If a signal interrupts malloc() while it holds the lock, and the handler calls malloc(), you get deadlock. Also, malloc() may be in the middle of updating heap metadata, leaving it in an inconsistent state.

Follow-up to expect: “What would happen if malloc didn’t use locks?”

3. “How would you implement a signal handler that needs to log messages?”

Expected answer: Use only write(2) for output. Pre-format simple messages as string constants. For dynamic data, implement integer-to-string conversion without malloc. Consider using a pipe or signal-safe queue to defer complex logging to the main thread.

Follow-up to expect: “What about timestamps or log levels?”

4. “Explain the errno problem in signal handlers and how to solve it.”

Expected answer: Many async-signal-safe functions (like write()) can set errno. If the handler modifies errno and the main code was about to check errno from its own syscall, the result is corrupted. Solution: Save errno at handler entry, restore before return.

Follow-up to expect: “When might you NOT need to save errno?”

5. “What’s the difference between reentrant and thread-safe?”

Expected answer: Thread-safe means safe when called concurrently from multiple threads (usually via locks). Reentrant means safe when interrupted and re-invoked before completing (no global/static state, no locks). All reentrant functions are thread-safe, but not vice versa. Async-signal-safe requires reentrancy.

Follow-up to expect: “Give an example of a function that is thread-safe but not reentrant.”

6. “How would you implement a printf-like format string parser that’s async-signal-safe?”

Expected answer: Parse the format string character by character. For each specifier, convert the value to a string using stack-local buffers and manual conversion (repeated division for integers). Accumulate output in a stack buffer, then call write() once. No dynamic allocation, no stdio.

Follow-up to expect: “How would you handle floating-point format specifiers?”


Uses concepts from:

  • P11 (Tiny Shell): Signal handlers for job control
  • P12 (Concurrent Web Proxy): Signal handling in multi-threaded context
  • P17 (Signal Catcher): Signal delivery and handling basics

Applies to:

  • Any project with signal handlers that need to output information
  • Crash reporters and debugging tools
  • High-reliability server applications

Completion Criteria

Minimum Viable Completion:

  • sio_write(), sio_puts(), sio_putl() implemented
  • Handler template with errno preservation
  • Basic tests pass
  • No deadlocks under rapid signal delivery

Full Completion:

  • All of the above plus sio_puthex() and sio_printf()
  • Format specifier tests pass
  • Comparison test demonstrates safety vs printf
  • Edge cases (LONG_MIN, zero, etc.) handled

Excellence (Going Above & Beyond):

  • Ring buffer for deferred logging
  • Timestamp support
  • Crash reporter with register dump
  • Integration with other projects (shell, proxy)

This guide was expanded from CSAPP_3E_DEEP_LEARNING_PROJECTS.md. For the complete learning path, see the project index.