diff --git a/pkg/sentry/kernel/BUILD b/pkg/sentry/kernel/BUILD index c53b54742..2100c2009 100644 --- a/pkg/sentry/kernel/BUILD +++ b/pkg/sentry/kernel/BUILD @@ -4,6 +4,13 @@ load("//pkg/sync/locking:locking.bzl", "declare_mutex", "declare_rwmutex") package(licenses = ["notice"]) +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", @@ -201,6 +208,7 @@ go_library( "cgroup.go", "cgroup_mutex.go", "context.go", + "cpu_clock_mutex.go", "fd_table.go", "fd_table_mutex.go", "fd_table_refs.go", diff --git a/pkg/sentry/kernel/kernel.go b/pkg/sentry/kernel/kernel.go index 4cac727b0..489522fee 100644 --- a/pkg/sentry/kernel/kernel.go +++ b/pkg/sentry/kernel/kernel.go @@ -20,7 +20,7 @@ // // Kernel.extMu // ThreadGroup.timerMu -// ktime.Timer.mu (for kernelCPUClockTicker and IntervalTimer) +// ktime.Timer.mu (for IntervalTimer) and Kernel.cpuClockMu // TaskSet.mu // SignalHandlers.mu // Task.mu @@ -183,12 +183,6 @@ type Kernel struct { // syslog is the kernel log. syslog syslog - // runningTasksMu synchronizes disable/enable of cpuClockTicker when - // the kernel is idle (runningTasks == 0). - // - // runningTasksMu is used to exclude critical sections when the timer - // disables itself and when the first active task enables the timer, - // ensuring that tasks always see a valid cpuClock value. runningTasksMu runningTasksMutex `state:"nosave"` // runningTasks is the total count of tasks currently in @@ -199,36 +193,46 @@ type Kernel struct { // further protected by runningTasksMu (see incRunningTasks). runningTasks atomicbitops.Int64 - // cpuClock is incremented every linux.ClockTick. 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. + // runningTasksCond is signaled when runningTasks is incremented from 0 to 1. + // + // 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 - // cpuClockTicker increments cpuClock. - cpuClockTicker *ktime.Timer `state:"nosave"` + // cpuClockMu is used to make increments of cpuClock, and updates of timers + // based on cpuClock, atomic. + cpuClockMu cpuClockMutex `state:"nosave"` - // cpuClockTickerDisabled indicates that cpuClockTicker has been - // disabled because no tasks are running. + // cpuClockTickerRunning is true if the goroutine that increments cpuClock is + // running and false if it is blocked in runningTasksCond.Wait() or if it + // never started. // - // cpuClockTickerDisabled is protected by runningTasksMu. - cpuClockTickerDisabled bool + // cpuClockTickerRunning is protected by runningTasksMu. + cpuClockTickerRunning bool - // cpuClockTickerSetting is the ktime.Setting of cpuClockTicker at the - // point it was disabled. It is cached here to avoid a lock ordering - // violation with cpuClockTicker.mu when runningTaskMu is held. + // cpuClockTickerWakeCh is sent to to wake the goroutine that increments + // cpuClock if it's sleeping between ticks. + cpuClockTickerWakeCh chan struct{} `state:"nosave"` + + // cpuClockTickerStopCond is broadcast when cpuClockTickerRunning transitions + // from true to false. // - // cpuClockTickerSetting is only valid when cpuClockTickerDisabled is - // true. - // - // cpuClockTickerSetting is protected by runningTasksMu. - cpuClockTickerSetting ktime.Setting + // Invariant: cpuClockTickerStopCond.L == &runningTasksMu. + cpuClockTickerStopCond sync.Cond `state:"nosave"` // uniqueID is used to generate unique identifiers. // @@ -411,6 +415,9 @@ func (k *Kernel) Init(args InitKernelArgs) error { if k.rootNetworkNamespace == nil { k.rootNetworkNamespace = inet.NewRootNamespace(nil, nil) } + k.runningTasksCond.L = &k.runningTasksMu + k.cpuClockTickerWakeCh = make(chan struct{}, 1) + k.cpuClockTickerStopCond.L = &k.runningTasksMu k.applicationCores = args.ApplicationCores if args.UseHostCores { k.useHostCores = true @@ -680,6 +687,10 @@ func (k *Kernel) invalidateUnsavableMappings(ctx context.Context) error { func (k *Kernel) LoadFrom(ctx context.Context, r wire.Reader, timeReady chan struct{}, net inet.Stack, clocks sentrytime.Clocks, vfsOpts *vfs.CompleteRestoreOptions) error { loadStart := time.Now() + k.runningTasksCond.L = &k.runningTasksMu + k.cpuClockTickerWakeCh = make(chan struct{}, 1) + k.cpuClockTickerStopCond.L = &k.runningTasksMu + initAppCores := k.applicationCores // Load the pre-saved CPUID FeatureSet. @@ -1119,11 +1130,10 @@ func (k *Kernel) Start() error { } k.started = true - k.cpuClockTicker = ktime.NewTimer(k.timekeeper.monotonicClock, newKernelCPUClockTicker(k)) - k.cpuClockTicker.Swap(ktime.Setting{ - Enabled: true, - Period: linux.ClockTick, - }) + k.runningTasksMu.Lock() + k.cpuClockTickerRunning = true + k.runningTasksMu.Unlock() + go k.runCPUClockTicker() // If k was created by LoadKernelFrom, timers were stopped during // Kernel.SaveTo and need to be resumed. If k was created by NewKernel, // this is a no-op. @@ -1150,11 +1160,18 @@ func (k *Kernel) Start() error { // - Any task goroutines running in k must be stopped. // - k.extMu must be locked. func (k *Kernel) pauseTimeLocked(ctx context.Context) { - // k.cpuClockTicker may be nil since Kernel.SaveTo() may be called before - // Kernel.Start(). - if k.cpuClockTicker != nil { - k.cpuClockTicker.Pause() + // Since all task goroutines have been stopped by precondition, the CPU clock + // ticker should stop on its own; wait for it to do so, waking it up from + // sleeping betwen ticks if necessary. + k.runningTasksMu.Lock() + for k.cpuClockTickerRunning { + select { + case k.cpuClockTickerWakeCh <- struct{}{}: + default: + } + k.cpuClockTickerStopCond.Wait() } + k.runningTasksMu.Unlock() // By precondition, nothing else can be interacting with PIDNamespace.tids // or FDTable.files, so we can iterate them without synchronization. (We @@ -1195,9 +1212,8 @@ func (k *Kernel) pauseTimeLocked(ctx context.Context) { // - Any task goroutines running in k must be stopped. // - k.extMu must be locked. func (k *Kernel) resumeTimeLocked(ctx context.Context) { - if k.cpuClockTicker != nil { - k.cpuClockTicker.Resume() - } + // The CPU clock ticker will automatically resume as task goroutines resume + // execution. k.timekeeper.ResumeUpdates() for t := range k.tasks.Root.tids { @@ -1236,73 +1252,10 @@ func (k *Kernel) incRunningTasks() { // Transition from 0 -> 1. Synchronize with other transitions and timer. k.runningTasksMu.Lock() - tasks = k.runningTasks.Load() - if tasks != 0 { - // We're no longer the first task, no need to - // re-enable. - k.runningTasks.Add(1) - k.runningTasksMu.Unlock() - return + if k.runningTasks.Add(1) == 1 { + k.runningTasksCond.Signal() } - - if !k.cpuClockTickerDisabled { - // Timer was never disabled. - k.runningTasks.Store(1) - k.runningTasksMu.Unlock() - return - } - - // We need to update cpuClock for all of the ticks missed while we - // slept, and then re-enable the timer. - // - // The Notify in Swap isn't sufficient. kernelCPUClockTicker.Notify - // always increments cpuClock by 1 regardless of the number of - // expirations as a heuristic to avoid over-accounting in cases of CPU - // throttling. - // - // We want to cover the normal case, when all time should be accounted, - // so we increment for all expirations. Throttling is less concerning - // here because the ticker is only disabled from Notify. This means - // that Notify must schedule and compensate for the throttled period - // before the timer is disabled. Throttling while the timer is disabled - // doesn't matter, as nothing is running or reading cpuClock anyways. - // - // S/R also adds complication, as there are two cases. Recall that - // monotonicClock will jump forward on restore. - // - // 1. If the ticker is enabled during save, then on Restore Notify is - // called with many expirations, covering the time jump, but cpuClock - // is only incremented by 1. - // - // 2. If the ticker is disabled during save, then after Restore the - // first wakeup will call this function and cpuClock will be - // incremented by the number of expirations across the S/R. - // - // These cause very different value of cpuClock. But again, since - // nothing was running while the ticker was disabled, those differences - // don't matter. - setting, exp := k.cpuClockTickerSetting.At(k.timekeeper.monotonicClock.Now()) - if exp > 0 { - k.cpuClock.Add(exp) - } - - // Now that cpuClock is updated it is safe to allow other tasks to - // transition to running. - k.runningTasks.Store(1) - - // N.B. we must unlock before calling Swap to maintain lock ordering. - // - // cpuClockTickerDisabled need not wait until after Swap to become - // true. It is sufficient that the timer *will* be enabled. - k.cpuClockTickerDisabled = false k.runningTasksMu.Unlock() - - // This won't call Notify (unless it's been ClockTick since setting.At - // above). This means we skip the thread group work in Notify. However, - // since nothing was running while we were disabled, none of the timers - // could have expired. - k.cpuClockTicker.Swap(setting) - return } } diff --git a/pkg/sentry/kernel/task_acct.go b/pkg/sentry/kernel/task_acct.go index 4b5ec7ca1..f23fb1cd9 100644 --- a/pkg/sentry/kernel/task_acct.go +++ b/pkg/sentry/kernel/task_acct.go @@ -68,42 +68,32 @@ func (t *Task) Setitimer(id int32, newitv linux.ItimerVal) (linux.ItimerVal, err tm, olds = t.tg.itimerRealTimer.Swap(news) case linux.ITIMER_VIRTUAL: c := t.tg.UserCPUClock() - var err error - t.k.cpuClockTicker.Atomically(func() { - tm = c.Now() - var news ktime.Setting - news, err = ktime.SettingFromSpecAt(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), tm) - if err != nil { - return - } - t.tg.signalHandlers.mu.Lock() - olds = t.tg.itimerVirtSetting - t.tg.itimerVirtSetting = news - t.tg.updateCPUTimersEnabledLocked() - t.tg.signalHandlers.mu.Unlock() - }) + 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() case linux.ITIMER_PROF: c := t.tg.CPUClock() - var err error - t.k.cpuClockTicker.Atomically(func() { - tm = c.Now() - var news ktime.Setting - news, err = ktime.SettingFromSpecAt(newitv.Value.ToDuration(), newitv.Interval.ToDuration(), tm) - if err != nil { - return - } - t.tg.signalHandlers.mu.Lock() - olds = t.tg.itimerProfSetting - t.tg.itimerProfSetting = news - t.tg.updateCPUTimersEnabledLocked() - t.tg.signalHandlers.mu.Unlock() - }) + 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() default: return linux.ItimerVal{}, linuxerr.EINVAL } diff --git a/pkg/sentry/kernel/task_sched.go b/pkg/sentry/kernel/task_sched.go index 36d54089f..04c06d4b0 100644 --- a/pkg/sentry/kernel/task_sched.go +++ b/pkg/sentry/kernel/task_sched.go @@ -335,140 +335,136 @@ func (tg *ThreadGroup) CPUClock() ktime.Clock { return &tgClock{tg: tg, includeSys: true} } -type kernelCPUClockTicker struct { - k *Kernel +func (k *Kernel) runCPUClockTicker() { + tickTimer := time.NewTimer(linux.ClockTick) + rng := rand.New(rand.NewSource(rand.Int63())) + var tgs []*ThreadGroup - // These are essentially kernelCPUClockTicker.Notify local variables that - // are cached between calls to reduce allocations. - rng *rand.Rand - tgs []*ThreadGroup -} + for { + // Wait for the next CPU clock tick. + wokenEarly := false + select { + case <-tickTimer.C: + tickTimer.Reset(linux.ClockTick) + case <-k.cpuClockTickerWakeCh: + // Wake up to check if we need to stop with cpuClockTickerRunning = + // false, but then continue waiting for the next CPU clock tick. + wokenEarly = true + } -func newKernelCPUClockTicker(k *Kernel) *kernelCPUClockTicker { - return &kernelCPUClockTicker{ - k: k, - rng: rand.New(rand.NewSource(rand.Int63())), - } -} + // Stop the CPU clock while nothing is running. + if k.runningTasks.Load() == 0 { + k.runningTasksMu.Lock() + if k.runningTasks.Load() == 0 { + k.cpuClockTickerRunning = false + k.cpuClockTickerStopCond.Broadcast() + for k.runningTasks.Load() == 0 { + k.runningTasksCond.Wait() + } + k.cpuClockTickerRunning = true + } + k.runningTasksMu.Unlock() + } -// NotifyTimer implements ktime.TimerListener.NotifyTimer. -func (ticker *kernelCPUClockTicker) NotifyTimer(exp uint64, setting ktime.Setting) (ktime.Setting, bool) { - // Only increment cpuClock by 1 regardless of the number of expirations. - // This approximately compensates for cases where thread throttling or bad - // Go runtime scheduling prevents the kernelCPUClockTicker goroutine, and - // presumably task goroutines as well, from executing for a long period of - // time. It's also necessary to prevent CPU clocks from seeing large - // discontinuous jumps. - now := ticker.k.cpuClock.Add(1) - - // Check thread group CPU timers. - tgs := ticker.k.tasks.Root.ThreadGroupsAppend(ticker.tgs) - for _, tg := range tgs { - if tg.cpuTimersEnabled.Load() == 0 { + if wokenEarly { continue } - ticker.k.tasks.mu.RLock() - if tg.leader == nil { - // No tasks have ever run in this thread group. - ticker.k.tasks.mu.RUnlock() - continue - } - // Accumulate thread group CPU stats, and randomly select running tasks - // using reservoir sampling to receive CPU timer signals. - var virtReceiver *Task - nrVirtCandidates := 0 - var profReceiver *Task - nrProfCandidates := 0 - tgUserTime := tg.exitedCPUStats.UserTime - tgSysTime := tg.exitedCPUStats.SysTime - for t := tg.tasks.Front(); t != nil; t = t.Next() { - tsched := t.TaskGoroutineSchedInfo() - tgUserTime += time.Duration(tsched.userTicksAt(now) * uint64(linux.ClockTick)) - tgSysTime += time.Duration(tsched.sysTicksAt(now) * uint64(linux.ClockTick)) - switch tsched.State { - case TaskGoroutineRunningApp: - // Considered by ITIMER_VIRT, ITIMER_PROF, and RLIMIT_CPU - // timers. - nrVirtCandidates++ - if int(randInt31n(ticker.rng, int32(nrVirtCandidates))) == 0 { - virtReceiver = t - } - fallthrough - case TaskGoroutineRunningSys: - // Considered by ITIMER_PROF and RLIMIT_CPU timers. - nrProfCandidates++ - if int(randInt31n(ticker.rng, int32(nrProfCandidates))) == 0 { - profReceiver = t + // Advance the CPU clock, and timers based on the CPU clock, atomically + // under cpuClockMu. + k.cpuClockMu.Lock() + now := k.cpuClock.Add(1) + + // Check thread group CPU timers. + tgs = k.tasks.Root.ThreadGroupsAppend(tgs) + for _, tg := range tgs { + if tg.cpuTimersEnabled.Load() == 0 { + continue + } + + k.tasks.mu.RLock() + if tg.leader == nil { + // No tasks have ever run in this thread group. + k.tasks.mu.RUnlock() + continue + } + // Accumulate thread group CPU stats, and randomly select running tasks + // using reservoir sampling to receive CPU timer signals. + var virtReceiver *Task + nrVirtCandidates := 0 + var profReceiver *Task + nrProfCandidates := 0 + tgUserTime := tg.exitedCPUStats.UserTime + tgSysTime := tg.exitedCPUStats.SysTime + for t := tg.tasks.Front(); t != nil; t = t.Next() { + tsched := t.TaskGoroutineSchedInfo() + tgUserTime += time.Duration(tsched.userTicksAt(now) * uint64(linux.ClockTick)) + tgSysTime += time.Duration(tsched.sysTicksAt(now) * uint64(linux.ClockTick)) + switch tsched.State { + case TaskGoroutineRunningApp: + // Considered by ITIMER_VIRT, ITIMER_PROF, and RLIMIT_CPU + // timers. + nrVirtCandidates++ + if int(randInt31n(rng, int32(nrVirtCandidates))) == 0 { + virtReceiver = t + } + fallthrough + case TaskGoroutineRunningSys: + // Considered by ITIMER_PROF and RLIMIT_CPU timers. + nrProfCandidates++ + if int(randInt31n(rng, int32(nrProfCandidates))) == 0 { + profReceiver = t + } } } - } - tgVirtNow := ktime.FromNanoseconds(tgUserTime.Nanoseconds()) - tgProfNow := ktime.FromNanoseconds((tgUserTime + tgSysTime).Nanoseconds()) + tgVirtNow := ktime.FromNanoseconds(tgUserTime.Nanoseconds()) + tgProfNow := ktime.FromNanoseconds((tgUserTime + tgSysTime).Nanoseconds()) - // All of the following are standard (not real-time) signals, which are - // automatically deduplicated, so we ignore the number of expirations. - tg.signalHandlers.mu.Lock() - // It should only be possible for these timers to advance if we found - // at least one running task. - if virtReceiver != nil { - // ITIMER_VIRTUAL - newItimerVirtSetting, exp := tg.itimerVirtSetting.At(tgVirtNow) - tg.itimerVirtSetting = newItimerVirtSetting - if exp != 0 { - virtReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGVTALRM), true) + // All of the following are standard (not real-time) signals, which are + // automatically deduplicated, so we ignore the number of expirations. + tg.signalHandlers.mu.Lock() + // It should only be possible for these timers to advance if we found + // at least one running task. + if virtReceiver != nil { + // ITIMER_VIRTUAL + newItimerVirtSetting, exp := tg.itimerVirtSetting.At(tgVirtNow) + tg.itimerVirtSetting = newItimerVirtSetting + if exp != 0 { + virtReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGVTALRM), true) + } } - } - if profReceiver != nil { - // ITIMER_PROF - newItimerProfSetting, exp := tg.itimerProfSetting.At(tgProfNow) - tg.itimerProfSetting = newItimerProfSetting - if exp != 0 { - profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGPROF), true) + if profReceiver != nil { + // ITIMER_PROF + newItimerProfSetting, exp := tg.itimerProfSetting.At(tgProfNow) + tg.itimerProfSetting = newItimerProfSetting + if exp != 0 { + profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGPROF), true) + } + // RLIMIT_CPU soft limit + newRlimitCPUSoftSetting, exp := tg.rlimitCPUSoftSetting.At(tgProfNow) + tg.rlimitCPUSoftSetting = newRlimitCPUSoftSetting + if exp != 0 { + profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGXCPU), true) + } + // RLIMIT_CPU hard limit + rlimitCPUMax := tg.limits.Get(limits.CPU).Max + if rlimitCPUMax != limits.Infinity && !tgProfNow.Before(ktime.FromSeconds(int64(rlimitCPUMax))) { + profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGKILL), true) + } } - // RLIMIT_CPU soft limit - newRlimitCPUSoftSetting, exp := tg.rlimitCPUSoftSetting.At(tgProfNow) - tg.rlimitCPUSoftSetting = newRlimitCPUSoftSetting - if exp != 0 { - profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGXCPU), true) - } - // RLIMIT_CPU hard limit - rlimitCPUMax := tg.limits.Get(limits.CPU).Max - if rlimitCPUMax != limits.Infinity && !tgProfNow.Before(ktime.FromSeconds(int64(rlimitCPUMax))) { - profReceiver.sendSignalLocked(SignalInfoPriv(linux.SIGKILL), true) - } - } - tg.signalHandlers.mu.Unlock() + tg.signalHandlers.mu.Unlock() - ticker.k.tasks.mu.RUnlock() + k.tasks.mu.RUnlock() + } + + k.cpuClockMu.Unlock() + + // Retain tgs between calls to Notify to reduce allocations. + for i := range tgs { + tgs[i] = nil + } + tgs = tgs[:0] } - - // Retain tgs between calls to Notify to reduce allocations. - for i := range tgs { - tgs[i] = nil - } - ticker.tgs = tgs[:0] - - // If nothing is running, we can disable the timer. - tasks := ticker.k.runningTasks.Load() - if tasks == 0 { - ticker.k.runningTasksMu.Lock() - defer ticker.k.runningTasksMu.Unlock() - tasks := ticker.k.runningTasks.Load() - if tasks != 0 { - // Raced with a 0 -> 1 transition. - return setting, false - } - - // Stop the timer. We must cache the current setting so the - // kernel can access it without violating the lock order. - ticker.k.cpuClockTickerSetting = setting - ticker.k.cpuClockTickerDisabled = true - setting.Enabled = false - return setting, true - } - - return setting, false } // randInt31n returns a random integer in [0, n). @@ -494,27 +490,27 @@ func randInt31n(rng *rand.Rand, n int32) int32 { // // Preconditions: The caller must be running on the task goroutine. func (t *Task) NotifyRlimitCPUUpdated() { - t.k.cpuClockTicker.Atomically(func() { - t.tg.pidns.owner.mu.RLock() - defer t.tg.pidns.owner.mu.RUnlock() - t.tg.signalHandlers.mu.Lock() - defer t.tg.signalHandlers.mu.Unlock() - rlimitCPU := t.tg.limits.Get(limits.CPU) - t.tg.rlimitCPUSoftSetting = ktime.Setting{ - Enabled: rlimitCPU.Cur != limits.Infinity, - Next: ktime.FromNanoseconds((time.Duration(rlimitCPU.Cur) * time.Second).Nanoseconds()), - Period: time.Second, + t.k.cpuClockMu.Lock() + defer t.k.cpuClockMu.Unlock() + t.tg.pidns.owner.mu.RLock() + defer t.tg.pidns.owner.mu.RUnlock() + t.tg.signalHandlers.mu.Lock() + defer t.tg.signalHandlers.mu.Unlock() + rlimitCPU := t.tg.limits.Get(limits.CPU) + t.tg.rlimitCPUSoftSetting = ktime.Setting{ + Enabled: rlimitCPU.Cur != limits.Infinity, + Next: ktime.FromNanoseconds((time.Duration(rlimitCPU.Cur) * time.Second).Nanoseconds()), + Period: time.Second, + } + if rlimitCPU.Max != limits.Infinity { + // Check if tg is already over the hard limit. + tgcpu := t.tg.cpuStatsAtLocked(t.k.CPUClockNow()) + tgProfNow := ktime.FromNanoseconds((tgcpu.UserTime + tgcpu.SysTime).Nanoseconds()) + if !tgProfNow.Before(ktime.FromSeconds(int64(rlimitCPU.Max))) { + t.sendSignalLocked(SignalInfoPriv(linux.SIGKILL), true) } - if rlimitCPU.Max != limits.Infinity { - // Check if tg is already over the hard limit. - tgcpu := t.tg.cpuStatsAtLocked(t.k.CPUClockNow()) - tgProfNow := ktime.FromNanoseconds((tgcpu.UserTime + tgcpu.SysTime).Nanoseconds()) - if !tgProfNow.Before(ktime.FromSeconds(int64(rlimitCPU.Max))) { - t.sendSignalLocked(SignalInfoPriv(linux.SIGKILL), true) - } - } - t.tg.updateCPUTimersEnabledLocked() - }) + } + t.tg.updateCPUTimersEnabledLocked() } // Preconditions: The signal mutex must be locked.