强制解锁由不同线程锁定的互斥体

Force unlock a mutex that was locked by a different thread

考虑以下测试程序:

#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。

正如您可能知道的那样,锁定互斥锁的线程拥有该资源的唯一所有权。所以它拥有解锁它的所有权利。至少到现在为止,没有办法强制一个线程,放弃它的资源,而不必像您在代码中所做的那样绕圈子。

但是,这将是我的方法。

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

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

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

我不完全确定你的用例,但只是我的 2 美分。喜欢的话别忘了给我的答案投上一票哦

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

您可以做的是包装 locking/unlocking 互斥量,这样您就有一个 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 lock/unlock 函数。

现在,线程并没有真正终止 "unexpectedly",它要么调用 pthread_exit 要么结束,或者你 pthread_kill 它,在这种情况下以上就足够了(另请注意线程仅在某些取消点退出,因此不存在竞争条件 e.g.between 推动清理处理程序并锁定互斥体),但逻辑错误或未定义的行为可能会留下影响整个过程的错误状态,你最好过得更好重新开始整个过程​​。

使用 robust mutex, and if the locking thread dies, fix the mutex with pthread_mutex_consistent().

If mutex is a robust mutex in an inconsistent state, the pthread_mutex_consistent() function can be used to mark the state protected by the mutex referenced by mutex as consistent again.

If an owner of a robust mutex terminates while holding the mutex, the mutex becomes inconsistent and the next thread that acquires the mutex lock shall be notified of the state by the return value [EOWNERDEAD]. In this case, the mutex does not become normally usable again until the state is marked consistent.

If the thread which acquired the mutex lock with the return value [EOWNERDEAD] terminates before calling either pthread_mutex_consistent() or pthread_mutex_unlock(), the next thread that acquires the mutex lock shall be notified about the state of the mutex by the return value [EOWNERDEAD].

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

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

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

这里定义了一些东西:

/* 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() 以终止程序执行并删除核心文件以进行调试。

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

我确定此代码中有 bugs/errors,但这是一项正在进行的工作。虽然还没有完全完成和调试,但我觉得这里已经足够回答这个问题了。