单线程进程中的 pthread 互斥量是否需要在 fork() 上重新初始化?
Do pthread mutexes in a single-threaded process need to be re-initialized on fork()?
前言
上面标记的“欺骗”没有回答我的问题,因为它涉及使用线程分叉的安全性。我没有在我的代码中生成线程,并且更关心 pthread_mutex_t
结构的有效性及其在 fork()
发生 . 时注册到 OS 的内部结构,即:是否在子进程内 fork()
上重新创建了互斥体,或者子进程是否仅具有父互斥体内部的有效(或无效)浅表副本?
背景
我有一个 audio/hardware 处理库,它使用递归 pthread_mutex_t
用一个简单的 API 包装了一些 DSP 函数。它是递归互斥锁的原因是因为一些 API 函数依次调用其他 API 函数,我想确保只有一个线程进入库的每个实例的临界区。所以,代码看起来像这样:
static pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
void read() {
pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
}
void write() {
pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
}
void toggle() {
pthread_mutex_lock(&mutex);
read();
// ...
write();
pthread_mutex_unlock(&mutex);
}
问题
如果用户应用程序使用我的库,并且该应用程序发出 fork()
调用,我的库的子进程实例是否需要重新初始化其互斥体实例?我知道子进程不继承线程,如果我希望两个进程真正共享互斥锁(不记得标志是什么)或者我必须使用 mmap
IIRC。但是子进程使用的互斥锁实例是否有效(即:fork()
是否复制了内部值,但它们不再有效,或者是使用 OS 初始化的新互斥锁)? 我不希望子进程和父进程在 fork()
发生时共享互斥锁,但我想确保客户端使用的是有效的互斥锁句柄。
注意:我可以保证在发出 fork()
调用时,互斥体 不会 被锁定。
谢谢。
仔细阅读 POSIX 表明它是未定义的:
fork()
说它创建了一个精确的 copy 过程;
- Synchronization Object Copies and Alternative Mappings 表示 在锁定、解锁或销毁对象时引用对象副本的效果未定义。
如果您同意这篇文章,那么最简单的解决方案就是在单线程情况下完全避免使用互斥锁。您可以要求库用户 link 针对库的多线程版本或具有 #ifdef 同步的单线程版本,具体取决于他们的用例。或者,您可以允许用户提供您的库调用的锁定/解锁回调,而不是自己调用 pthread_mutex_lock()
/ pthread_mutex_unlock()
- 单线程代码只会提供空回调。
Note: I can guarantee that the mutex will not be locked when the fork()
call is issued.
为了保证这一点,这意味着您[如您所说]没有使用线程。否则,您无法保证这一点。所以,我同意山姆的观点。您不需要使用互斥量。
如果你是使用线程,你可能想做pthread_create
而不是fork
,这样互斥就会被释放"naturally" [被抱线].
当您 fork
时,pthread 互斥量 没有 validity/meaning。它需要一个共享地址 space,在你 fork 之后,你就不再拥有了。因此,安全的做法是重新初始化 child 中的互斥量。但是,此互斥锁不再与 parent 进程或其线程兄弟一起运行。如果分叉线程 不是 互斥锁所有者,init 只是为了防止 child 阻塞。
要在 个线程 之间锁定,请使用 pthread_mutex_*
。要在 进程 之间锁定,请使用 SysV 信号量或 posix 信号量。
根据您在代码示例中使用的 pthread 互斥锁,我怀疑您希望在给定的 API 调用期间独占访问音频设备。
如果您不使用线程,那么您已经拥有 没有 互斥锁。当您添加分叉时,互斥体不起作用,因此您需要 [named] 信号量而不是 [并且您必须实现自己的包装器,因为 IIRC,sem_wait
等。阿尔。不要递归]
Wrt。 POSIX,我同意 ,但幸运的是在 Linux 中定义了语义 。
来自 Linux man 2 fork
手册页:
The child process is created with a single thread—the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
因此,在 Linux 中,如果互斥量已解锁,则不需要在 fork()
.
后重新初始化它们
更重要的是,POSIX 为信号量定义了相同的东西;那
Any semaphores that are open in the parent process shall also be open in the child process.
这意味着您可以用信号量替换递归互斥量;只需将您的代码替换为
#include <semaphore.h>
static sem_t my_lock;
static void my_lock_init(void) __attribute__((constructor));
static void my_lock_init(void) {
sem_init(&my_lock, 0, 1U);
}
void my_read() {
sem_wait(&my_lock);
// ...
sem_post(&my_lock);
}
void my_write() {
sem_wait(&my_lock);
// ...
sem_post(&my_lock);
}
void toggle() {
sem_wait(&my_lock);
// ...
my_read();
// ...
my_write();
// ...
sem_post(&my_lock);
}
重新初始化已经初始化的信号量会导致未定义的行为。这意味着上面的工作,如果你能以某种方式证明 fork()
永远不会发生在你的代码执行第一个 sem_wait(&my_lock)
和最后一个 sem_post(&my_lock)
.
之间
问题是,通常不可能证明在多线程程序中,当另一个线程执行 fork()
.[= 时,其他线程的 none 正在执行上述任何功能。 25=]
在 Linux 中,内核 2.6 或更高版本,GNU C 库版本 2.5 或更高版本,pthreads 基于 NPTL,pthread 锁定原语在 futex()
syscall 之上实现。
内核仅在线程被阻塞或等待时才知道 futex。其余时间,futex 只是一个普通的数据结构。 (内核使用 futex 的地址来区分它们;共享内存的地址被特殊处理。)
这意味着当使用 futex 时,您可以在 fork()
之后安全地重新初始化互斥量,只要您自己的代码在持有互斥量时不进行分叉。
举个例子——请记住,仅适用于 Linux 2.6 或更高版本,以及 GNU C 库 2.5 或更高版本:
#define _GNU_SOURCE
#include <pthread.h>
static pthread_mutex_t my_lock = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
static void my_reinit(void)
{
my_lock = (pthread_mutex_t)PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
}
static void my_init(void) __attribute__((constructor (65535)));
static void my_init(void)
{
pthread_atfork(NULL, NULL, my_reinit);
}
void my_read()
{
pthread_mutex_lock(&my_lock);
// ...
pthread_mutex_unlock(&my_lock);
}
void my_write()
{
pthread_mutex_lock(&my_lock);
// ...
pthread_mutex_unlock(&my_lock);
}
void toggle()
{
pthread_mutex_lock(&my_lock);
// ...
my_read();
// ...
my_write();
// ...
pthread_mutex_unlock(&my_lock);
}
前言
上面标记的“欺骗”没有回答我的问题,因为它涉及使用线程分叉的安全性。我没有在我的代码中生成线程,并且更关心 pthread_mutex_t
结构的有效性及其在 fork()
发生 . 时注册到 OS 的内部结构,即:是否在子进程内 fork()
上重新创建了互斥体,或者子进程是否仅具有父互斥体内部的有效(或无效)浅表副本?
背景
我有一个 audio/hardware 处理库,它使用递归 pthread_mutex_t
用一个简单的 API 包装了一些 DSP 函数。它是递归互斥锁的原因是因为一些 API 函数依次调用其他 API 函数,我想确保只有一个线程进入库的每个实例的临界区。所以,代码看起来像这样:
static pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
void read() {
pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
}
void write() {
pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
}
void toggle() {
pthread_mutex_lock(&mutex);
read();
// ...
write();
pthread_mutex_unlock(&mutex);
}
问题
如果用户应用程序使用我的库,并且该应用程序发出 fork()
调用,我的库的子进程实例是否需要重新初始化其互斥体实例?我知道子进程不继承线程,如果我希望两个进程真正共享互斥锁(不记得标志是什么)或者我必须使用 mmap
IIRC。但是子进程使用的互斥锁实例是否有效(即:fork()
是否复制了内部值,但它们不再有效,或者是使用 OS 初始化的新互斥锁)? 我不希望子进程和父进程在 fork()
发生时共享互斥锁,但我想确保客户端使用的是有效的互斥锁句柄。
注意:我可以保证在发出 fork()
调用时,互斥体 不会 被锁定。
谢谢。
仔细阅读 POSIX 表明它是未定义的:
fork()
说它创建了一个精确的 copy 过程;- Synchronization Object Copies and Alternative Mappings 表示 在锁定、解锁或销毁对象时引用对象副本的效果未定义。
如果您同意这篇文章,那么最简单的解决方案就是在单线程情况下完全避免使用互斥锁。您可以要求库用户 link 针对库的多线程版本或具有 #ifdef 同步的单线程版本,具体取决于他们的用例。或者,您可以允许用户提供您的库调用的锁定/解锁回调,而不是自己调用 pthread_mutex_lock()
/ pthread_mutex_unlock()
- 单线程代码只会提供空回调。
Note: I can guarantee that the mutex will not be locked when the
fork()
call is issued.
为了保证这一点,这意味着您[如您所说]没有使用线程。否则,您无法保证这一点。所以,我同意山姆的观点。您不需要使用互斥量。
如果你是使用线程,你可能想做pthread_create
而不是fork
,这样互斥就会被释放"naturally" [被抱线].
当您 fork
时,pthread 互斥量 没有 validity/meaning。它需要一个共享地址 space,在你 fork 之后,你就不再拥有了。因此,安全的做法是重新初始化 child 中的互斥量。但是,此互斥锁不再与 parent 进程或其线程兄弟一起运行。如果分叉线程 不是 互斥锁所有者,init 只是为了防止 child 阻塞。
要在 个线程 之间锁定,请使用 pthread_mutex_*
。要在 进程 之间锁定,请使用 SysV 信号量或 posix 信号量。
根据您在代码示例中使用的 pthread 互斥锁,我怀疑您希望在给定的 API 调用期间独占访问音频设备。
如果您不使用线程,那么您已经拥有 没有 互斥锁。当您添加分叉时,互斥体不起作用,因此您需要 [named] 信号量而不是 [并且您必须实现自己的包装器,因为 IIRC,sem_wait
等。阿尔。不要递归]
Wrt。 POSIX,我同意
来自 Linux man 2 fork
手册页:
The child process is created with a single thread—the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
因此,在 Linux 中,如果互斥量已解锁,则不需要在 fork()
.
更重要的是,POSIX 为信号量定义了相同的东西;那
Any semaphores that are open in the parent process shall also be open in the child process.
这意味着您可以用信号量替换递归互斥量;只需将您的代码替换为
#include <semaphore.h>
static sem_t my_lock;
static void my_lock_init(void) __attribute__((constructor));
static void my_lock_init(void) {
sem_init(&my_lock, 0, 1U);
}
void my_read() {
sem_wait(&my_lock);
// ...
sem_post(&my_lock);
}
void my_write() {
sem_wait(&my_lock);
// ...
sem_post(&my_lock);
}
void toggle() {
sem_wait(&my_lock);
// ...
my_read();
// ...
my_write();
// ...
sem_post(&my_lock);
}
重新初始化已经初始化的信号量会导致未定义的行为。这意味着上面的工作,如果你能以某种方式证明 fork()
永远不会发生在你的代码执行第一个 sem_wait(&my_lock)
和最后一个 sem_post(&my_lock)
.
问题是,通常不可能证明在多线程程序中,当另一个线程执行 fork()
.[= 时,其他线程的 none 正在执行上述任何功能。 25=]
在 Linux 中,内核 2.6 或更高版本,GNU C 库版本 2.5 或更高版本,pthreads 基于 NPTL,pthread 锁定原语在 futex()
syscall 之上实现。
内核仅在线程被阻塞或等待时才知道 futex。其余时间,futex 只是一个普通的数据结构。 (内核使用 futex 的地址来区分它们;共享内存的地址被特殊处理。)
这意味着当使用 futex 时,您可以在 fork()
之后安全地重新初始化互斥量,只要您自己的代码在持有互斥量时不进行分叉。
举个例子——请记住,仅适用于 Linux 2.6 或更高版本,以及 GNU C 库 2.5 或更高版本:
#define _GNU_SOURCE
#include <pthread.h>
static pthread_mutex_t my_lock = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
static void my_reinit(void)
{
my_lock = (pthread_mutex_t)PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
}
static void my_init(void) __attribute__((constructor (65535)));
static void my_init(void)
{
pthread_atfork(NULL, NULL, my_reinit);
}
void my_read()
{
pthread_mutex_lock(&my_lock);
// ...
pthread_mutex_unlock(&my_lock);
}
void my_write()
{
pthread_mutex_lock(&my_lock);
// ...
pthread_mutex_unlock(&my_lock);
}
void toggle()
{
pthread_mutex_lock(&my_lock);
// ...
my_read();
// ...
my_write();
// ...
pthread_mutex_unlock(&my_lock);
}