【问题标题】:Bug with robust mutex具有强大互斥锁的错误
【发布时间】:2023-03-03 20:31:01
【问题描述】:

我正在尝试在 linux 上使用健壮的互斥锁来保护进程之间的资源,似乎在某些情况下它们不会以“健壮”的方式运行。 “稳健”的方式是指 pthread_mutex_lock 如果拥有锁的进程已终止,则应返回 EOWNERDEAD。

这是它不起作用的场景:

2 进程 p1 和 p2。 p1 创建健壮的互斥体并等待它(在用户输入之后)。 p2 有 2 个线程:线程 1 映射到互斥体并获取它。线程 2(在线程 1 获得互斥锁之后)也映射到同一个互斥锁并等待它(因为线程 1 现在拥有它)。另请注意,在 p2-thread1 已经获取互斥体后,p1 开始等待互斥体。

现在,如果我们终止 p2,p1 永远不会解除阻塞(意味着它的 pthread_mutex_lock 永远不会返回),这与 p1 应该因 EOWNERDEAD 错误解除阻塞的假定“稳健性”相反。

代码如下:

p1.cpp:

    #include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

struct MyMtx {
    pthread_mutex_t m;
};

int main(int argc, char **argv)
{
    int r;

    pthread_mutexattr_t ma;
    pthread_mutexattr_init(&ma);
    pthread_mutexattr_setpshared(&ma, PTHREAD_PROCESS_SHARED);
    pthread_mutexattr_setrobust_np(&ma, PTHREAD_MUTEX_ROBUST_NP);

    int fd = shm_open("/test_mtx_p", O_RDWR|O_CREAT, 0666);
    ftruncate(fd, sizeof(MyMtx));

    MyMtx *m = (MyMtx *)mmap(NULL, sizeof(MyMtx),
        PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
    //close (fd);

    pthread_mutex_init(&m->m, &ma);

    puts("Press Enter to lock mutex");
    fgetc(stdin);

    puts("locking...");
    r = pthread_mutex_lock(&m->m);
    printf("pthread_mutex_lock returned %d\n", r);

    puts("Press Enter to unlock");
    fgetc(stdin);
    r = pthread_mutex_unlock(&m->m);
    printf("pthread_mutex_unlock returned %d\n", r);

    puts("Before pthread_mutex_destroy");
    r = pthread_mutex_destroy(&m->m);
    printf("After pthread_mutex_destroy, r=%d\n", r);

    munmap(m, sizeof(MyMtx));
    shm_unlink("/test_mtx_p");

    return 0;
}

p2.cpp:

    #include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

struct MyMtx {
    pthread_mutex_t m;
};

static void *threadFunc(void *arg)
{
    int fd = shm_open("/test_mtx_p", O_RDWR|O_CREAT, 0666);
    ftruncate(fd, sizeof(MyMtx));

    MyMtx *m = (MyMtx *)mmap(NULL, sizeof(MyMtx),
        PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
    sleep(2); //to let the first thread lock the mutex
    puts("Locking from another thread");
    int r = 0;
    r = pthread_mutex_lock(&m->m);
    printf("locked from another thread r=%d\n", r);
}

int main(int argc, char **argv)
{
    int r;
    int fd = shm_open("/test_mtx_p", O_RDWR|O_CREAT, 0666);
    ftruncate(fd, sizeof(MyMtx));

    MyMtx *m = (MyMtx *)mmap(NULL, sizeof(MyMtx),
        PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
    //close (fd);

    pthread_t tid;
    pthread_create(&tid, NULL, threadFunc, NULL);

    puts("locking");
    r = pthread_mutex_lock(&m->m);
    printf("pthread_mutex_lock returned %d\n", r);

    puts("Press Enter to terminate");
    fgetc(stdin);

    kill(getpid(), 9);
    return 0;
}

首先,运行 p1,然后运行 ​​p2 并等待它打印“Locking from another thread”。在 p1 的 shell 上按 Enter 以锁定互斥锁,然后在 p2 的 shell 上按 Enter 以终止 p2,或者您可以通过其他方式杀死它。你会看到 p1 打印“locking...”并且 pthread_mutex_lock 永远不会返回。

这个问题实际上并不总是发生,看起来它取决于时间。如果在 p1 开始锁定之后和终止 p2 之前等了一段时间,有时它会起作用并且 p2 的 pthread_mutex_lock 返回 130 (EOWNERDEAD)。但是,如果您在 p1 开始等待互斥体之后立即或短时间内终止 p2,则 p1 永远不会解除阻塞。

有没有人遇到过同样的问题?

【问题讨论】:

  • 我还更改了 p2.cpp 的代码以避免两次映射到共享内存,方法是使 MyMtx *m 全局变量和 threadFunc 使用它而不是调用 mmap。我得到了同样的结果。
  • 奇怪的是,当我用 SIGTERM 代替您的 SIGKILL 时,它似乎按预期工作。规范是否说明了基于信号的不同行为?
  • 嗯,我尝试使用 SIGTERM,但它仍然可以重现。与 p2 的 Ctrl-C 相同(我猜这是 SIGINT)。如果我在 p1 开始等待后立即终止 p2,它实际上总是为我重现。
  • SIGINT 也适用于我。 p1 返回 retcode = 130(所有者死亡)。我有点摆弄你的代码,但我现在看到的唯一区别是我注释掉了 p2 中的 ftruncates。
  • 您在哪个系统上运行?我在 Oracle Linux 5 上。

标签: c++ linux pthreads ipc mutex


【解决方案1】:

刚刚验证了 glibc 版本的行为:Linux 内核 2.6.32 和更高版本上的 2.11.1。

我的第一个发现:如果您在 p2 中的“从另一个线程锁定”之前在 p1 中按 Enter 键(在 2 秒内),那么健壮的互斥锁就可以正常工作。正如人们所期望的那样。结论:等待线程的顺序很重要。

第一个等待的线程被唤醒。不幸的是,当时被杀死的是 p2 中的线程。

有关问题的描述,请参阅https://lkml.org/lkml/2013/9/27/338

我不知道周围是否有内核修复/补丁。甚至不知道它是否被认为是一个错误。

尽管如此,对于整个混乱似乎有一种解决方法。通过 PTHREAD_PRIO_INHERIT 使用强大的互斥锁:

pthread_mutexattr_setprotocol(&ma, PTHREAD_PRIO_INHERIT);

在内核 (futex.c) 内部,而不是 handle_futex_death()exit_pi_state_list() 中的一些其他机制确实处理了其他互斥体服务员的唤醒。它似乎解决了这个问题。

【讨论】:

    【解决方案2】:

    尝试简化您的问题。看来您的问题是运行顺序。
    始终考虑最坏的情况:即使您运行 A 然后 B,B 仍然可以完成,而 A 刚刚开始运行。如有必要,请为此添加互斥控制。
    下面是 A(生产者)和 B(消费者)的简单示例:

            Main:
                Call A
    
            A:
                Lock
                Call B
                Produce
                Unlock
    
            B:
                Lock
                Consume
                Unlock
    

    【讨论】:

    • 你不会有死锁,因为你获得了两次锁(除非你使用可重入锁)?不过,对于我的示例,我知道何时以及由谁从 printfs 获取锁。无论如何,进程2中的哪个线程将获得锁并不重要,根据我理解的“鲁棒性”,当进程2死亡时,进程1应该能够获得锁。
    • 所以也许你需要的是pthread_cond_t,在使用互斥体时你会需要它。如果您需要示例,我可以发布一个简单的。
    • 我不确定 pthread_cond 在这里有什么帮助,因为进程 2 已终止,因此它没有机会发出条件信号。
    猜你喜欢
    • 2013-05-28
    • 1970-01-01
    • 2015-09-10
    • 1970-01-01
    • 1970-01-01
    • 2022-11-28
    • 2012-06-05
    • 2018-05-23
    • 1970-01-01
    相关资源
    最近更新 更多