强制解锁一个被其他线程锁定的互斥量

6
考虑以下测试程序:
#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 吗?编写一个自己的锁,可以从其他线程释放它,基于它并不难。 - deviantfan
1
不一定需要。我只需要能够解锁被另一个线程锁住的东西。 - Daniel Rudy
5个回答

7

使用健壮互斥锁,如果锁定线程死亡,请使用pthread_mutex_consistent()修复互斥锁。

如果互斥锁处于不一致状态且为健壮互斥锁,则可以使用pthread_mutex_consistent()函数将由互斥锁引用的受保护状态标记为再次一致。

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

如果使用返回值[EOWNERDEAD]获取互斥锁的线程在调用pthread_mutex_consistent()或pthread_mutex_unlock()之前终止,则下一个获取互斥锁的线程将通过返回值[EOWNERDEAD]被通知互斥锁的状态。


1
请注意,这不仅仅是“使用pthread_mutex_consistent()修复互斥锁”的问题 - 这种机制的重点在于接收到EOWNERDEAD的线程应该分支到一个慢路径,仔细检查由互斥锁保护的共享状态,并修复死线程留下的任何不一致。只有在这之后,它才能调用pthread_mutex_consistent(),然后可以像正常情况下一样持有锁并继续执行。 - caf
2
就像审计例程一样。我会使用这种机制,但是FreeBSD尚未支持它。无论是在9.3还是10.1版本都不支持。 - Daniel Rudy

1

好的,您不能使用普通的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 lock/unlock函数。
现在,线程不会真正意义上的“意外终止”,它要么调用pthread_exit结束,或者自然结束,或者被pthread_kill杀死,在这种情况下以上操作就足够了(还要注意,线程仅在某些取消点退出,因此没有竞争条件,例如在推送清理处理程序和锁定互斥量之间),但逻辑错误或未定义行为可能会留下影响整个进程的错误状态,最好重新启动整个进程。

1
我想出了一个可行的方法来处理这种情况。正如我之前提到的,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。这样,没有线程可以永久地被阻塞在死亡的互斥锁上。因此,一旦在互斥表中切换指针以指向新的互斥锁,所有等待该互斥锁的线程都将立即切换到新的互斥锁。
我确信这段代码中存在错误和漏洞,但它仍处于进行中的状态。虽然还没有完全完成和调试,但我认为这里已经足够回答这个问题。

0

你可能已经知道,锁定互斥量的线程拥有该资源的唯一所有权。因此,它有权解锁。至少到目前为止,没有办法强制一个线程放弃其资源,而不必像你在代码中所做的那样绕个大圈。

然而,这将是我的方法。

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

当工作线程想要进入关键部分时,它向资源线程注册以代表其锁定互斥量。完成后,工作线程假设它已经独占访问关键部分。这种假设是有效的,因为任何需要访问关键部分的其他工作线程都必须经过相同的步骤。

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

我不确定你的使用情况,但这是我的建议。如果你喜欢,请别忘了给我的答案投票。


1
虽然这是一个有趣的答案,但它涉及的内容已经超出了我现在的水平,所以我不会使用它。不过还是要点赞创意。 - Daniel Rudy
这完全违背了互斥锁的目的,因为:1)现在只有一个线程可以获取互斥锁。如果两个线程请求互斥锁会怎样?==> 互斥锁线程会阻塞直到互斥锁被解锁,但这不会发生,因为这是应该解锁互斥锁的线程。2)获取锁的线程可能随时失去它,让给另一个(更高优先级的)线程。这意味着你没有互斥性,因为在计算过程中任何时候都可能有另一个线程进来。 - Ivo
我猜你没有想象出答案。请求线程将“永远”不会获取互斥锁。有一个特殊的线程(也称为资源线程),作为代理来锁定/解锁,代表请求线程。当第二个线程请求资源线程代表其锁定时,代理线程将不会返回积极响应。假设是try_lock类似的伪代码。在这种情况下,(2)不会发生。 - kspviswa
资源线程将比较请求线程的标志/优先级,然后解锁互斥量并再次为请求线程加锁。这样"所有者"(最初获得锁定请求授权的线程)不会知道互斥量已被解锁。 - Ivo
这个解决方案实际上只是一种听起来不错的方式,即:自己实现互斥锁。如果可以保证没有其他线程会在未经资源线程允许的情况下访问该资源,那么资源线程为什么需要对该资源进行加锁呢?你同样可以给资源线程一个标志 RESOURCE_USED_BY = [当前正在使用该资源的线程的ID,如果没有则为NULL] 并让资源线程在内部使用它。但是,如果您想以这种方式实现它,并使其正常工作,那么您只需自己实现互斥锁即可。 - Ivo

0

您可以使用exec系列中的函数更改进程映像,仅重启崩溃线程所在的进程。我认为重新加载进程比重启服务器更快。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接