PTHREAD_ATFORK(3POSIX)
π PTHREAD_ATFORK π β from the POSIX Programmer's Manual (3POSIX)
okay bestie, quick heads up before we dive in: this page lives in the POSIX Programmer's Manual. That means the Linux version of this thing might act a little different β if you're on Linux, go read the matching Linux man page for the deets on Linux behavior. Or, plot twist, the interface might not even be implemented on Linux at all. π Not every platform is as committed as we are.
β¨ NAME β¨
pthread_atfork β register fork handlers
π SYNOPSIS π
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
void (*child)(void));
π¦ DESCRIPTION π¦
So pthread_atfork() declares fork handlers β lil callback functions that get called before and after fork(), all in the context of the thread that called fork(). Cute, right? Here's the lineup:
- The
preparefork handler gets called beforefork()processing even commences. - The
parentfork handler gets called afterfork()processing completes, in the parent process. - The
childfork handler gets called afterfork()processing completes, in the child process.
And if you don't want handling at one, two, or all three of those points? Just set the corresponding fork handler address(es) to NULL. No pressure. π
π¨ okay this part matters: if a fork() call in a multi-threaded process leads to a child fork handler calling any function that is not async-signal-safe, the behavior is undefined. Not "weird", not "risky" β undefined. Periodt.
The order you call pthread_atfork() in is significant. The parent and child fork handlers get called in the order they were established by calls to pthread_atfork(). The prepare fork handlers? Called in the opposite order. Yes, it's giving stack. LIFO for prepare, FIFO for parent and child. Slay. β
π RETURN VALUE π
If everything goes great, pthread_atfork() returns a value of zero. Otherwise, it returns an error number to tell you what went wrong.
π₯ ERRORS π₯
The pthread_atfork() function shall fail if:
ENOMEMβ there's insufficient table space to record the fork handler addresses. She's full. π
Also, lowkey important: the pthread_atfork() function shall not return an error code of [EINTR].
The following sections are informative.
πΈ EXAMPLES πΈ
None. Bestie, there are literally no examples here. We're on our own. π«Ά
π APPLICATION USAGE π
So the original usage pattern they imagined for pthread_atfork() was this: the prepare fork handler locks mutexes and other locks, and the parent and child handlers unlock them. Iconic in theory.
But here's the thing β since all of the relevant unlocking functions, except sem_post(), are not async-signal-safe, this usage results in undefined behavior in the child process unless the only such unlocking function it calls is sem_post(). So basically: sem_post() or bust. π
π RATIONALE π
okay so this is the big one, buckle up, we're getting into the lore.
There are at least two serious problems with the semantics of fork() in a multi-threaded program.
Problem the first: state covered by mutexes. Picture this: one thread has a mutex locked, and the state covered by that mutex is inconsistent, while another thread calls fork(). In the child, that mutex is now in the locked state β locked by a nonexistent thread, which means it can never be unlocked. Ever. And just having the child reinitialize the mutex is unsatisfactory, because that approach doesn't resolve the question of how to correct or otherwise deal with the inconsistent state in the child. It's giving "we fixed the lock but the data is still cursed." π«
The suggested workaround: programs that use fork() should call an exec function very soon afterwards in the child process, which resets all states. In the meantime, only a short list of async-signal-safe library routines are promised to be available.
But unfortunately this solution does not address the needs of multi-threaded libraries. Application programs may not even be aware that a multi-threaded library is in use, and they feel free to call any number of library routines between the fork() and exec calls β just as they always have. Truly, they may be extant single-threaded programs, and therefore cannot be expected to obey new restrictions imposed by the threads library. Rude to ask, honestly.
On the other hand, the multi-threaded library needs a way to protect its internal state during fork() in case it gets re-entered later in the child process. This problem arises especially in multi-threaded I/O libraries, which are almost sure to be invoked between the fork() and exec calls to effect I/O redirection. The solution may require locking mutex variables during fork(), or it may entail simply resetting the state in the child after the fork() processing completes.
So what was pthread_atfork() for? It was intended to give multi-threaded libraries a means to protect themselves from innocent application programs that call fork(), and to give multi-threaded application programs a standard mechanism for protecting themselves from fork() calls in a library routine or the application itself.
The expected usage was that the prepare handler would acquire all mutex locks and the other two fork handlers would release them.
For example, an application could have supplied a prepare routine that acquires the necessary mutexes the library maintains, and supplied child and parent routines that release those mutexes β thus ensuring that the child would have got a consistent snapshot of the state of the library (and that no mutexes would have been left stranded). Great in theory. In reality? Not practical. Why? Because:
- Each and every mutex and lock in the process must be located and locked.
- Every component of a program β including third-party components β must participate, and they must all agree on who is responsible for which mutex or lock. Good luck getting that group chat to agree on anything. π
- This is especially problematic for mutexes and locks in dynamically allocated memory.
- All mutexes and locks internal to the implementation must be locked, too.
- This possibly delays the thread calling
fork()for a long time, or even indefinitely, since uses of these synchronization objects may not be under control of the application. - And the final problem to mention: locking streams. At least the streams under control of the system (like
stdin,stdout,stderr) must be protected by locking the stream withflockfile(). But the application itself could have done that, possibly in the same thread callingfork(). In this case, the process will deadlock. π
Alternative idea that also flopped: some libraries might have been able to supply just a child routine that reinitializes the mutexes in the library and all associated states to some known value (for example, what it was when the image was originally executed). This approach is not possible, though, because implementations are allowed to fail *_init() and *_destroy() calls for mutexes and locks if the mutex or lock is still locked. In that case, the child routine is not able to reinitialize the mutexes and locks. Caught in a loop, bestie.
More lore: when fork() is called, only the calling thread is duplicated in the child process. Synchronization variables remain in the same state in the child as they were in the parent at the time fork() was called. So, for example, mutex locks may be held by threads that no longer exist in the child process, and any associated states may be inconsistent. The intention was that the parent process could have avoided this by explicit code that acquires and releases locks critical to the child via pthread_atfork(). In addition, any critical threads would have needed to be recreated and reinitialized to the proper state in the child (also via pthread_atfork()). Main character energy from the parent process only. π
Cute lil design detail: a higher-level package may acquire locks on its own data structures before invoking lower-level packages. Under this scenario, the order specified for fork handler calls allows a simple rule of initialization for avoiding package deadlock: a package initializes all packages on which it depends before it calls the pthread_atfork() function for itself. Dependencies first, always. π
And the conclusion: as explained, there is no suitable solution for functionality which requires non-atomic operations to be protected through mutexes and locks. This is why the POSIX.1 standard, since the 1996 release, requires that the child process after fork() in a multi-threaded process only calls async-signal-safe interfaces. That's the rule. That's the vibe check. β
π§ FUTURE DIRECTIONS π§
The pthread_atfork() function may be formally deprecated (for example, by shading it OB) in a future version of this standard. π so watch this space.
π SEE ALSO π
atexit()execfork()
And from the Base Definitions volume of POSIX.1β2017: <pthread.h>, <sys_types.h>
π COPYRIGHT π
Portions of this text are reprinted and reproduced in electronic form from IEEE Std 1003.1-2017, Standard for Information Technology -- Portable Operating System Interface (POSIX), The Open Group Base Specifications Issue 7, 2018 Edition, Copyright (C) 2018 by the Institute of Electrical and Electronics Engineers, Inc and The Open Group. In the event of any discrepancy between this version and the original IEEE and The Open Group Standard, the original IEEE and The Open Group Standard is the referee document. The original Standard can be obtained online at http://www.opengroup.org/unix/online.html .
Any typographical or formatting errors that appear in this page are most likely to have been introduced during the conversion of the source files to man page format. To report such errors, see https://www.kernel.org/doc/man-pages/reporting_bugs.html .
mwah π that's the whole page, bestie. Shoutout to IEEE/The Open Group, year 2017, holding it down for PTHREAD_ATFORK(3POSIX). Go forth and fork responsibly β or, y'know, just exec right after. π«Άβ¨
π¦ where this page comes from
- debian bookworm: manpages-posix-dev 2017a-2 Β· Other
- debian trixie: manpages-posix-dev 2017a-2 Β· Other
- ubuntu jammy: manpages-posix-dev 2017a-2 Β· Other
- ubuntu noble: manpages-posix-dev 2017a-2 Β· Other
- ubuntu resolute: manpages-posix-dev 2017a-2build1 Β· Other
- ubuntu stonking: manpages-posix-dev 2017a-2.1 Β· Other
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms