Ashok G

Kernel, Traced · 6.12 LTS · Draft

12 — Signals, timers and RCU: three mechanisms every path uses

Three things appear in almost every chapter without being the subject: a signal arrives, a timer fires, a reader runs lock-free under RCU. How do they work, in the source?

Requirements: the guest from chapter 7, with linux-headers-$(uname -r) for the RCU module.

The signal path

Step 1: queue it. kill() reaches kill_something_info, kill_proc_info, kill_pid_info, kill_pid_info_type and group_send_sig_info (kernel/signal.c). Then do_send_sig_info and __send_signal_locked allocate a sigqueue and add it to the target’s pending list.

Step 2: pick a thread. complete_signal chooses one, and signal_wake_up_state sets TIF_SIGPENDING and kicks it out of an interruptible sleep. Delivery isn’t here.

Step 3: deliver it on the way out. When that thread next heads to user mode, exit_to_user_mode_loop (chapter 1) calls arch_do_signal_or_restart. get_signal dequeues it and handles default actions. handle_signal then calls x64_setup_rt_frame, which writes a frame onto the user stack and points ip at your handler.

Step 4: return. The handler returns into a restorer, which calls rt_sigreturn, and the original context resumes.

What about a blocked read()? It often returns -ERESTARTSYS inside the kernel. That becomes -EINTR only without SA_RESTART (signal.c).

The timer path

Step 5: go to sleep. nanosleep reaches hrtimer_nanosleep and do_nanosleep, which calls hrtimer_sleeper_start_expires and schedule().

Step 6: wake up. The clock event device interrupts. hrtimer_interrupt calls __hrtimer_run_queues, then hrtimer_wakeup, which wakes the task.

Why does a sleep overshoot? One source-backed cause is timer slack, 50,000 ns by default (init_task.c). With high-resolution timers on, CONFIG_HZ doesn’t set the resolution here.

Step 7: the other timer system. The timer wheel (mod_timer, run_timer_softirq) serves coarse, often-cancelled timeouts. TCP’s retransmit timer from chapter 7 lives there. There are two systems because the use cases differ: precise-and-fires versus cheap-and-usually-cancelled. tick_nohz_handler handles the tick in NO_HZ mode.

The RCU path

Step 8: the reader. On preemptible kernels, rcu_read_lock() increments current->rcu_read_lock_nesting. Readers stay preemptible. The famous “zero cost” applies to classic RCU on non-preemptive kernels (whatisRCU, tree_plugin.h).

Step 9: the writer. A writer publishes with rcu_assign_pointer, then synchronize_rcu() waits for a grace period. The grace-period kthread (rcu_preempt on preemptible builds) moves through rcu_gp_init, rcu_gp_fqs_loop and rcu_gp_cleanup (tree.c). CPUs report quiescent states, and on preemptible kernels a preempted reader can also hold the grace period up. After that, no reader can still hold the old pointer.

Step 10: asynchronous freeing. call_rcu does the same without blocking. The path walk and many lookups rely on this.

Hands-on: follow a signal

  1. Write a program that installs a SIGUSR1 handler with sigaction, then blocks in read(0, ...).

  2. Trace it:

    bpftrace -e 'tracepoint:signal:signal_generate { printf("gen sig=%d pid=%d by %s\n", args.sig, args.pid, comm); }
    tracepoint:signal:signal_deliver { printf("deliver sig=%d in %s\n", args.sig, comm); }
    kprobe:x64_setup_rt_frame { printf("frame %s\n", comm); }
    kprobe:__x64_sys_rt_sigreturn { printf("sigreturn %s\n", comm); }'
  3. Send the signal with kill -USR1 <pid>.

  4. Look for: gen in the sender’s context, then deliver, frame and sigreturn in the target’s.

  5. Repeat with and without SA_RESTART. read should return EINTR only without it.

Hands-on: measure a timer

  1. Check the event’s fields, then trace a short sleep:

    bpftrace -lv tracepoint:timer:hrtimer_start
    bpftrace -e 'tracepoint:timer:hrtimer_start /comm=="sleep"/ { printf("%lld slack_ns=%lld\n", args.expires, args.expires-args.softexpires); }
    kprobe:hrtimer_wakeup { @wake=count(); }' &
    sleep 0.01
  2. Expected: a 50,000 ns gap at the default slack. If nothing prints, remove the comm filter.

  3. Write a small C program that calls prctl(PR_SET_TIMERSLACK, 1), then measures clock_nanosleep overshoot with and without it.

  4. Write down your numbers. Don’t trust a figure unless you measured it, and only under KVM.

Hands-on: watch RCU

  1. Check whether the grace-period tracepoint exists. It needs CONFIG_RCU_TRACE, which may be off:

    grep RCU_TRACE /boot/config-$(uname -r)
    ps -e | grep -E 'rcu_(preempt|sched)'
  2. Use functions that are always compiled in. Run the symbol check first, since these may be local symbols:

    bpftrace -e 'kprobe:synchronize_rcu { @s[comm]=count(); }
    kprobe:rcu_gp_init { @init=count(); } kprobe:rcu_gp_cleanup { @done=count(); }
    tracepoint:rcu:rcu_utilization { @u=count(); }'
  3. Look for: rcu_gp_init and rcu_gp_cleanup pairing up over time.

  4. Extend it. Write a kernel module with a reader holding rcu_read_lock() for 100 ms using mdelay, never msleep, and another thread timing synchronize_rcu().

  5. Expected: the writer blocks for at least the rest of that reader. You should not see a stall warning, because the default stall timeout is 21 seconds (stall warnings).

The takeaway

Deferral is the kernel’s native idiom. A signal is queued now and acted on at the next safe point; a timer is a request now and a wakeup later; an RCU free is a promise now and a reclaim after everyone’s done. Each separates deciding from doing so that the deciding side never has to wait and the doing side never has to race. Three mechanisms, one pattern, and the pattern (enqueue now, act at a well-defined later point) appears in most concurrent systems.

This closes the series. The rest of the tree is the same work, path by path.