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.
- Target: process or process group
- Cannot target a specific thread
- Process group ID given as negative value
- Ambiguity with PGIDs < 4096
- 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()
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().
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).
#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
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 */
Discover why epoll outperforms select/poll and how the interest list and ready list work.
