kernel: drive all CPU timers in CPU clock ticker

gVisor currently implements CPU clocks as follows:

- A per-sentry "CPU clock ticker goroutine"
  (task_sched.go:Kernel.runCPUClockTicker()) periodically advances
  Kernel.cpuClock, causing it to serve as a very coarse but inexpensive
  monotonic wall clock (that happens to be suspended when no tasks are
  running).

- Task goroutines observe the most recent value of Kernel.cpuClock when
  changing state (Task.gosched.Timestamp), and use it to compute the number of
  CPU clock ticks that have elapsed in a given state. Thus, task CPU clocks are
  approximately based on the wall time during which they were marked as
  running.

- ITIMER_VIRTUAL, ITIMER_PROF, and RLIMIT_CPU are checked by the CPU clock
  ticker goroutine after advancing Kernel.cpuClock. POSIX interval timers and
  timerfds check CPU clocks (taskClock/tgClock) in ktime.SampledTimer
  goroutines.

This has three major problems:

- ktime.SampledTimer goroutines for CPU clock timers run concurrently with the
  CPU clock ticker, and are not informed as to when corresponding tasks start
  or stop running (due to overhead on the task execution critical path), so
  they can't determine when CPU clocks have/will advance; instead, they simply
  poll CPU clocks on a period equal to that of the represented timer, resulting
  in significant overhead for CPU-clock-based POSIX interval timers and
  timerfds.

- For the same reason, CPU clock interval timers and timerfds may expire much
  later than when the CPU clock is actually incremented; in the interval timer
  case, this can result in notification signals being sent long after tasks
  have stopped running. (This is the same problem as in b/116538398, which
  motivated the special-casing of ITIMER_VIRTUAL and ITIMER_PROF described
  above, but applied to POSIX interval timers.)

- The sentry does not impose a limit on the number of tasks that may be
  concurrently marked running, so if more tasks are marked running than the
  number of CPUs advertised to applications, application CPU utilization can
  appear to exceed 100%.

This CL fixes these problems by introducing explicit per-Task and ThreadGroup
CPU clocks, directly advancing (up to Kernel.applicationCores of) them in the
CPU clock ticker, and directly expiring CPU timers when doing so. Itimer and
RLIMIT_CPU timers lose their special-casing and instead behave like other CPU
timers (see task_acct.go). Kernel.cpuClock is still required, but only for the
sentry watchdog.

Minor cleanup changes:

- Gather all stateify hooks in kernel_state.go.

- Replace kernel.randInt31n() with math/rand/v2, which fixes the same problem
  (https://go.dev/blog/randv2#problem.rand).

Test workload:

```
#include <err.h>
#include <signal.h>
#include <time.h>
#include <chrono>
#include <thread>

constexpr int kNumTimers = 1000;
constexpr long kTimerPeriodNS = 10000000;

int main(int argc, char** argv) {
  for (int i = 0; i < kNumTimers; i++) {
    struct sigevent sev = {.sigev_notify = SIGEV_NONE};
    timer_t timerid;
    if (timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &timerid) < 0) {
      err(1, "timer_create failed");
    }
    struct itimerspec it = {
      .it_interval = {0, kTimerPeriodNS},
      .it_value = {0, kTimerPeriodNS},
    };
    if (timer_settime(timerid, 0, &it, nullptr) < 0) {
      err(1, "timer_settime failed");
    }
  }
  std::this_thread::sleep_for(std::chrono::seconds(5));
  return 0;
}
```

Before this CL:
```
# /usr/bin/time ./runsc --ignore-cgroups --platform kvm --network none do $(pwd)/workloads/threadcputimers
1.50user 0.17system 0:05.25elapsed 31%CPU (0avgtext+0avgdata 35792maxresident)k
0inputs+184outputs (10major+20889minor)pagefaults 0swaps
```

After this CL:
```
# /usr/bin/time ./runsc --ignore-cgroups --platform kvm --network none do $(pwd)/workloads/threadcputimers
0.10user 0.12system 0:05.22elapsed 4%CPU (0avgtext+0avgdata 34040maxresident)k
0inputs+192outputs (6major+20929minor)pagefaults 0swaps
```

PiperOrigin-RevId: 695198313
This commit is contained in:
Jamie Liu
2024-11-10 22:19:30 -08:00
committed by gVisor bot
parent 7c2bcddc13
commit 2d90353f9f
15 changed files with 819 additions and 564 deletions
-20
View File
@@ -40,13 +40,6 @@ go_template_instance(
},
)
declare_mutex(
name = "cpu_clock_mutex",
out = "cpu_clock_mutex.go",
package = "kernel",
prefix = "cpuClock",
)
declare_mutex(
name = "user_counters_mutex",
out = "user_counters_mutex.go",
@@ -143,17 +136,6 @@ go_template_instance(
},
)
go_template_instance(
name = "seqatomic_taskgoroutineschedinfo",
out = "seqatomic_taskgoroutineschedinfo_unsafe.go",
package = "kernel",
suffix = "TaskGoroutineSchedInfo",
template = "//pkg/sync/seqatomic:generic_seqatomic",
types = {
"Value": "TaskGoroutineSchedInfo",
},
)
go_template_instance(
name = "session_list",
out = "session_list.go",
@@ -240,7 +222,6 @@ go_library(
"cgroup_mounts_mutex.go",
"cgroup_mutex.go",
"context.go",
"cpu_clock_mutex.go",
"fd_table.go",
"fd_table_mutex.go",
"fd_table_refs.go",
@@ -267,7 +248,6 @@ go_library(
"running_tasks_mutex.go",
"seccheck.go",
"seccomp.go",
"seqatomic_taskgoroutineschedinfo_unsafe.go",
"session_list.go",
"session_refs.go",
"sessions.go",
+9 -26
View File
@@ -21,7 +21,7 @@
// Kernel.extMu
// TTY.mu
// ThreadGroup.timerMu
// ktime.Timer.mu (for IntervalTimer) and Kernel.cpuClockMu
// Locks acquired by ktime.Timer methods
// TaskSet.mu
// SignalHandlers.mu
// Task.mu
@@ -197,30 +197,11 @@ type Kernel struct {
// Invariant: runningTasksCond.L == &runningTasksMu.
runningTasksCond sync.Cond `state:"nosave"`
// cpuClock is incremented every linux.ClockTick by a goroutine running
// kernel.runCPUClockTicker() while runningTasks != 0.
//
// cpuClock is used to measure task CPU usage, since sampling monotonicClock
// twice on every syscall turns out to be unreasonably expensive. This is
// similar to how Linux does task CPU accounting on x86
// (CONFIG_IRQ_TIME_ACCOUNTING), although Linux also uses scheduler timing
// information to improve resolution
// (kernel/sched/cputime.c:cputime_adjust()), which we can't do since
// "preeemptive" scheduling is managed by the Go runtime, which doesn't
// provide this information.
//
// cpuClock is mutable, and is accessed using atomic memory operations.
cpuClock atomicbitops.Uint64
// cpuClockTickTimer drives increments of cpuClock.
cpuClockTickTimer *time.Timer `state:"nosave"`
// cpuClockMu is used to make increments of cpuClock, and updates of timers
// based on cpuClock, atomic.
cpuClockMu cpuClockMutex `state:"nosave"`
// cpuClockTickerRunning is true if the goroutine that increments cpuClock is
// running and false if it is blocked in runningTasksCond.Wait() or if it
// running, and false if it is blocked in runningTasksCond.Wait() or if it
// never started.
//
// cpuClockTickerRunning is protected by runningTasksMu.
@@ -236,6 +217,13 @@ type Kernel struct {
// Invariant: cpuClockTickerStopCond.L == &runningTasksMu.
cpuClockTickerStopCond sync.Cond `state:"nosave"`
// cpuClock is a coarse monotonic clock that is advanced by the CPU clock
// ticker and thus approximates wall time when tasks are running (but is
// strictly slower due to CPU clock ticker goroutine wakeup latency). This
// does not use ktime.SyntheticClock since this clock currently does not
// need to support timers.
cpuClock atomicbitops.Int64
// uniqueID is used to generate unique identifiers.
//
// uniqueID is mutable, and is accessed using atomic memory operations.
@@ -1643,11 +1631,6 @@ func (k *Kernel) MonotonicClock() ktime.SampledClock {
return k.timekeeper.monotonicClock
}
// CPUClockNow returns the current value of k.cpuClock.
func (k *Kernel) CPUClockNow() uint64 {
return k.cpuClock.Load()
}
// Syslog returns the syslog.
func (k *Kernel) Syslog() *syslog {
return &k.syslog
+60
View File
@@ -36,3 +36,63 @@ func (k *Kernel) loadDanglingEndpoints(_ context.Context, es []tcpip.Endpoint) {
tcpip.AddDanglingEndpoint(e)
}
}
// saveVforkParent is invoked by stateify.
func (t *Task) saveVforkParent() *Task {
return t.vforkParent.Load()
}
// loadVforkParent is invoked by stateify.
func (t *Task) loadVforkParent(_ context.Context, vforkParent *Task) {
t.vforkParent.Store(vforkParent)
}
// savePtraceTracer is invoked by stateify.
func (t *Task) savePtraceTracer() *Task {
return t.ptraceTracer.Load()
}
// loadPtraceTracer is invoked by stateify.
func (t *Task) loadPtraceTracer(_ context.Context, tracer *Task) {
t.ptraceTracer.Store(tracer)
}
// saveSeccomp is invoked by stateify.
func (t *Task) saveSeccomp() *taskSeccomp {
return t.seccomp.Load()
}
// loadSeccomp is invoked by stateify.
func (t *Task) loadSeccomp(_ context.Context, seccompData *taskSeccomp) {
t.seccomp.Store(seccompData)
}
// saveAppCPUClockLast is invoked by stateify.
func (tg *ThreadGroup) saveAppCPUClockLast() *Task {
return tg.appCPUClockLast.Load()
}
// loadAppCPUClockLast is invoked by stateify.
func (tg *ThreadGroup) loadAppCPUClockLast(_ context.Context, task *Task) {
tg.appCPUClockLast.Store(task)
}
// saveAppSysCPUClockLast is invoked by stateify.
func (tg *ThreadGroup) saveAppSysCPUClockLast() *Task {
return tg.appSysCPUClockLast.Load()
}
// loadAppSysCPUClockLast is invoked by stateify.
func (tg *ThreadGroup) loadAppSysCPUClockLast(_ context.Context, task *Task) {
tg.appSysCPUClockLast.Store(task)
}
// saveOldRSeqCritical is invoked by stateify.
func (tg *ThreadGroup) saveOldRSeqCritical() *OldRSeqCriticalRegion {
return tg.oldRSeqCritical.Load()
}
// loadOldRSeqCritical is invoked by stateify.
func (tg *ThreadGroup) loadOldRSeqCritical(_ context.Context, r *OldRSeqCriticalRegion) {
tg.oldRSeqCritical.Store(r)
}
+24 -30
View File
@@ -106,12 +106,30 @@ type Task struct {
// interruptChan is always notified after restore (see Task.run).
interruptChan chan struct{} `state:"nosave"`
// gosched contains the current scheduling state of the task goroutine.
// gostateSeq allows Task.TaskGoroutineStateTime() to read gostate and
// gostateTime atomically.
//
// gosched is protected by goschedSeq. gosched is owned by the task
// goroutine.
goschedSeq sync.SeqCount `state:"nosave"`
gosched TaskGoroutineSchedInfo
// gostateSeq is owned by the task goroutine.
gostateSeq sync.SeqCount `state:"nosave"`
// gostate is the current scheduling state of the task goroutine.
//
// gostate is owned by the task goroutine.
gostate atomicbitops.Uint32
// gostateTime was the value of Kernel.cpuClock when gostate was last
// updated or refreshed.
//
// gostateTime is owned by the task goroutine.
gostateTime atomicbitops.Int64
// appCPUClock approximates the amount of time the task goroutine has spent
// in TaskGoroutineRunningApp.
appCPUClock ktime.SyntheticClock
// appSysCPUClock approximates the amount of time the task goroutine has
// spent in TaskGoroutineRunningApp or TaskGoroutineRunningSys.
appSysCPUClock ktime.SyntheticClock
// yieldCount is the number of times the task goroutine has called
// Task.InterruptibleSleepStart, Task.UninterruptibleSleepStart, or
@@ -633,30 +651,6 @@ var (
})
)
func (t *Task) saveVforkParent() *Task {
return t.vforkParent.Load()
}
func (t *Task) loadVforkParent(_ gocontext.Context, vforkParent *Task) {
t.vforkParent.Store(vforkParent)
}
func (t *Task) savePtraceTracer() *Task {
return t.ptraceTracer.Load()
}
func (t *Task) loadPtraceTracer(_ gocontext.Context, tracer *Task) {
t.ptraceTracer.Store(tracer)
}
func (t *Task) saveSeccomp() *taskSeccomp {
return t.seccomp.Load()
}
func (t *Task) loadSeccomp(_ gocontext.Context, seccompData *taskSeccomp) {
t.seccomp.Store(seccompData)
}
// afterLoad is invoked by stateify.
func (t *Task) afterLoad(gocontext.Context) {
t.updateInfoLocked()
@@ -664,7 +658,7 @@ func (t *Task) afterLoad(gocontext.Context) {
ts.populateCache(t)
}
t.interruptChan = make(chan struct{}, 1)
t.gosched.State = TaskGoroutineNonexistent
t.gostate.Store(uint32(TaskGoroutineNonexistent))
if t.stop != nil {
t.stopCount = atomicbitops.FromInt32(1)
}
+97 -44
View File
@@ -17,6 +17,10 @@ package kernel
// Accounting, limits, timers.
import (
"math"
"sync/atomic"
"time"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/ktime"
@@ -28,24 +32,18 @@ import (
//
// Preconditions: The caller must be running on the task goroutine.
func (t *Task) Getitimer(id int32) (linux.ItimerVal, error) {
var tm ktime.Time
var s ktime.Setting
var timer ktime.Timer
switch id {
case linux.ITIMER_REAL:
tm, s = t.tg.itimerRealTimer.Get()
timer = t.tg.itimerRealTimer
case linux.ITIMER_VIRTUAL:
tm = t.tg.UserCPUClock().Now()
t.tg.signalHandlers.mu.Lock()
s, _ = t.tg.itimerVirtSetting.At(tm)
t.tg.signalHandlers.mu.Unlock()
timer = &t.tg.itimerVirtTimer
case linux.ITIMER_PROF:
tm = t.tg.CPUClock().Now()
t.tg.signalHandlers.mu.Lock()
s, _ = t.tg.itimerProfSetting.At(tm)
t.tg.signalHandlers.mu.Unlock()
timer = &t.tg.itimerProfTimer
default:
return linux.ItimerVal{}, linuxerr.EINVAL
}
tm, s := timer.Get()
val, iv := ktime.SpecFromSetting(tm, s)
return linux.ItimerVal{
Value: linux.DurationToTimeval(val),
@@ -57,46 +55,30 @@ func (t *Task) Getitimer(id int32) (linux.ItimerVal, error) {
//
// Preconditions: The caller must be running on the task goroutine.
func (t *Task) Setitimer(id int32, newitv linux.ItimerVal) (linux.ItimerVal, error) {
var tm ktime.Time
var olds ktime.Setting
var (
timer ktime.Timer
last *atomic.Pointer[Task]
)
switch id {
case linux.ITIMER_REAL:
news, err := ktime.SettingFromSpec(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), t.tg.itimerRealTimer.Clock())
if err != nil {
return linux.ItimerVal{}, err
}
tm, olds = t.tg.itimerRealTimer.Set(news, nil)
timer = t.tg.itimerRealTimer
case linux.ITIMER_VIRTUAL:
c := t.tg.UserCPUClock()
t.k.cpuClockMu.Lock()
defer t.k.cpuClockMu.Unlock()
tm = c.Now()
news, err := ktime.SettingFromSpecAt(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), tm)
if err != nil {
return linux.ItimerVal{}, err
}
t.tg.signalHandlers.mu.Lock()
olds = t.tg.itimerVirtSetting
t.tg.itimerVirtSetting = news
t.tg.updateCPUTimersEnabledLocked()
t.tg.signalHandlers.mu.Unlock()
timer = &t.tg.itimerVirtTimer
last = &t.tg.appCPUClockLast
case linux.ITIMER_PROF:
c := t.tg.CPUClock()
t.k.cpuClockMu.Lock()
defer t.k.cpuClockMu.Unlock()
tm = c.Now()
news, err := ktime.SettingFromSpecAt(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), tm)
if err != nil {
return linux.ItimerVal{}, err
}
t.tg.signalHandlers.mu.Lock()
olds = t.tg.itimerProfSetting
t.tg.itimerProfSetting = news
t.tg.updateCPUTimersEnabledLocked()
t.tg.signalHandlers.mu.Unlock()
timer = &t.tg.itimerProfTimer
last = &t.tg.appSysCPUClockLast
default:
return linux.ItimerVal{}, linuxerr.EINVAL
}
news, err := ktime.SettingFromSpec(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), timer.Clock())
if err != nil {
return linux.ItimerVal{}, err
}
if last != nil {
last.Store(t)
}
tm, olds := timer.Set(news, nil)
oldval, oldiv := ktime.SpecFromSetting(tm, olds)
return linux.ItimerVal{
Value: linux.DurationToTimeval(oldval),
@@ -104,6 +86,77 @@ func (t *Task) Setitimer(id int32, newitv linux.ItimerVal) (linux.ItimerVal, err
}, nil
}
// NotifyRlimitCPUUpdated is called by setrlimit.
//
// Preconditions: The caller must be running on the task goroutine.
func (t *Task) NotifyRlimitCPUUpdated() {
// Lock t.tg.timerMu to synchronize updates to these timers between tasks
// in t.tg.
t.tg.timerMu.Lock()
defer t.tg.timerMu.Unlock()
rlimitCPU := t.tg.limits.Get(limits.CPU)
t.tg.appSysCPUClockLast.Store(t)
t.tg.rlimitCPUSoftTimer.Set(ktime.Setting{
Enabled: rlimitCPU.Cur != limits.Infinity,
Next: ktime.FromSeconds(int64(min(rlimitCPU.Cur, math.MaxInt64))),
Period: time.Second,
}, nil)
t.tg.rlimitCPUHardTimer.Set(ktime.Setting{
Enabled: rlimitCPU.Max != limits.Infinity,
Next: ktime.FromSeconds(int64(min(rlimitCPU.Max, math.MaxInt64))),
}, nil)
}
// +stateify savable
type itimerRealListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.Listener.NotifyTimer.
func (l *itimerRealListener) NotifyTimer(exp uint64) {
l.tg.SendSignal(SignalInfoPriv(linux.SIGALRM))
}
// +stateify savable
type itimerVirtListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.Listener.NotifyTimer.
func (l *itimerVirtListener) NotifyTimer(exp uint64) {
l.tg.appCPUClockLast.Load().SendGroupSignal(SignalInfoPriv(linux.SIGVTALRM))
}
// +stateify savable
type itimerProfListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.Listener.NotifyTimer.
func (l *itimerProfListener) NotifyTimer(exp uint64) {
l.tg.appSysCPUClockLast.Load().SendGroupSignal(SignalInfoPriv(linux.SIGPROF))
}
// +stateify savable
type rlimitCPUSoftListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.Listener.NotifyTimer.
func (l *rlimitCPUSoftListener) NotifyTimer(exp uint64) {
l.tg.appSysCPUClockLast.Load().SendGroupSignal(SignalInfoPriv(linux.SIGXCPU))
}
// +stateify savable
type rlimitCPUHardListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.Listener.NotifyTimer.
func (l *rlimitCPUHardListener) NotifyTimer(exp uint64) {
l.tg.appSysCPUClockLast.Load().SendGroupSignal(SignalInfoPriv(linux.SIGKILL))
}
// IOUsage returns the io usage of the thread.
func (t *Task) IOUsage() *usage.IO {
return t.ioUsage
+1 -9
View File
@@ -741,7 +741,6 @@ func (t *Task) exitNotifyLocked(fromPtraceDetach bool) {
t.tg.tasks.Remove(t)
t.tg.tasksCount--
tc := t.tg.tasksCount
t.tg.exitedCPUStats.Accumulate(t.CPUStats())
t.tg.signalHandlers.mu.Unlock()
t.tg.ioUsage.Accumulate(t.ioUsage)
if tc == 1 && t != t.tg.leader {
@@ -1126,14 +1125,7 @@ func (t *Task) waitCollectZombieLocked(target *Task, opts *WaitOptions, asPtrace
if target.parent != nil && target.parent.tg == t.tg && target.exitParentNotified {
target.exitParentAcked = true
if target == target.tg.leader {
// target.tg.exitedCPUStats doesn't include target.CPUStats() yet,
// and won't until after target.exitNotifyLocked() (maybe). Include
// target.CPUStats() explicitly. This is consistent with Linux,
// which accounts an exited task's cputime to its thread group in
// kernel/exit.c:release_task() => __exit_signal(), and uses
// thread_group_cputime_adjusted() in wait_task_zombie().
t.tg.childCPUStats.Accumulate(target.CPUStats())
t.tg.childCPUStats.Accumulate(target.tg.exitedCPUStats)
t.tg.childCPUStats.Accumulate(target.tg.CPUStats())
t.tg.childCPUStats.Accumulate(target.tg.childCPUStats)
// Update t's child max resident set size. The size will be the maximum
// of this thread's size and all its childrens' sizes.
+1
View File
@@ -391,5 +391,6 @@ func (tg *ThreadGroup) WaitExited() {
// Yield yields the processor for the calling task.
func (t *Task) Yield() {
t.yieldCount.Add(1)
t.tg.yieldCount.Add(1)
runtime.Gosched()
}
File diff suppressed because it is too large Load Diff
+46 -56
View File
@@ -15,7 +15,6 @@
package kernel
import (
goContext "context"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
@@ -170,31 +169,19 @@ type ThreadGroup struct {
timerMu threadGroupTimerMutex `state:"nosave"`
// itimerRealTimer implements ITIMER_REAL for the thread group.
itimerRealTimer *ktime.SampledTimer
// ITIMER_* timers:
itimerRealTimer *ktime.SampledTimer
itimerRealListener itimerRealListener
itimerVirtTimer ktime.SyntheticTimer
itimerVirtListener itimerVirtListener
itimerProfTimer ktime.SyntheticTimer
itimerProfListener itimerProfListener
// itimerVirtSetting is the ITIMER_VIRTUAL setting for the thread group.
//
// itimerVirtSetting is protected by the signal mutex.
itimerVirtSetting ktime.Setting
// itimerProfSetting is the ITIMER_PROF setting for the thread group.
//
// itimerProfSetting is protected by the signal mutex.
itimerProfSetting ktime.Setting
// rlimitCPUSoftSetting is the setting for RLIMIT_CPU soft limit
// notifications for the thread group.
//
// rlimitCPUSoftSetting is protected by the signal mutex.
rlimitCPUSoftSetting ktime.Setting
// cpuTimersEnabled is non-zero if itimerVirtSetting.Enabled is true,
// itimerProfSetting.Enabled is true, rlimitCPUSoftSetting.Enabled is true,
// or limits.Get(CPU) is finite.
//
// cpuTimersEnabled is protected by the signal mutex.
cpuTimersEnabled atomicbitops.Uint32
// RLIMIT_CPU timers:
rlimitCPUSoftTimer ktime.SyntheticTimer
rlimitCPUSoftListener rlimitCPUSoftListener
rlimitCPUHardTimer ktime.SyntheticTimer
rlimitCPUHardListener rlimitCPUHardListener
// timers is the thread group's POSIX interval timers. nextTimerID is the
// TimerID at which allocation should begin searching for an unused ID.
@@ -203,13 +190,25 @@ type ThreadGroup struct {
timers map[linux.TimerID]*IntervalTimer
nextTimerID linux.TimerID
// exitedCPUStats is the CPU usage for all exited tasks in the thread
// group. exitedCPUStats is protected by both the TaskSet mutex and the
// signal mutex. Mutating it requires that the TaskSet mutex is locked for
// writing *and* that the signal mutex is locked. Reading it requires
// locking the TaskSet mutex (for reading or writing) *or* locking the
// signal mutex.
exitedCPUStats usage.CPUStats
// appCPUClockLast is the last task to have incremented appCPUClock or set
// a timer dependent on appCPUClock.
appCPUClockLast atomic.Pointer[Task] `state:".(*Task)"`
// appCPUClock is the sum of Task.appCPUClock for all past and present
// tasks in the thread group.
appCPUClock ktime.SyntheticClock
// appSysCPUClockLast is the last task to have incremented appSysCPUClock
// or set a timer dependent on appSysCPUClock.
appSysCPUClockLast atomic.Pointer[Task] `state:".(*Task)"`
// appSysCPUClock is the sum of Task.appSysCPUClock for all past and
// present tasks in the thread group.
appSysCPUClock ktime.SyntheticClock
// yieldCount is the sum of Task.yieldCount for all past and present tasks
// in the thread group.
yieldCount atomicbitops.Uint64
// childCPUStats is the CPU usage of all joined descendants of this thread
// group. childCPUStats is protected by the TaskSet mutex.
@@ -292,25 +291,24 @@ func (k *Kernel) NewThreadGroup(pidns *PIDNamespace, sh *SignalHandlers, termina
},
signalHandlers: sh,
terminationSignal: terminationSignal,
timers: make(map[linux.TimerID]*IntervalTimer),
ioUsage: &usage.IO{},
limits: limits,
}
tg.itimerRealTimer = ktime.NewSampledTimer(k.timekeeper.monotonicClock, &itimerRealListener{tg: tg})
tg.timers = make(map[linux.TimerID]*IntervalTimer)
tg.itimerRealTimer = ktime.NewSampledTimer(k.timekeeper.monotonicClock, &tg.itimerRealListener)
tg.itimerRealListener.tg = tg
tg.itimerVirtTimer.Init(&tg.appCPUClock, &tg.itimerVirtListener)
tg.itimerVirtListener.tg = tg
tg.itimerProfTimer.Init(&tg.appSysCPUClock, &tg.itimerProfListener)
tg.itimerProfListener.tg = tg
tg.rlimitCPUSoftTimer.Init(&tg.appSysCPUClock, &tg.rlimitCPUSoftListener)
tg.rlimitCPUSoftListener.tg = tg
tg.rlimitCPUHardTimer.Init(&tg.appSysCPUClock, &tg.rlimitCPUHardListener)
tg.rlimitCPUHardListener.tg = tg
tg.oldRSeqCritical.Store(&OldRSeqCriticalRegion{})
return tg
}
// saveOldRSeqCritical is invoked by stateify.
func (tg *ThreadGroup) saveOldRSeqCritical() *OldRSeqCriticalRegion {
return tg.oldRSeqCritical.Load()
}
// loadOldRSeqCritical is invoked by stateify.
func (tg *ThreadGroup) loadOldRSeqCritical(_ goContext.Context, r *OldRSeqCriticalRegion) {
tg.oldRSeqCritical.Store(r)
}
// signalLock atomically locks tg.SignalHandlers().mu and returns the
// SignalHandlers.
func (tg *ThreadGroup) signalLock() *SignalHandlers {
@@ -336,6 +334,10 @@ func (tg *ThreadGroup) Release(ctx context.Context) {
// Timers must be destroyed without holding the TaskSet or signal mutexes
// since timers send signals with Timer.mu locked.
tg.itimerRealTimer.Destroy()
tg.itimerVirtTimer.Destroy()
tg.itimerProfTimer.Destroy()
tg.rlimitCPUSoftTimer.Destroy()
tg.rlimitCPUHardTimer.Destroy()
var its []*IntervalTimer
tg.signalHandlers.mu.Lock()
for _, it := range tg.timers {
@@ -649,15 +651,3 @@ func (tg *ThreadGroup) IsInitIn(pidns *PIDNamespace) bool {
func (tg *ThreadGroup) isInitInLocked(pidns *PIDNamespace) bool {
return pidns.tgids[tg] == initTID
}
// itimerRealListener implements ktime.Listener for ITIMER_REAL expirations.
//
// +stateify savable
type itimerRealListener struct {
tg *ThreadGroup
}
// NotifyTimer implements ktime.TimerListener.NotifyTimer.
func (l *itimerRealListener) NotifyTimer(exp uint64) {
l.tg.SendSignal(SignalInfoPriv(linux.SIGALRM))
}
+8 -4
View File
@@ -299,13 +299,17 @@ func (ns *PIDNamespace) IDOfThreadGroup(tg *ThreadGroup) ThreadID {
// Tasks returns a snapshot of the tasks in ns.
func (ns *PIDNamespace) Tasks() []*Task {
return ns.TasksAppend(nil)
}
// TasksAppend appends a snapshot of the tasks in ns to ts.
func (ns *PIDNamespace) TasksAppend(ts []*Task) []*Task {
ns.owner.mu.RLock()
defer ns.owner.mu.RUnlock()
tasks := make([]*Task, 0, len(ns.tasks))
for t := range ns.tids {
tasks = append(tasks, t)
ts = append(ts, t)
}
return tasks
return ts
}
// NumTasks returns the number of tasks in ns.
@@ -487,7 +491,7 @@ func (tg *ThreadGroup) ID() ThreadID {
type taskNode struct {
// tg is the thread group that this task belongs to. The tg pointer is
// immutable.
tg *ThreadGroup `state:"wait"`
tg *ThreadGroup
// taskEntry links into tg.tasks. Note that this means that
// Task.Next/Prev/SetNext/SetPrev refer to sibling tasks in the same thread
+52 -1
View File
@@ -1,4 +1,4 @@
load("//tools:defs.bzl", "go_library")
load("//tools:defs.bzl", "go_library", "go_test")
load("//tools/go_generics:defs.bzl", "go_template_instance")
package(
@@ -17,6 +17,43 @@ go_template_instance(
},
)
go_template_instance(
name = "synthetic_timer_list",
out = "synthetic_timer_list.go",
package = "ktime",
prefix = "syntheticTimer",
template = "//pkg/ilist:generic_list",
types = {
"Element": "*SyntheticTimer",
"Linker": "*SyntheticTimer",
},
)
go_template_instance(
name = "synthetic_timer_set",
out = "synthetic_timer_set.go",
package = "ktime",
prefix = "syntheticTimer",
template = "//pkg/segment:generic_set",
types = {
"Key": "uint64",
"Range": "uint64Range",
"Value": "syntheticTimerQueue",
"Functions": "syntheticTimerSetFunctions",
},
)
go_template_instance(
name = "uint64_range",
out = "uint64_range.go",
package = "ktime",
prefix = "uint64",
template = "//pkg/segment:generic_range",
types = {
"T": "uint64",
},
)
go_library(
name = "ktime",
srcs = [
@@ -24,11 +61,16 @@ go_library(
"ktime.go",
"sampled_timer.go",
"seqatomic_sampled_clock_unsafe.go",
"synthetic_timer.go",
"synthetic_timer_list.go",
"synthetic_timer_set.go",
"uint64_range.go",
"util.go",
],
visibility = ["//pkg/sentry:internal"],
deps = [
"//pkg/abi/linux",
"//pkg/atomicbitops",
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/gohacks",
@@ -36,3 +78,12 @@ go_library(
"//pkg/waiter",
],
)
go_test(
name = "ktime_test",
size = "small",
srcs = [
"synthetic_timer_test.go",
],
library = ":ktime",
)
+260
View File
@@ -0,0 +1,260 @@
// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ktime
import (
"fmt"
"math"
"time"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/sync"
)
// SyntheticTimer implements Timer for SyntheticClocks.
//
// +stateify savable
type SyntheticTimer struct {
// immutable
clock *SyntheticClock
listener Listener
// setting is the timer's current setting. setting is protected by
// clock.mu.
setting Setting
// syntheticTimerEntry links the SyntheticTimer into
// syntheticTimerQueue.timers when setting.Enabled == true.
// syntheticTimerEntry is protected by mu.
syntheticTimerEntry
}
// SyntheticClock is a Clock whose current time is set manually by calling
// Store or Add.
//
// +stateify savable
type SyntheticClock struct {
mu sync.Mutex `state:"nosave"`
// now is the Clock's current time. Writes to now require that mu is
// locked.
now atomicbitops.Int64
// timers maps each timer expiration time to a list of all enabled timers
// with that expiration time. timers is protected by mu.
timers syntheticTimerSet
}
// syntheticTimerQueue is the value type of SyntheticClock.timers.
//
// +stateify savable
type syntheticTimerQueue struct {
timers syntheticTimerList
}
// NewSyntheticTimer returns an initialized heap-allocated SyntheticTimer.
func NewSyntheticTimer(clock *SyntheticClock, listener Listener) *SyntheticTimer {
t := &SyntheticTimer{}
t.Init(clock, listener)
return t
}
// Init makes a zero-value SyntheticTimer ready for use.
func (t *SyntheticTimer) Init(clock *SyntheticClock, listener Listener) {
t.clock = clock
t.listener = listener
}
// Destroy implements Timer.Destroy.
func (t *SyntheticTimer) Destroy() {
// Just stop the timer.
t.clock.mu.Lock()
defer t.clock.mu.Unlock()
if t.setting.Enabled {
t.setting.Enabled = false
t.clock.delTimerLocked(t)
}
}
// Pause implements Timer.Pause. Since SyntheticTimer expirations are caused by
// changes to the corresponding SyntheticClock's time, Pause is a no-op;
// clients must ensure that the SyntheticClock's time cannot advance while the
// timer is paused.
func (t *SyntheticTimer) Pause() {
}
// Resume implements Timer.Resume. Since Pause is a no-op, Resume is also a
// no-op.
func (t *SyntheticTimer) Resume() {
}
// Clock implements Timer.Clock.
func (t *SyntheticTimer) Clock() Clock {
return t.clock
}
// Get implements Timer.Get.
func (t *SyntheticTimer) Get() (Time, Setting) {
t.clock.mu.Lock()
defer t.clock.mu.Unlock()
// SyntheticTimers are expired synchronously with SyntheticClock time
// changes, so t.setting is always up to date.
return t.clock.nowLocked(), t.setting
}
// Set implements Timer.Set.
func (t *SyntheticTimer) Set(s Setting, f func()) (Time, Setting) {
t.clock.mu.Lock()
defer t.clock.mu.Unlock()
// SyntheticTimers are expired synchronously with SyntheticClock time
// changes, so t.setting is always up to date.
now := t.clock.nowLocked()
oldS := t.setting
newS, newExp := s.At(now)
if f != nil {
f()
}
if oldS != newS {
if oldS.Enabled {
t.clock.delTimerLocked(t)
}
t.setting = newS
if newS.Enabled {
t.clock.addTimerLocked(t)
}
}
if newExp > 0 {
t.listener.NotifyTimer(newExp)
}
return now, oldS
}
// Now implements Clock.Now.
func (c *SyntheticClock) Now() Time {
return FromNanoseconds(c.now.Load())
}
// Preconditions: c.mu must be locked.
func (c *SyntheticClock) nowLocked() Time {
return FromNanoseconds(c.now.RacyLoad())
}
// NewTimer implements Clock.NewTimer.
func (c *SyntheticClock) NewTimer(listener Listener) Timer {
return NewSyntheticTimer(c, listener)
}
// Store sets c's current time to now and notifies expired timers.
//
// Preconditions:
// - now.Nanoseconds() >= 0.
// - The caller must not hold locks following Timer methods in the lock order
// (since Listener notification requires acquiring such locks).
func (c *SyntheticClock) Store(now Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.setTimeLocked(now.Nanoseconds())
}
// Add increases c's current time by d and notifies expired timers.
//
// Preconditions:
// - c's resulting current time >= 0.
// - The caller must not hold locks following Timer methods in the lock order
// (since Listener notification requires acquiring such locks).
func (c *SyntheticClock) Add(delta time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.setTimeLocked(c.now.RacyLoad() + delta.Nanoseconds())
}
// Preconditions: c.mu must be locked.
func (c *SyntheticClock) setTimeLocked(nowNS int64) {
if nowNS < 0 {
panic(fmt.Sprintf("invalid time %d", nowNS))
}
c.now.Store(nowNS)
now := FromNanoseconds(nowNS)
// Expire timers.
for {
seg := c.timers.FirstSegment()
if !seg.Ok() || uint64(nowNS) < seg.Start() {
break
}
// Make a copy of the timers list, then remove seg and iterate the
// copy, since insertion of new segments (for periodic timers) will
// invalidate seg.
timers := seg.ValuePtr().timers
c.timers.Remove(seg)
for !timers.Empty() {
t := timers.Front()
timers.Remove(t)
s, exp := t.setting.At(now)
if exp == 0 {
panic(fmt.Sprintf("ktime.SyntheticClock (time=%d) contains enqueued timer %p for time=%d with unexpired setting %+v", nowNS, t, seg.Start(), t.setting))
}
t.setting = s
t.listener.NotifyTimer(exp)
if t.setting.Enabled {
c.addTimerLocked(t)
}
}
}
}
// Preconditions: c.mu must be locked.
func (c *SyntheticClock) addTimerLocked(t *SyntheticTimer) {
nextNS := uint64(t.setting.Next.Nanoseconds())
seg, gap := c.timers.Find(nextNS)
if gap.Ok() {
seg = c.timers.Insert(gap, uint64Range{nextNS, nextNS + 1}, syntheticTimerQueue{})
}
seg.ValuePtr().timers.PushBack(t)
}
// Preconditions: c.mu must be locked.
func (c *SyntheticClock) delTimerLocked(t *SyntheticTimer) {
nextNS := uint64(t.setting.Next.Nanoseconds())
seg := c.timers.FindSegment(nextNS)
if !seg.Ok() {
panic(fmt.Sprintf("ktime.SyntheticClock (time=%d) does not contain enqueued timer %p for time=%d with setting %+v", c.now.RacyLoad(), t, nextNS, t.setting))
}
q := seg.ValuePtr()
q.timers.Remove(t)
if q.timers.Empty() {
c.timers.Remove(seg)
}
}
type syntheticTimerSetFunctions struct{}
func (syntheticTimerSetFunctions) MinKey() uint64 {
return 0
}
func (syntheticTimerSetFunctions) MaxKey() uint64 {
return math.MaxUint64
}
func (syntheticTimerSetFunctions) ClearValue(*syntheticTimerQueue) {
}
func (syntheticTimerSetFunctions) Merge(_ uint64Range, _ syntheticTimerQueue, _ uint64Range, _ syntheticTimerQueue) (syntheticTimerQueue, bool) {
return syntheticTimerQueue{}, false
}
func (syntheticTimerSetFunctions) Split(_ uint64Range, _ syntheticTimerQueue, _ uint64) (syntheticTimerQueue, syntheticTimerQueue) {
panic("syntheticTimerSetFunctions.Split should never be called")
}
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ktime
import (
"slices"
"testing"
"time"
)
func TestSyntheticClockNow(t *testing.T) {
var c SyntheticClock
if got := c.Now(); got.Nanoseconds() != 0 {
t.Errorf("zero-value SyntheticClock: Now() = %v, want 0", got)
}
want := FromSeconds(1)
c.Store(want)
if got := c.Now(); got != want {
t.Errorf("after Store: Now() = %v, want %v", got, want)
}
c.Add(10 * time.Second)
if got, want := c.Now(), FromSeconds(11); got != want {
t.Errorf("after positive Add: Now() = %v, want %v", got, want)
}
c.Add(-5 * time.Second)
if got, want := c.Now(), FromSeconds(6); got != want {
t.Errorf("after negative Add: Now() = %v, want %v", got, want)
}
}
type testRecorder struct {
vals []int
}
type testRecorderListener struct {
r *testRecorder
val int
}
func (l *testRecorderListener) NotifyTimer(exp uint64) {
l.r.vals = append(l.r.vals, l.val)
}
func newTestRecorderTimer(c Clock, r *testRecorder, val int, next Time, period time.Duration) Timer {
t := c.NewTimer(&testRecorderListener{
r: r,
val: val,
})
t.Set(Setting{
Enabled: true,
Next: next,
Period: period,
}, nil)
return t
}
func checkRecordAt(t *testing.T, c *SyntheticClock, r *testRecorder, now Time, want []int) {
c.Store(now)
if !slices.Equal(r.vals, want) {
t.Errorf("at time %v: got %v, want %v", now, r.vals, want)
}
}
func TestSyntheticTimer(t *testing.T) {
var (
c SyntheticClock
r testRecorder
)
// Set up timers.
//
// t0 and t1 are "far apart".
// t1 and t2 are as close as possible. t1 is also periodic.
// t3 and t4 expire at the same time.
// t5 occurs between the second and third occurrences of t1.
newTestRecorderTimer(&c, &r, 0, FromSeconds(2), 0)
newTestRecorderTimer(&c, &r, 1, FromSeconds(4), 4*time.Second)
newTestRecorderTimer(&c, &r, 2, FromSeconds(4).Add(time.Nanosecond), 0)
newTestRecorderTimer(&c, &r, 3, FromSeconds(6), 0)
newTestRecorderTimer(&c, &r, 4, FromSeconds(6), 0)
newTestRecorderTimer(&c, &r, 5, FromSeconds(10), 0)
// The order in which timers expire isn't specified, but is FIFO in the
// current implementation.
checkRecordAt(t, &c, &r, FromSeconds(1), []int{})
checkRecordAt(t, &c, &r, FromSeconds(2), []int{0})
checkRecordAt(t, &c, &r, FromSeconds(3), []int{0})
checkRecordAt(t, &c, &r, FromSeconds(4).Add(-time.Nanosecond), []int{0})
checkRecordAt(t, &c, &r, FromSeconds(4), []int{0, 1})
checkRecordAt(t, &c, &r, FromSeconds(4).Add(time.Nanosecond), []int{0, 1, 2})
checkRecordAt(t, &c, &r, FromSeconds(5), []int{0, 1, 2})
checkRecordAt(t, &c, &r, FromSeconds(6), []int{0, 1, 2, 3, 4})
checkRecordAt(t, &c, &r, FromSeconds(7), []int{0, 1, 2, 3, 4})
checkRecordAt(t, &c, &r, FromSeconds(8).Add(-time.Nanosecond), []int{0, 1, 2, 3, 4})
checkRecordAt(t, &c, &r, FromSeconds(8), []int{0, 1, 2, 3, 4, 1})
checkRecordAt(t, &c, &r, FromSeconds(8), []int{0, 1, 2, 3, 4, 1})
checkRecordAt(t, &c, &r, FromSeconds(9), []int{0, 1, 2, 3, 4, 1})
checkRecordAt(t, &c, &r, FromSeconds(10), []int{0, 1, 2, 3, 4, 1, 5})
checkRecordAt(t, &c, &r, FromSeconds(11), []int{0, 1, 2, 3, 4, 1, 5})
checkRecordAt(t, &c, &r, FromSeconds(12), []int{0, 1, 2, 3, 4, 1, 5, 1})
checkRecordAt(t, &c, &r, FromSeconds(13), []int{0, 1, 2, 3, 4, 1, 5, 1})
checkRecordAt(t, &c, &r, FromSeconds(14), []int{0, 1, 2, 3, 4, 1, 5, 1})
checkRecordAt(t, &c, &r, FromSeconds(15), []int{0, 1, 2, 3, 4, 1, 5, 1})
}
-1
View File
@@ -10,7 +10,6 @@ go_library(
srcs = ["watchdog.go"],
visibility = ["//:sandbox"],
deps = [
"//pkg/abi/linux",
"//pkg/log",
"//pkg/metric",
"//pkg/sentry/kernel",
+3 -6
View File
@@ -33,7 +33,6 @@ import (
"fmt"
"time"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/metric"
"gvisor.dev/gvisor/pkg/sentry/kernel"
@@ -280,7 +279,7 @@ func (w *Watchdog) runTurn() {
newOffenders := make(map[*kernel.Task]*offender)
newTaskFound := false
now := ktime.FromNanoseconds(int64(w.k.CPUClockNow() * uint64(linux.ClockTick)))
now := w.k.CPUClockNow()
// The process may be running with low CPU limit making tasks appear stuck because
// are starved of CPU cycles. An estimate is that Tasks could have been starved
@@ -294,11 +293,9 @@ func (w *Watchdog) runTurn() {
log.Infof("Watchdog starting loop, tasks: %d, discount: %v", len(tasks), discount)
for _, t := range tasks {
tsched := t.TaskGoroutineSchedInfo()
// An offender is a task running inside the kernel for longer than the specified timeout.
if tsched.State == kernel.TaskGoroutineRunningSys {
lastUpdateTime := ktime.FromNanoseconds(int64(tsched.Timestamp * uint64(linux.ClockTick)))
tstate, lastUpdateTime := t.TaskGoroutineStateTime()
if tstate == kernel.TaskGoroutineRunningSys {
elapsed := now.Sub(lastUpdateTime) - discount
if elapsed > w.TaskTimeout {
tc, ok := w.offenders[t]