3

我有 3 个需要同步的进程。进程一做某事然后唤醒进程二并休眠,它做一些事然后唤醒进程三并休眠,它做某事并唤醒进程一并休眠。整个循环的定时运行在 25hz 左右(由于在我的“真实”应用程序中触发进程 2 之前,外部同步到进程 1)。我使用 sem_post 触发(唤醒)每个进程,并使用 sem_timedwait() 等待触发。

这一切都成功地工作了几个小时。然而,在某个随机时间(通常在两到四个小时之后),其中一个进程在 sem_timedwait() 中开始超时,即使我确信信号量是由 sem_post() 触发的。为了证明这一点,我什至在超时后立即使用 sem_getvalue() ,值为 1,因此应该触发了 timedwait。

请看以下代码:

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <semaphore.h>

sem_t trigger_sem1, trigger_sem2, trigger_sem3;

// The main thread process.  Called three times with a different num arg - 1, 2 or 3.
void *thread(void *arg)
{
  int num = (int) arg;
  sem_t *wait, *trigger;
  int val, retval;
  struct timespec ts;
  struct timeval tv;

  switch (num)
    {
      case 1:
        wait = &trigger_sem1;
        trigger = &trigger_sem2;
        break;
      case 2:
        wait = &trigger_sem2;
        trigger = &trigger_sem3;
        break;
      case 3:
        wait = &trigger_sem3;
        trigger = &trigger_sem1;
        break;
    }

  while (1)
    {
      // The first thread delays by 40ms to time the whole loop.  
      // This is an external sync in the real app.
      if (num == 1)   
        usleep(40000);

      // print sem value before we wait.  If this is 1, sem_timedwait() will
      // return immediately, otherwise it will block until sem_post() is called on this sem. 
      sem_getvalue(wait, &val);
      printf("sem%d wait sync sem%d. val before %d\n", num, num, val);

          // get current time and add half a second for timeout.
      gettimeofday(&tv, NULL);
      ts.tv_sec = tv.tv_sec;
      ts.tv_nsec = (tv.tv_usec + 500000);    // add half a second
      if (ts.tv_nsec > 1000000)
        {
          ts.tv_sec++;
          ts.tv_nsec -= 1000000;
        }
      ts.tv_nsec *= 1000;    /* convert to nanosecs */

      retval = sem_timedwait(wait, &ts);
      if (retval == -1)
        {
          // timed out.  Print value of sem now.  This should be 0, otherwise sem_timedwait
          // would have woken before timeout (unless the sem_post happened between the 
          // timeout and this call to sem_getvalue).
          sem_getvalue(wait, &val);
          printf("!!!!!!    sem%d sem_timedwait failed: %s, val now %d\n", 
            num, strerror(errno), val);
        }
      else
        printf("sem%d wakeup.\n", num);

        // get value of semaphore to trigger.  If it's 1, don't post as it has already been 
        // triggered and sem_timedwait on this sem *should* not block.
      sem_getvalue(trigger, &val);
      if (val <= 0)
        {
          printf("sem%d send sync sem%d. val before %d\n", num, (num == 3 ? 1 : num+1), val);
          sem_post(trigger);
        }
      else
        printf("!! sem%d not sending sync, val %d\n", num, val);
    }
}



int main(int argc, char *argv[])
{
  pthread_t t1, t2, t3;

   // create semaphores.  val of sem1 is 1 to trigger straight away and start the whole ball rolling.
  if (sem_init(&trigger_sem1, 0, 1) == -1)
    perror("Error creating trigger_listman semaphore");
  if (sem_init(&trigger_sem2, 0, 0) == -1)
    perror("Error creating trigger_comms semaphore");
  if (sem_init(&trigger_sem3, 0, 0) == -1)
    perror("Error creating trigger_vws semaphore");

  pthread_create(&t1, NULL, thread, (void *) 1);
  pthread_create(&t2, NULL, thread, (void *) 2);
  pthread_create(&t3, NULL, thread, (void *) 3);

  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
}

当程序正确运行时(开始时以及随机但很长时间之后),将打印以下输出。sem1 的值在 thread1 等待之前始终为 1,因为它休眠了 40 毫秒,此时 sem3 已触发它,因此它立即唤醒。其他两个线程等待,直到从前一个线程接收到信号量。

[...]
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
sem2 wakeup.
sem2 send sync sem3. val before 0
sem2 wait sync sem2. val before 0
sem3 wakeup.
sem3 send sync sem1. val before 0
sem3 wait sync sem3. val before 0
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
[...]

但是,几个小时后,其中一个线程开始超时。我可以从输出中看到信号量正在被触发,当我在超时后打印该值时,它是 1。所以 sem_timedwait 应该在超时之前就醒了。我永远不会期望信号量的值在超时后为 1,除非在非常罕见的情况下(几乎肯定不会,但这是可能的),即在超时之后但在我调用 sem_getvalue 之前发生触发器。

此外,一旦它开始失败,该信号量上的每个 sem_timedwait() 也会以同样的方式失败。请参阅以下输出,我已对其进行了行编号:

01  sem3 wait sync sem3. val before 0
02  sem1 wakeup.
03  sem1 send sync sem2. val before 0
04  sem2 wakeup.
05  sem2 send sync sem3. val before 0
06  sem2 wait sync sem2. val before 0
07  sem1 wait sync sem1. val before 0
08  !!!!!!    sem3 sem_timedwait failed: Connection timed out, val now 1
09  sem3 send sync sem1. val before 0
10  sem3 wait sync sem3. val before 1
11  sem3 wakeup.
12  !! sem3 not sending sync, val 1
13  sem3 wait sync sem3. val before 0
14  sem1 wakeup.
[...]

在第 1 行,线程 3(我在 printf 中混淆地称为 sem3)等待 sem3 被触发。在第 5 行,thread2 为 sem3 调用 sem_post。但是,第 8 行显示 sem3 超时,但信号量的值为 1。thread3 然后触发 sem1 并再次等待 (10)。但是,因为值已经是 1,所以它会立即唤醒。它不会再次发送 sem1,因为这一切都是在将控制权交给 thread1 之前发生的,但是它会再次等待(val 现在为 0)并且 sem1 被唤醒。现在这会永远重复,sem3 总是超时并显示值为 1。

所以,我的问题是为什么 sem3 超时,即使信号量已被触发并且值显然是 1?我永远不会期望在输出中看到第 08 行。如果超时(因为线程 2 已经崩溃或耗时太长),该值应该为 0。为什么它在进入此状态之前先正常工作 3 或 4 小时?

我尝试过使用三个单独的程序进行类似的测试,通过共享内存进行通信,而不是同一个程序中的三个线程。这更接近于我的真实世界应用程序。结果和输出是一样的。问题确实出现在信号量(特别是 sem_timedwait 调用)中,而不是与 pthread 有任何关系。

我也尝试过更短和更长的延迟,以及完全消除延迟,结果与上述类似。没有任何延迟,它有时会在几分钟而不是几小时后开始产生错误。这当然意味着可以更快地重现问题。

这是使用带有内核 2.6.28 的 Ubuntu 9.4。相同的程序在 Redhat 和 Fedora 上运行正常,但我现在正在尝试移植到 Ubuntu。我也尝试过使用 Ubuntu 9.10,但没有任何区别。

感谢您的任何建议,贾尔斯

4

6 回答 6

2

(很抱歉给出第二个答案,但这个答案太混乱了,无法通过编辑来清理)

我认为,答案已经在该问题的原始帖子中。

所以,我的问题是为什么 sem3 超时,即使信号量已被触发并且值显然是 1?我永远不会期望在输出中看到第 08 行。如果超时(因为线程 2 已经崩溃或花费了太长时间),该值应该为 0。为什么它在进入此状态之前先正常工作 3 或 4 小时?

所以场景是:

  1. 线程 2 耗时太长
  2. sem3 超时sem_timedwait
  3. 线程 3 被取消调度或以任何方式到达sem_getvalue
  4. 线程 2 唤醒并 sem_post启动sem3
  5. 线程 3 发出它sem_getvalue 并看到 1
  6. 线程 3 分支到错误的分支并且不执行sem_postsem1

这种竞争条件很难触发,基本上你必须达到一个线程在等待信号量时遇到问题sem_getvalue的微小时间窗口,然后使用. 这种情况的发生很大程度上取决于环境(系统类型、内核数量、负载、IO 中断),所以这就解释了为什么它只在几个小时后才会发生,如果根本没有的话。

让控制流依赖于 asem_getvalue通常是一个坏主意。对 a的唯一原子非阻塞访问sem_t是通过sem_postsem_trywait

所以这个问题中的示例代码具有这种竞争条件。这并不意味着 gillez 的原始问题代码确实具有相同的竞争条件。也许这个例子太简单了,对他来说仍然表现出同样的现象。

我的猜测是,在他原来的问题中有一个不受保护 sem_wait的. 那sem_wait是只检查它的返回值,而不是errno在它失败的情况下。如果进程有一些 IO,EINTRs 确实会很自然地发生。sem_wait如果遇到 . do - while_errnoEINTR

于 2010-06-24T20:53:02.313 回答
1

This is very interesting. While I have not located the source of the error, (still looking) I have verified this on Ubuntu 9.04 running Linux 2.6.34.

于 2010-06-05T13:11:29.570 回答
1

问题似乎来自传递无效的超时参数。

至少在我的机器上,第一次失败不是 ETIMEDOUT 而是:

!!!!!!sem2 sem_timedwait 失败:参数无效,val 现在为 0

现在,如果我写:

  if (ts.tv_nsec >= 1000000)

(注意添加=)然后它工作正常。这是另一个问题,为什么信号量的状态(可能)被削弱了,以至于它在随后的尝试中超时,或者只是在直接的 sem_wait 上永远阻塞。看起来像 libc 或内核中的错误。

于 2011-12-07T14:51:33.927 回答
1

不要责怪 ubuntu 或任何其他发行版 :-) 这里当然更重要的是您使用的 gcc 版本,32 位或 64 位等,您的系统有多少内核。所以请提供更多信息。但是通过您的代码,我发现了几个可能会给您带来意想不到的行为的地方:

  • 它从一开始就开始,来回铸造,你正在寻找麻烦int 。 如果必须,void*请使用它,但在这里你没有理由只传递指向值的真实指针。并且一些更理智的铸造可以为 C99 解决问题。uintptr_t&(int){ 1 }

  • ts.tv_nsec = (tv.tv_usec + 500000)是另一个麻烦点。右侧的宽度可能与左侧的宽度不同。做

    ts.tv_nsec = tv.tv_usec;

    ts.tv_nsec += 500000;

  • sem 系列函数不是中断安全的。例如,此类中断可能由 IO 触发,因为您正在执行 printf 等。检查-1左右的返回值是不够的,但在这种情况下,您应该检查errno并决定是否要重试。然后,如果您想要精确,则必须重新计算剩余时间之类的东西。然后手册页sem_timedwait列出了可能发生的不同错误代码及其原因。

  • 您还可以从通过 获得的值得出结论sem_getvalue。在多线程/多进程/多处理器环境中,您的线程可能在从sem_timedwaitsem_getvalue. 基本上你不能从中推断出任何东西,变量只是偶然地在你观察到的值。不要由此得出结论。

于 2010-06-23T21:00:20.687 回答
1

我不知道出了什么问题,代码看起来也不错。您可以通过以下方式获得更多信息。

  • 使用不同的超时,无论是更短的还是更长的,看看你的问题是否仍然存在。
  • 使用非定时版本,看看程序是否挂起。
  • 尝试修改内核调度程序的行为,例如使用内核命令行参数,或使用 procfs 或 sysfs。

正如 Jens 所指出的,有两个种族:

第一个是在调用 sem_timedwait 之后评估信号量的值。这不会改变与信号量有关的控制流。无论线程是否超时,它仍然会通过“我应该触发下一个线程”块。

第二个是在“我应该唤醒下一个线程”部分。我们可以有以下事件:

  1. 线程 n 调用sem_getvalue(trigger)并获得 1
  2. 线程 n+1 从返回,sem_timedwait信号量变为 0
  3. 线程 n 决定不发布,信号量保持为 0

现在,我看不出这如何触发观察到的行为。毕竟,既然线程 n+1 无论如何都会被唤醒,它会反过来唤醒线程 n+2,这将唤醒线程 n 等等......

虽然可能会出现故障,但我看不出这会如何导致线程系统超时。

于 2010-05-29T20:08:09.640 回答
0

我在我的 Ubuntu 10.04 x86_64 Core i7 机器上试了一下这个程序。

当使用 usleep(40000) 运行时,程序可以正常运行半小时或一些无聊的东西。

当使用 usleep(40) 运行时,程序又运行了半个小时,也许更长时间,然后我的机器就死机了。X死了。Control+alt+F1-7 死了。我无法 SSH。(遗憾的是,这个愚蠢的 Apple 键盘没有 sysrq 键。我喜欢在上面打字,但我肯定不需要 f13、f14 或 f15。我会做可怕的事情来获得正确的 sysrq 密钥。)

绝对最好的:我的日志中没有任何内容告诉我发生了什么。

$ uname -a
Linux haig 2.6.32-22-generic #36-Ubuntu SMP Thu Jun 3 19:31:57 UTC 2010 x86_64 GNU/Linux

同时,我还在浏览器中玩 Java 游戏(由 stackoverflow 用户发布,寻找反馈,有趣的转移 :) - 所以 jvm 可能负责对某些东西进行搔痒以冻结我的机器。

于 2010-06-28T10:28:02.990 回答