mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+142
-367
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -10,7 +10,6 @@ go_library(
|
||||
srcs = ["watchdog.go"],
|
||||
visibility = ["//:sandbox"],
|
||||
deps = [
|
||||
"//pkg/abi/linux",
|
||||
"//pkg/log",
|
||||
"//pkg/metric",
|
||||
"//pkg/sentry/kernel",
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user