ubuntu:sem_timedwait 没有醒来(C)
Posted
技术标签:
【中文标题】ubuntu:sem_timedwait 没有醒来(C)【英文标题】:ubuntu: sem_timedwait not waking (C) 【发布时间】:2010-05-28 11:33:58 【问题描述】:我有 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,但没有任何区别。
感谢您的任何建议, 贾尔斯
【问题讨论】:
是否有任何 ubuntu 用户可以尝试在系统上编译和运行此代码?它应该很容易用 -lpthread 编译,我真的很想知道它是否显示相同的行为。不要忘记,在此行为发生之前,您可能需要让它运行至少四个小时!非常感谢任何可以做到这一点的人。对此,我真的非常感激。贾尔斯 我在原始描述中忘记提到的一件事是,我也尝试过使用三个单独的程序进行类似的测试,通过共享内存进行通信,而不是同一个程序中的三个线程。这更接近于我的真实世界应用程序。结果和输出是一样的。问题似乎出在信号量上(尤其是 sem_timedwait 调用),而不是与 pthread 有任何关系。 也许您可以将此更新放在问题的正文中。这会将您的问题“撞”回列表的 tp,并且可能会引起更多关注。 非常感谢您提供的有用提示。我现在已经这样做了。 刚刚编辑了这个以纠正几个错别字。我还想看看有没有其他人在 18 个月后有任何进一步的想法,因为我还没有深入了解它。 【参考方案1】:(很抱歉给出第二个答案,但这个答案太混乱了,无法通过编辑来清理)
我认为,答案已经在该问题的原始帖子中。
所以,我的问题是为什么 sem3 超时,即使信号量有 被触发并且值为 显然1?我从没想过会看到 输出中的第 08 行。如果超时 (因为,假设线程 2 已崩溃或 花费的时间太长),该值应该 为 0。为什么它对 3 有效 或 4 小时前进入 这个状态?
所以场景是:
-
线程 2 耗时过长
sem3 在
sem_timedwait
中超时
线程 3 被取消调度或其他
它需要到达sem_getvalue
线程 2 唤醒并执行
sem_post
sem3
线程 3 发出其sem_getvalue
并看到一个 1
线程 3 分支进入错误
分支并且不做它的sem_post
在sem1
这种竞争条件很难触发,基本上你必须在一个线程在等待信号量时遇到问题的微小时间窗口,然后使用sem_getvalue
读取信号量。这种情况的发生很大程度上取决于环境(系统类型、内核数量、负载、IO 中断),所以这就解释了为什么它只在几个小时后才会发生,如果根本没有的话。
控制流依赖于sem_getvalue
通常是个坏主意。对sem_t
的唯一原子非阻塞访问是通过sem_post
和sem_trywait
。
所以问题中的这个示例代码具有竞争条件。这并不意味着 gillez 的原始问题代码确实具有相同的竞争条件。也许这个例子太简单了,对他来说仍然是同样的现象。
我的猜测是,在他最初的问题中,有一个 unprotected sem_wait
。那是一个sem_wait
,它只检查它的返回值,而不是在它失败的情况下检查errno
。如果进程有一些 IO,EINTR
s 会很自然地出现在 sem_wait
上。如果遇到EINTR
,您只需执行do - while
并检查和重置errno
。
【讨论】:
您好 Jens,感谢您的全面回答。我很欣赏你对此的想法。我理解你的回答,但仍然不认为它解释了我看到的行为(由于字符限制,在下一条评论中继续......) 一旦失败,每次循环都会失败。每次,sem_getvalue 返回的值都是 1。我很欣赏这是在调用 sem_timedwait 失败之后完成的,因此线程二可能已经唤醒并在其间执行 sem_post。所以我可以理解这种情况是如何偶尔发生的。但是在它失败一次之后,它每次在循环中都会显示相同的行为。这是否意味着您场景中的第四步每次都在这个小窗口中发生?在 sem_timedwait 失败后,来自 sem_getvalue 的值是 always 1. 另外,超时设置为相当大的间隔(半秒)。我真的不明白为什么线程 2 会花费这么长时间(步骤 1)以至于线程 3 超时,除非系统真的很忙。除非它实际上没有超时,而是由于中断而失败,在这种情况下,它的失败速度可能比每秒 1/2 快得多,但如果是这种情况,为什么 strerror 输出会显示“连接超时”而不是“打断”?我会尝试将超时时间增加到 10 秒左右。我还会在调用 sem_timedout 之前使用 errno 调用 strerror,以防 errno 发生变化。 一旦程序错过了与另一个线程的 post 相对应的等待,信号量可能永远不会再次下降到 0。它总是 1 或 2。所以不,每次都不是相同的行为,你的(隐式)不变量不再成立。对于超时值,0.5 秒不算多。如果您的系统上发生了一些繁重的事情,那么线程很可能会在这段时间内被取消调度。甚至还有另一个线程可能会被取消调度,即在 gettimeofday 之后。我会循环几秒钟,用 printf 发出警告。 如果信号量值为 1 或 2 而不是 0,则下一个 sem_timedwait 将立即返回而不会超时。行为与此相反。【参考方案2】:问题似乎来自于传递了一个无效的超时参数。
至少在我的机器上,第一次失败不是 ETIMEDOUT 而是:
!!!!!! sem2 sem_timedwait 失败:参数无效,val 现在为 0
现在,如果我写:
if (ts.tv_nsec >= 1000000)
(注意添加=)然后它工作正常。这是另一个问题,为什么信号量的状态(可能)被削弱了,以至于它在随后的尝试中超时,或者只是在直接的 sem_wait 上永远阻塞。看起来像是 libc 或内核中的错误。
【讨论】:
啊,我明白了,所以当 tv_nsec 正好是 1000000 或者当前时间正好是半秒时它会失败。这是一种罕见的情况,但最终总会发生。我还没有确认这可以在我的系统上修复它,但这是一个有意义的答案。谢谢。 至于为什么无效参数会弄乱信号量,正如您所说,它看起来像 libc 或内核中的错误。但是,如果我们确保论点永远不会无效,那么一切都应该没问题。谢谢。 虽然我没有真正验证这一点,因为我不再有一个方便的 ubuntu 系统,但这是一个令人信服的解释。也可以解释为什么它可以在其他风格的 Linux(Redhat 或 Fedora,甚至可能只是不同版本的 libc)中工作 - 他们仍然会偶尔得到无效的参数,但它不会弄乱信号量,所以下次循环就好了。由于我对这个解释感到相当满意,我将接受这个答案。谢谢。【参考方案3】:这很有趣。虽然我还没有找到错误的来源,但(仍在寻找)我已经在运行 Linux 2.6.34 的 Ubuntu 9.04 上验证了这一点。
【讨论】:
优秀。非常感谢您的尝试。老实说,我认为现在再回复已经太晚了,我的问题已经在 *** 的积压中丢失了!很高兴听到您可以看到相同的行为,这不仅仅是我系统上的一些奇怪设置。很高兴找到它的原因 - 希望......【参考方案4】:不要责怪 ubuntu 或任何其他发行版 :-) 这里当然更重要的是您使用的 gcc 版本,32 位或 64 位等,您的系统有多少内核。所以请提供更多信息。但是通过您的代码,我发现了几个可能会给您带来意想不到的行为的地方:
从头开始,投射int
在void*
来回,你是
找麻烦。使用uintptr_t
如果你必须这样做,但在这里你
没有借口只是通过真实
指向值的指针。 &(int) 1
和一些更理智的演员可以为 C99 解决问题。
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_timedwait
和
sem_getvalue
。基本上你不能
从中推断出任何东西,变量
只是偶然的价值
你观察到的。不要由此得出结论。
【讨论】:
不安全中断是什么意思?这看起来不像 -EINTR 问题 @shodanex:你为什么不能说它没有?我们只是没有信息来排除任何此类事情。线程可以在需要打印时获得 IO 中断,它们被重新安排或其他。 sem 接口非常低级,并且在编写此代码时,例如,在单核或多核系统上执行时,它的行为很可能会非常不同。我认为应该先整理代码,然后才能说一件事。正如它在这里展示的那样,它只是糟糕的风格和不可移植的。 因为打印的错误信息不是-EINTR。当不使用定时等待时,它也会挂起。除非内核行为不正确,否则 sem_* 操作在单核或多核上的行为不应有所不同。因为依赖于 sem 操作的特定顺序而起作用的程序当然会表现不同。让我们指出程序依赖于特定调度来引发所描述的行为的地方。值可能在 sem_timedwait 和 sem_getvalue 之间变化的事实并不能解释观察到的行为。 没关系,它的行为不应该有所不同,所以我猜这个错误就在那里,即使你认为它工作正常。调度、排序、并行执行只是更容易触发竞争条件。关于 sem_getvalue 的问题,至少在此处的版本中,控制流取决于它。使 sem_post 对信号量的值无条件。 您好 Jens 和 Shodanex,感谢 cmets。关于 Jens,前两点,无论您是否认为这是不好的做法,它们都不是问题的一部分(如果在整数中传递指针时出现问题,那么在我们看到错误之前很久就会导致问题这里)。第三点,正如 shodanex 所说,用 strerror 打印的错误(即来自 errno)是超时,而不是中断。【参考方案5】:我不知道出了什么问题,而且我的代码看起来也不错。您可以通过以下方式获取更多信息。
使用不同的超时时间,无论是更短的还是更长的,看看您的问题是否仍然存在。 使用非定时版本,看看程序是否挂起。 尝试修改内核调度程序的行为,例如使用内核命令行参数,或使用 procfs 或 sysfs。正如 Jens 所指出的,有两个种族:
第一个是在调用 sem_timedwait 之后评估信号量的值。 这不会改变与信号量有关的控制流。无论线程是否超时,它仍然会通过“我应该触发下一个线程”块。
第二个是在“我应该唤醒下一个线程”部分。我们可以有以下事件:
-
线程 n 调用
sem_getvalue(trigger)
并获得 1
线程 n+1 从 sem_timedwait
返回,信号量变为 0
线程 n 决定不发布,信号量保持为 0
现在,我看不出这会如何触发观察到的行为。毕竟,既然线程 n+1 无论如何都会被唤醒,它会反过来唤醒线程 n+2,这会唤醒线程 n 等等......
虽然可能会出现故障,但我看不出这会如何导致线程系统超时。
【讨论】:
非常感谢您的回答。这当然是一个奇怪的问题,正如我所说,它在 Redhat 和 Fedora 上运行良好。我会尝试你的三个建议,看看它们是否能说明情况。 与此同时,我仍然希望得到 Ubuntu 专家的其他更明确的答案... 这些建议结果的更新。在没有超时的情况下,程序会在几分钟后很快挂起。在短暂的超时(usleep 40,而不是 40000)下,程序也很快挂起,大约 10 分钟后。我用更长的超时时间尝试过一次,但它最终还是挂了。基本上它运行得越快,循环次数越多,程序挂起的可能性就越大。 很好,现在你的复制速度更快了:) 是的,确实如此。它使测试变得更加容易。但仍然没有找到解决方案。【参考方案6】:我在我的 Ubuntu 10.04 x86_64 Core i7 机器上试了一下这个程序。
当使用usleep(40000)运行时,程序正常运行了半小时或一些无聊的东西。
当使用 usleep(40) 运行时,程序又运行了半小时,也许更长时间,然后我的机器就死机了。 X死了。 Control+alt+F1-7 死了。我无法 SSH。(遗憾的是,这个愚蠢的 Apple 键盘没有 sysrq 键。我喜欢在上面打字,但我肯定不需要 f13、f14 或 f15。我会做可怕的事情来获得正确的 sysrq 密钥。)
绝对是最好的:我的日志中的 NOTHING 告诉我发生了什么。
$ 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 游戏(由 *** 用户发布,寻求反馈,有趣的转移:)——所以 jvm 可能负责挠痒痒以冻结我的机器.
【讨论】:
以上是关于ubuntu:sem_timedwait 没有醒来(C)的主要内容,如果未能解决你的问题,请参考以下文章
Linux C语言 信号量 sem_init() sem_wait() sem_timedwait() sem_post() sem_destroy()