描述
我正在开发一个嵌入式Linux系统(使用内核3.4和bionic,类似于Android),它运行在多核ARMv7a SoC上。我们有一个用户空间线程,主要服务于内核产生的事件。这些事件是从IRQ生成的,并且必须由用户空间以非常低的延迟进行响应。
一个线程以SCHED_FIFO优先级0运行。它是系统中唯一的优先级0线程。 大致代码如下:
while (1)
{
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN|POLLRDNORM|POLLPRI;
int ret = poll(fds, 1, reallyLongTimeout);
FTRACE("poll() exit");
if (ret > 0)
{
// notify worker threads of pending events
}
}
通常我们能获得非常好的延迟(线程在毫秒内完成轮询并返回到 poll()),但是有时会出现延迟十几毫秒的情况,这会破坏一切。在全面跟踪后,我得出结论:延迟发生在中断触发后 poll() 系统调用返回之前,因为线程自己进入了睡眠状态。然后,在某个时间点上,由于某种未知的力量,线程被唤醒,一切继续进行。
我怀疑是其他中断造成的问题,但是在启用sched:,irq:,timer:*跟踪之后,我排除了这种可能性。我遇到了一些困难,在 ARM 内核中移植 syscalls:* 跟踪器。syscalls 跟踪器可以工作,但如果我还启用 sched:*,那么就会在 ring_buffer 代码内部出现各种错误。
在 fs/select.c 中插入一些自定义跟踪点后,我不得不不舒服地得出结论:在 sys_poll() 返回后,但尚未重新进入用户空间时,我的线程会进入睡眠状态。
以下是附带注释的跟踪结果:
<my thread>-915 [001] ...1 17.589394: custom: do_poll:786 - calling do_pollfd
<my thread>-915 [001] ...1 17.589399: custom: do_poll:794 - failed, no events
<my thread>-915 [001] ...1 17.589402: custom: do_poll:823 - going to sleep, count = 0, timed_out = 0
.... // everything going OK, then the IRQ happens, which runs a tasklet:
<random proc>-834 [000] d.h1 17.616541: irq_handler_entry: irq=17 name=hwblock handler=hw_block_process_irq
<random proc>-834 [000] d.h1 17.616569: softirq_raise: vec=6 [action=TASKLET]
<random proc>-834 [000] d.h1 17.616570: irq_handler_exit: irq=17 ret=handled
<random proc>-834 [000] ..s2 17.616627: softirq_entry: vec=6 [action=TASKLET]
.... // the tasklet signals the wait queue of the poll, which wakes up my thread:
<my thread>-915 [001] ...1 17.616827: custom: do_poll:826 - woke up, count = 0, timed_out = 0
<my thread>-915 [001] ...1 17.616833: custom: do_poll:772 - start of loop
<my thread>-915 [001] ...1 17.616840: custom: do_poll:786 - calling do_pollfd
<my thread>-915 [001] ...1 17.616852: custom: do_poll:788 - success, event!
<my thread>-915 [001] ...1 17.616859: custom: do_poll:810 - bailing, count = 1, timed_out = 0
<my thread>-915 [001] ...1 17.616862: custom: do_sys_poll:880 - before free_wait()
<my thread>-915 [001] ...1 17.616867: custom: do_sys_poll:882 - before __put_user()
<my thread>-915 [001] ...1 17.616872: custom: sys_poll:940 - do_sys_poll - exit
.... // the tasklet exits, and my thread appears to be about to be
<random proc>-834 [000] .Ns2 17.616922: softirq_exit: vec=6 [action=TASKLET]
.... // wait wait, why is my thread going back to sleep, and what was it doing for 75us?
<my thread>-915 [001] d..3 17.616947: sched_stat_wait: comm=<another thread> pid=1165 delay=1010000 [ns]
<my thread>-915 [001] ...2 17.616957: sched_switch: prev_comm=<my thread> prev_pid=915 prev_prio=0 prev_state=S ==> next_comm=<another thread> next_pid=1165 next_prio=120
.... // everything running on for 20ms as if nothing is wrong, then my thread suddenly gets woken up.
.... // nothing pid 947 is doing should have any effect on <my thread>
<random proc>-947 [000] d..4 17.636087: sched_wakeup: comm=<my thread> pid=915 prio=0 success=1 target_cpu=001
<random proc>-1208 [001] ...2 17.636212: sched_switch: prev_comm=<rancom proc> prev_pid=1208 prev_prio=120 prev_state=R ==> next_comm=<my thread> next_pid=915 next_prio=0
<my thread>-915 [001] ...1 17.636713: tracing_mark_write: poll() exit
My线程在某个地方变成了TASK_INTERRUPTIBLE
,然后自愿地走入调度程序,然后......20ms后醒来,原因不明。
这种情况似乎至少在一定程度上与时间有关,各种尝试观察它往往使它更难以再现。
问题
- 有什么想法是什么导致了这个问题?
- 有没有简单的方法找出我的线程睡眠在哪里?
- 有没有简单的方法找出我的线程为什么会醒来?
- 我考虑过如何适应
unwind_backtrace()
生成一个我可以塞进每个trace_sched_switch()
调用中的单个字符串,但那似乎有点令人生畏。还有什么其他类似的简单方法吗? - 有没有想法,为什么跟踪syscalls:*和sched:*会导致它在需要移动尾部的环形缓冲区代码中爆炸出未处理的页故障?看起来它在解除引用用户空间指针(基于数字相似性),但每次都是不同的。
我已经尝试并检查过的事情
这不是运行时间过长的正常IRQ,或者与禁用中断有关。使用irq:*跟踪显示了这一点。可能是某种TrustZone NMI,但我有点怀疑。
它不应该是任何RT限制/时间切片的产物,因为:
a) sched_rt_runtime_us = 10000和sched_rt_period_us = 10000
b) 线程具有相当低的工作周期(每秒小于30ms,在60-80个事件每秒)
这可能不是来自用户空间的跟踪或写入
/sys/kernel/debug/tracing/trace_marker
的人工产物——即使从内核中禁用跟踪(甚至编译掉),它也会发生。此外,在trace.c和ring_buffer.c中相关的代码似乎大多是无锁的。除了优先级0之外没有其他东西,并且它没有被抢占,而是似乎自愿取消调度。
我在
syscall_trace()
的顶部放置了一个panic(),以确保我没有在从sys_poll()
退出时不小心掉入跟踪/审核路径之一。它没有触发,所以不是这个问题。
非常感谢您提前的帮助。
更新#1
我放弃寻找简单的方法,并实现了一个unwind_backtrace_to_str()
函数,让我用回溯信息装饰各种跟踪点。在给trace_sched_switch()和trace_sched_wake()添加回溯后,我成功地隔离出了几个延迟的原因,其中两个主要是:
未能运行计划任务,原因是在调用
sched_wake()
的CPU与需要运行任务的CPU不同。这似乎是一个更大的问题。我通过调度程序进行了跟踪,并发现在糟糕的情况下,wake_up_process()
调用try_to_wake_up()
,这最终调用ttwu_queue()
,这就比较有趣了。
优先级倒置是由于在同一进程中执行fork()
/mmap()
/munmap()
时,某些线程会占用mm->mmap_sem,因此在RT线程进行futex_wait()
或tracing_mark_write()
时无法使用该信号量。通过重构代码并在某些地方使用vfork()而不是fork(),可以大部分避免此问题。
在ttwu_queue()
内部,我们没有进入 'if',因为cpus_share_cache()
对于我们的任何核心始终返回true(听起来正确,共享L2缓存)。这意味着它只是为任务调用ttwu_do_activate()
然后退出。ttwu_do_activate()
似乎只将任务放在正确的运行队列上并将其标记为TASK_RUNNING
,但没有任何SMP处理。
我在ttwu_do_wakeup()
中的p->state = TASK_RUNNING;
之后添加了以下内容:
#ifdef CONFIG_SMP
if (task_cpu(p) != smp_processor_id())
smp_send_reschedule(task_cpu(p));
#endif
该问题可以通过强制目标CPU运行调度器来解决。然而,我怀疑这不是它应该工作的方式,即使这是一个真正的错误,也可能有一个更精细的修复方法。我检查了最新的内核(3.14),core.c中的代码看起来基本相同。
为什么会这样?如果cpus_share_cache()返回true,为什么不调用ttwu_queue_remote()呢?即使它们共享缓存,那又怎样--我可以看出这对于迁移决策很重要,但唤醒是本地还是远程完成呢?也许我们的cpus_share_cache()应该返回false?这个函数似乎没有很好的文档记录(或者我没有找对地方)。
uname -a
是什么?如果你想要进行RT(实时)任务,你测试过Linux内核的RT版本(https://www.kernel.org/pub/linux/kernel/projects/rt/3.4/)了吗?这可能会修复这个bug... - osgx