【问题标题】:Force unlock a mutex that was locked by a different thread强制解锁被其他线程锁定的互斥锁
【发布时间】:2015-04-26 19:46:12
【问题描述】:

考虑以下测试程序:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>


pthread_mutex_t mutex;
pthread_mutexattr_t mattr;
pthread_t thread1;
pthread_t thread2;
pthread_t thread3;


void mutex_force_unlock(pthread_mutex_t *mutex, pthread_mutexattr_t *mattr)
  {
    int e;
    e = pthread_mutex_destroy(mutex);
    printf("mfu: %s\n", strerror(e));
    e = pthread_mutex_init(mutex, mattr);
    printf("mfu: %s\n", strerror(e));
  }

void *thread(void *d)
  {
    int e;

    e = pthread_mutex_trylock(&mutex);
    if (e != 0)
      {
        printf("thr: %s\n", strerror(e));
        mutex_force_unlock(&mutex, &mattr);
        e = pthread_mutex_unlock(&mutex);
        printf("thr: %s\n", strerror(e));
        if (e != 0) pthread_exit(NULL);
        e = pthread_mutex_lock(&mutex);
        printf("thr: %s\n", strerror(e));
      }
    pthread_exit(NULL);
  }


void * thread_deadtest(void *d)
  {
    int e;
    e = pthread_mutex_lock(&mutex);
    printf("thr2: %s\n", strerror(e));
    e = pthread_mutex_lock(&mutex);
    printf("thr2: %s\n", strerror(e));
    pthread_exit(NULL);
  }


int main(void)
  {
    /* Setup */
    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ERRORCHECK);
    //pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_NORMAL);
    pthread_mutex_init(&mutex, &mattr);

    /* Test */
    pthread_create(&thread1, NULL, &thread, NULL);
    pthread_join(thread1, NULL);
    if (pthread_kill(thread1, 0) != 0) printf("Thread 1 has died.\n");
    pthread_create(&thread2, NULL, &thread, NULL);
    pthread_join(thread2, NULL);
    pthread_create(&thread3, NULL, &thread_deadtest, NULL);
    pthread_join(thread3, NULL);
    return(0);
  }

现在当这个程序运行时,我得到以下输出:

Thread 1 has died.
thr: Device busy
mfu: Device busy
mfu: No error: 0
thr: Operation not permitted
thr2: No error: 0
thr2: Resource deadlock avoided

现在我知道这已经被问过很多次了,但是有没有办法强制解锁互斥锁?似乎该实现只允许互斥锁被锁定它的线程解锁,因为它似乎在主动检查,即使是普通的互斥锁类型。

我为什么要这样做?它与编写一个防弹网络服务器有关,该服务器能够从大多数错误中恢复,包括线程意外终止的错误。在这一点上,我看不到从与锁定它的线程不同的线程中解锁互斥锁的方法。所以我看到它的方式是我有几个选择:

  1. 放弃互斥并创建一个新的。这是不受欢迎的选项,因为它会造成内存泄漏。
  2. 关闭所有网络端口并重新启动服务器。
  3. 进入内核内部并在那里释放互斥锁,绕过错误检查。

我已经问过这个before 但是,绝对想要这个功能的权力,他们不会拒绝回答(我已经尝试过),所以我有点坚持这个。我不是这样设计的,我真的很想射杀这样做的人,但这也不是一个选择。

在有人说什么之前,我对 pthread_kill 的使用在 POSIX 下是合法的...我检查了。

我忘了说,这是我们正在使用的 FreeBSD 9.3。

【问题讨论】:

  • 它必须是pthread_mutex_t 本身吗?在此基础上编写一个可以从其他线程释放的自己的锁并不难。
  • 不一定。我只需要能够解锁被另一个线程锁定的东西。

标签: c multithreading pthreads freebsd


【解决方案1】:

使用robust mutex,如果锁定线程终止,则使用pthread_mutex_consistent() 修复互斥锁。

如果互斥锁是一个处于不一致状态的健壮互斥锁,则 pthread_mutex_consistent() 函数可用于标记状态 由 mutex 引用的 mutex 保护再次一致。

如果健壮互斥锁的所有者在持有互斥锁时终止,则 互斥量变得不一致,下一个获取互斥量的线程 lock 应通过返回值 [EOWNERDEAD] 通知状态。 在这种情况下,互斥锁不会再次变得正常可用,直到 状态被标记为一致。

如果获取互斥锁的线程返回值 [EOWNERDEAD] 在调用之前终止 pthread_mutex_consistent() 或 pthread_mutex_unlock(),下一个线程 获得互斥锁的应通知其状态 互斥量按返回值 [EOWNERDEAD]。

【讨论】:

  • 请注意,这不仅仅是“用pthread_mutex_consistent()修复互斥锁”的问题——这个机制的重点是接收EOWNERDEAD的线程应该分支到一条缓慢的路径,仔细检查受互斥锁保护的共享状态,并修复死线程留下的任何不一致。只有这样它才能调用pthread_mutex_consistent(),然后它才能在持有锁的情况下正常继续。
  • 就像一个审计程序。我会使用这种机制,但 FreeBSD 还不支持它。不在 9.3 中,甚至在 10.1 中。
【解决方案2】:

好吧,你不能用普通的 pthread 互斥锁做你所要求的事情,因为正如你所说,你只能从锁定它的线程中解锁一个互斥锁。

你可以做的是包装一个互斥锁的锁定/解锁,这样你就有一个 pthread 取消处理程序,如果线程终止,它会解锁互斥锁。给你一个想法:

void cancel_unlock_handler(void *p)
{
    pthread_mutex_unlock(p);
}

int my_pthread_mutex_lock(pthread_mutex_t *m)
{
    int rc;
    pthread_cleanup_push(cancel_unlock_handler, m);
    rc = pthread_mutex_lock(&m);
    if (rc != 0) {
        pthread_cleanup_pop(0);   
    }
    return rc;
}       

int my_pthread_mutex_unlock(pthread_mutex_t *m)
{
    pthread_cleanup_pop(0);
    return pthread_mutex_unlock(&m);
}

现在您需要使用 my_pthread_mutex_lock/my_pthread_mutex_unlock 而不是 pthread 锁定/解锁函数。

现在,线程并没有真正“意外”终止,它调用 pthread_exit 或结束,或者你 pthread_kill 它,在这种情况下,上面的就足够了(还要注意线程只在某些取消点退出,所以没有竞争条件,例如在推送清理处理程序和锁定互斥体之间),但逻辑错误或未定义的行为可能会留下影响整个过程的错误状态,您最好重新启动整个过程。

【讨论】:

    【解决方案3】:

    我想出了一个可行的方法来处理这种情况。正如我之前提到的,FreeBSD 不支持强大的互斥锁,因此该选项已被淘汰。还有一个线程锁定了一个互斥锁,它不能以任何方式解锁。

    所以我为解决这个问题所做的就是放弃互斥体并将其指针放在一个列表中。由于锁包装器代码使用 pthread_mutex_trylock,然后如果它失败则放弃 CPU,因此没有线程可以在等待永久锁定的互斥锁时卡住。在健壮互斥体的情况下,锁定互斥体的线程将能够恢复它,如果它获得 EOWNERDEAD 作为返回码。

    这里有一些定义:

    /* Checks to see if we have access to robust mutexes. */
    #ifndef PTHREAD_MUTEX_ROBUST
    #define TSRA__ALTERNATE
    #define TSRA_MAX_MUTEXABANDON   TSRA_MAX_MUTEX * 4
    #endif
    
    /* Mutex: Mutex Data Table Datatype */
    typedef struct mutex_lock_table_tag__ mutexlock_t;
    struct mutex_lock_table_tag__
      {
        pthread_mutex_t *mutex;     /* PThread Mutex */
        tsra_daclbk audcallbk;      /* Audit Callback Function Pointer */
        tsra_daclbk reicallbk;      /* Reinit Callback Function Pointer */
        int acbkstat;               /* Audit Callback Status */
        int rcbkstat;               /* Reinit Callback Status */
        pthread_t owner;            /* Owner TID */
        #ifdef TSRA__OVERRIDE
        tsra_clnup_t *cleanup;      /* PThread Cleanup */
        #endif
      };
    
    /* ******** ******** Global Variables */
    
    pthread_rwlock_t tab_lock;              /* RW lock for mutex table */
    pthread_mutexattr_t mtx_attrib;         /* Mutex attributes */
    mutexlock_t *mutex_table;               /* Mutex Table */
    int tabsizeentry;                       /* Table Size (Entries) */
    int tabsizebyte;                        /* Table Size (Bytes) */
    int initialized = 0;                    /* Modules Initialized 0=no, 1=yes */
    #ifdef TSRA__ALTERNATE
    pthread_mutex_t *mutex_abandon[TSRA_MAX_MUTEXABANDON];
    pthread_mutex_t mtx_abandon;            /* Abandoned Mutex Lock */
    int mtx_abandon_count;                  /* Abandoned Mutex Count */
    int mtx_abandon_init = 0;               /* Initialization Flag */
    #endif
    pthread_mutex_t mtx_recover;            /* Mutex Recovery Lock */
    

    这里有一些锁恢复的代码:

    /* Attempts to recover a broken mutex. */
    int tsra_mutex_recover(int lockid, pthread_t tid)
      {
        int result;
    
        /* Check Prerequisites */
        if (initialized == 0) return(EDOOFUS);
        if (lockid < 0 || lockid >= tabsizeentry) return(EINVAL);
    
        /* Check Mutex Owner */
        result = pthread_equal(tid, mutex_table[lockid].owner);
        if (result != 0) return(0);
    
        /* Lock Recovery Mutex */
        result = pthread_mutex_lock(&mtx_recover);
        if (result != 0) return(result);
    
        /* Check Mutex Owner, Again */
        result = pthread_equal(tid, mutex_table[lockid].owner);
        if (result != 0)
          {
            pthread_mutex_unlock(&mtx_recover);
            return(0);
          }
    
        /* Unless the system supports robust mutexes, there is
           really no way to recover a mutex that is being held
           by a thread that has terminated.  At least in FreeBSD,
           trying to destory a mutex that is held will result
           in EBUSY.  Trying to overwrite a held mutex results
           in a memory fault and core dump.  The only way to
           recover is to abandon the mutex and create a new one. */
        #ifdef TSRA__ALTERNATE      /* Abandon Mutex */
        pthread_mutex_t *ptr;
    
        /* Too many abandoned mutexes? */
        if (mtx_abandon_count >= TSRA_MAX_MUTEXABANDON)
          {
            result = TSRA_PROGRAM_ABORT;
            goto error_1;
          }
    
        /* Get a read lock on the mutex table. */
        result = pthread_rwlock_rdlock(&tab_lock);
        if (result != 0) goto error_1;
    
        /* Perform associated data audit. */
        if (mutex_table[lockid].acbkstat != 0)
          {
            result = mutex_table[lockid].audcallbk();
            if (result != 0)
              {
                result = TSRA_PROGRAM_ABORT;
                goto error_2;
              }
          }
    
        /* Allocate New Mutex */
        ptr = malloc(sizeof(pthread_mutex_t));
        if (ptr == NULL)
          {
            result = errno;
            goto error_2;
          }
    
        /* Init new mutex and abandon the old one. */
        result = pthread_mutex_init(ptr, &mtx_attrib);
        if (result != 0) goto error_3;
        mutex_abandon[mtx_abandon_count] = mutex_table[lockid].mutex;
        mutex_abandon[mtx_abandon_count] = mutex_table[lockid].mutex;
        mtx_abandon_count++;
        mutex_table[lockid].mutex = ptr;
    
        #else       /* Recover Mutex */
    
        /* Try locking the mutex and see what we get. */
        result = pthread_mutex_trylock(mutex_table[lockid].mutex);
        switch (result)
          {
            case 0:                 /* No error, unlock and return */
              pthread_unlock_mutex(mutex_table[lockid].mutex);
              return(0);
              break;
            case EBUSY:             /* No error, return */
              return(0);
              break;
            case EOWNERDEAD:        /* Error, try to recover mutex. */
              if (mutex_table[lockid].acbkstat != 0)
                  {
                    result = mutex_table[lockid].audcallbk();
                    if (result != 0)
                      {
                        if (mutex_table[lockid].rcbkstat != 0)
                            {
                              result = mutex_table[lockid].reicallbk();
                              if (result != 0)
                                {
                                  result = TSRA_PROGRAM_ABORT;
                                  goto error_2;
                                }
                            }
                          else
                            {
                              result = TSRA_PROGRAM_ABORT;
                              goto error_2;
                            }
                      }
                  }
                else
                  {
                    result = TSRA_PROGRAM_ABORT;
                    goto error_2;
                  }
              break;
            case EDEADLK:           /* Error, deadlock avoided, abort */
            case ENOTRECOVERABLE:   /* Error, recovery failed, abort */
              /* NOTE: We shouldn't get this, but if we do... */
              abort();
              break;
            default:
              /* Ambiguous situation, best to abort. */
              abort();
              break;
          }
        pthread_mutex_consistant(mutex_table[lockid].mutex);
        pthread_mutex_unlock(mutex_table[lockid].mutex);
        #endif
    
        /* Housekeeping */
        mutex_table[lockid].owner = pthread_self();
        pthread_mutex_unlock(&mtx_recover);
    
        /* Return */
        return(0);
    
        /* We only get here on errors. */
        #ifdef TSRA__ALTERNATE
        error_3:
        free(ptr);
        error_2:
        pthread_rwlock_unlock(&tab_lock);
        #else
        error_2:
        pthread_mutex_unlock(mutex_table[lockid].mutex);
        #endif
        error_1:
        pthread_mutex_unlock(&mtx_recover);
        return(result);
      }
    

    因为 FreeBSD 是一个像 Linux 一样不断发展的操作系统,所以我已经做出了规定,以允许将来使用强大的互斥锁。由于没有健壮的互斥体,因此如果支持健壮的互斥体,则确实无法进行增强的错误检查。

    对于健壮的互斥体,执行增强的错误检查以验证是否需要恢复互斥体。对于不支持健壮互斥体的系统,我们必须信任调用者来验证有问题的互斥体是否需要恢复。此外,还有一些检查以确保只有一个线程执行恢复。在互斥体上阻塞的所有其他线程都被阻塞。我已经考虑过如何向其他线程发出恢复正在进行的信号,因此例程的这方面仍然需要工作。在恢复情况下,我正在考虑比较指针值以查看互斥锁是否被替换。

    在这两种情况下,审计例程都可以设置为回调函数。审计例程的目的是验证和纠正受保护数据中的任何数据差异。如果审计未能更正数据,则调用另一个回调例程,即数据重新初始化例程。这样做的目的是重新初始化受互斥锁保护的数据。如果失败,则调用 abort() 以终止程序执行并删除核心文件以进行调试。

    对于放弃互斥的情况,指针并没有被丢弃,而是放在一个列表中。如果放弃了太多互斥锁,则程序将中止。如上所述,在互斥锁例程中,使用 pthread_mutex_trylock 代替 pthread_mutex_lock。这样,就不会在死互斥体上永久阻塞任何线程。所以一旦在互斥表中切换指针指向新的互斥量,所有等待互斥量的线程都会立即切换到新的互斥量。

    我确信此代码中存在错误/错误,但这是一项正在进行的工作。虽然还没有完全完成和调试,但我觉得这里有足够的理由来回答这个问题。

    【讨论】:

      【解决方案4】:

      您可能知道,锁定互斥锁的线程拥有该资源的唯一所有权。所以它拥有解锁它的所有权利。至少到目前为止,没有任何方法可以强制线程放弃其资源,而无需像在代码中所做的那样绕道而行。

      但是,这将是我的方法。

      拥有一个拥有互斥锁的线程,称为资源线程。确保该线程接收并响应其他工作线程的事件。

      当一个工作线程想要进入临界区时,它会向资源线程注册以代表它锁定一个互斥锁。完成后,工作线程假定它拥有对临界区的独占访问权。该假设是有效的,因为任何其他需要访问临界区的工作线程都必须经过相同的步骤。

      现在假设,有另一个线程想强制前一个工作线程解锁,然后他可以进行特殊调用,可能是一个标志或具有高优先级的线程来授予访问权限。资源线程在比较请求线程的标志/优先级后,将为请求线程解锁互斥锁并再次锁定。

      我不完全确定您的用例,但只知道我的 2 美分。如果你喜欢它,不要忘记投票给我的答案。

      【讨论】:

      • 虽然这是一个有趣的答案,但在我现在所处的位置上它太复杂了,所以我没有使用它。为创意投票。
      • 这违背了互斥锁的全部目的,因为 1) 现在有一个线程获取互斥锁。如果两个线程请求互斥锁怎么办? ==> 互斥锁线程阻塞,直到互斥锁被解锁,这不会发生,因为这是应该解锁互斥锁的线程。 2)获得锁的线程可能随时将其丢失给另一个(更高优先级)线程。这意味着您没有互斥,因为在您的计算过程中,另一个线程可以随时进入。
      • 我猜你没有想象出答案。请求线程将“永远不会”获得互斥锁。有一个特殊线程(又名资源线程)充当锁定/解锁的代理。代表请求线程。当第二个线程请求资源线程为其锁定时,代理线程不会返回肯定的响应。假设 try_lock 是一种伪代码。在这种情况下,(2) 不会发生。
      • "资源线程在比较请求线程的标志/优先级后,将为请求线程解锁互斥锁并再次锁定。" ==> 互斥锁在“所有者”(最初获得锁请求的线程)不知情的情况下被解锁。
      • 这个解决方案真的只是一个很好的说法:自己实现互斥锁。如果保证在不询问资源线程的情况下没有其他线程将访问所述资源,那么资源线程为什么需要锁定资源?您也可以给资源线程一个标志RESOURCE_USED_BY = [id of thread currently using the resource, or NULL if none] 并让资源线程在内部使用它。但如果你想以一种可行的方式实现它,你只需要自己实现互斥锁。
      【解决方案5】:

      您可以使用exec family 中的函数仅重新启动带有崩溃线程的进程以更改进程映像。我认为重新加载进程会比重新启动服务器更快。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-12-25
        • 1970-01-01
        • 2021-12-23
        • 2023-03-21
        • 1970-01-01
        • 2018-05-23
        • 2011-05-20
        • 2020-12-07
        相关资源
        最近更新 更多