Signal-Driven I/O in Multithreaded Apps

Signal-Driven I/O in Multithreaded Apps
Chapter 63 โ€“ Alternative I/O Models | Part 3 of 5
๐Ÿงต Topic: Multithreaded I/O
๐Ÿ”‘ Key: F_SETOWN_EX
๐ŸŽฏ Level: Advanced

The Problem: F_SETOWN Cannot Target a Thread

The older fcntl(fd, F_SETOWN, pid) lets you direct I/O signals to a process or a process group. In a single-threaded program this works perfectly. But in a multithreaded program, you might want a specific thread to receive the signal โ€” not just any thread in the process chosen arbitrarily by the kernel.

Starting with Linux kernel 2.6.32, two new fcntl() operations solve this: F_SETOWN_EX and F_GETOWN_EX. They let you direct signals to a specific thread, a process, or a process group.

Keywords in this tutorial:

F_SETOWN_EX F_GETOWN_EX f_owner_ex F_OWNER_TID F_OWNER_PID F_OWNER_PGRP gettid() clone() pthread

๐Ÿ”„ F_SETOWN vs F_SETOWN_EX โ€” What Changed

๐Ÿ”ด Old: F_SETOWN
  • Target: process or process group
  • Cannot target a specific thread
  • Process group ID given as negative value
  • Ambiguity with PGIDs < 4096
๐ŸŸข New: F_SETOWN_EX (kernel 2.6.32+)
  • Target: process, process group, OR a thread
  • Uses a struct to specify target type
  • Process group ID given as positive value (no ambiguity)
  • Thread ID via gettid() or clone()

๐Ÿ“ฆ The f_owner_ex Structure

For F_SETOWN_EX and F_GETOWN_EX, the third argument of fcntl() is a pointer to a struct f_owner_ex:

struct f_owner_ex {
    int   type;   /* what does pid mean? */
    pid_t pid;    /* the actual ID value */
};

The type field can be one of three values:

type value pid field means Notes
F_OWNER_PID A process ID Same as old F_SETOWN with a positive value
F_OWNER_PGRP A process group ID Given as positive (unlike F_SETOWN where PGRP was negative)
F_OWNER_TID A thread ID (TID) Value returned by gettid() or clone() โ€” not pthread_t!

โš ๏ธ The thread ID used in F_OWNER_TID is the kernel-level TID returned by gettid() โ€” this is different from the POSIX pthread_t handle returned by pthread_self().

๐Ÿ“– Reading the Owner: F_GETOWN_EX

F_GETOWN_EX does the reverse โ€” it reads back the current signal target. It fills in the f_owner_ex structure that the third argument points to.

struct f_owner_ex owner;
if (fcntl(fd, F_GETOWN_EX, &owner) == -1) {
    perror("F_GETOWN_EX");
}

switch (owner.type) {
case F_OWNER_TID:
    printf("Signal target: thread  TID=%d\n", owner.pid);
    break;
case F_OWNER_PID:
    printf("Signal target: process PID=%d\n", owner.pid);
    break;
case F_OWNER_PGRP:
    printf("Signal target: pgroup  PGID=%d\n", owner.pid);
    break;
}

Because F_GETOWN_EX represents process group IDs as positive values, it avoids the ambiguity that existed in the old F_GETOWN for process group IDs smaller than 4096 (which could be confused with process IDs).

๐Ÿ’ป Complete Example โ€” Directing I/O Signal to a Specific Thread
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>   /* SYS_gettid */
#include <errno.h>

/* Get kernel-level thread ID (glibc wrapper available since glibc 2.30) */
static pid_t get_tid(void)
{
    return (pid_t)syscall(SYS_gettid);
}

/* Signal handler for the I/O thread */
static void io_signal_handler(int sig, siginfo_t *si, void *ctx)
{
    printf("[TID %d] I/O event: fd=%d  si_code=%d\n",
           get_tid(), si->si_fd, si->si_code);
}

/* This thread will receive all I/O signals for fd */
static void *io_thread_func(void *arg)
{
    int fd = *(int *)arg;
    pid_t my_tid = get_tid();

    printf("[io_thread] TID = %d\n", my_tid);

    /* Install signal handler with SA_SIGINFO */
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = io_signal_handler;
    sa.sa_flags     = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGRTMIN, &sa, NULL);

    /* Point I/O signals for fd to THIS specific thread */
    struct f_owner_ex owner;
    owner.type = F_OWNER_TID;
    owner.pid  = my_tid;          /* kernel TID, not pthread_t */

    if (fcntl(fd, F_SETOWN_EX, &owner) == -1) {
        perror("F_SETOWN_EX");
        return NULL;
    }

    /* Enable async I/O on the fd */
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_ASYNC);

    /* Use realtime signal */
    fcntl(fd, F_SETSIG, SIGRTMIN);

    /* Verify โ€” read back the owner */
    struct f_owner_ex check;
    if (fcntl(fd, F_GETOWN_EX, &check) == 0) {
        printf("[io_thread] Verified owner: type=%d  TID=%d\n",
               check.type, check.pid);
    }

    printf("[io_thread] Ready โ€” waiting for I/O signals.\n");

    /* This thread loops receiving signals */
    for (;;) {
        pause();
    }

    return NULL;
}

int main(void)
{
    int fd = STDIN_FILENO;
    pthread_t tid;

    printf("[main] Starting I/O thread to handle fd=%d\n", fd);

    if (pthread_create(&tid, NULL, io_thread_func, &fd) != 0) {
        perror("pthread_create");
        return 1;
    }

    /* Main thread does other work */
    printf("[main TID %d] Main thread running. Type to trigger I/O.\n", get_tid());

    pthread_join(tid, NULL);
    return 0;
}

Compile:

gcc -o mt_sig_io mt_sig_io.c -lpthread
./mt_sig_io

๐Ÿ“Š How F_SETOWN_EX Routes the Signal to the Right Thread
I/O event
on fd
โ†’
Kernel checks
f_owner_ex on fd
โ†’
SIGRTMIN sent
to TID 12345 only
โ†’
io_thread_func
receives signal
Other threads in the same process are NOT disturbed

๐Ÿ“ Why Process Group IDs Are Positive in F_SETOWN_EX

The old F_SETOWN used a negative value to mean “process group” โ€” for example, passing -1234 meant “process group 1234”. This caused a bug: if the process group ID was less than 4096, the kernel could confuse it with a process ID (since PIDs also start from 1 and low values overlap).

F_SETOWN_EX eliminates this completely by using the type field to say what the ID represents. The pid field is always a positive number regardless of whether it is a PID, PGID, or TID.

/* Old way โ€” PGRP must be negative, ambiguous for small values */
fcntl(fd, F_SETOWN, -pgrp_id);   /* error-prone */

/* New way โ€” type makes it unambiguous */
struct f_owner_ex owner;
owner.type = F_OWNER_PGRP;
owner.pid  = pgrp_id;            /* always positive */
fcntl(fd, F_SETOWN_EX, &owner);  /* correct */

๐ŸŽฏ Interview Questions
Q1. What limitation does F_SETOWN have in multithreaded programs, and how does F_SETOWN_EX solve it?
F_SETOWN can only direct signals to a process or process group โ€” not to a specific thread. In a multithreaded program, any thread may receive the signal, which makes per-fd signal routing impossible. F_SETOWN_EX (added in kernel 2.6.32) adds F_OWNER_TID support, allowing signals to be directed to a specific kernel-level thread ID.
Q2. What are the three possible values of the type field in struct f_owner_ex?
F_OWNER_PID (target is a process), F_OWNER_PGRP (target is a process group โ€” positive ID), and F_OWNER_TID (target is a specific thread โ€” kernel TID from gettid()).
Q3. What is the difference between pthread_self() and gettid() for F_SETOWN_EX?
pthread_self() returns a POSIX pthread_t handle โ€” a user-space library concept. gettid() (syscall SYS_gettid) returns the kernel-level Thread ID (TID), which is what F_SETOWN_EX with F_OWNER_TID requires. These are different values and are not interchangeable.
Q4. Why did the old F_SETOWN have ambiguity with process group IDs less than 4096?
F_SETOWN encoded process group IDs as negative values. For small PGIDs (< 4096), the kernel could confuse the negated PGID with a valid negative pid_t range used for other purposes internally. F_SETOWN_EX avoids this by using an explicit type field and always representing all IDs as positive values.
Q5. Which kernel version introduced F_SETOWN_EX and F_GETOWN_EX?
Linux kernel 2.6.32.

Next: The epoll API โ€” Introduction โ†’

Discover why epoll outperforms select/poll and how the interest list and ready list work.

Part 4: epoll API Overview โ† Part 2

Leave a Reply

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