diff --git a/cmd/pidtree/treevisitor.go b/cmd/pidtree/treevisitor.go index 6ec3eecb..1306bf74 100644 --- a/cmd/pidtree/treevisitor.go +++ b/cmd/pidtree/treevisitor.go @@ -18,7 +18,7 @@ package main import ( "reflect" - "sort" + "slices" "github.com/thediveo/lxkns/cmd/internal/tool" "github.com/thediveo/lxkns/model" @@ -74,8 +74,8 @@ func (v *TreeVisitor) Get(node reflect.Value) ( clist := []interface{}{} if proc, ok := node.Interface().(*model.Process); ok { pidns := proc.Namespaces[model.PIDNS] - childprocesses := model.ProcessListByPID(proc.Children) - sort.Sort(childprocesses) + childprocesses := slices.Clone(proc.Children) + slices.SortFunc(childprocesses, model.SortProcessByPID) childpidns := map[species.NamespaceID]bool{} for _, childproc := range childprocesses { if childproc.Namespaces[model.PIDNS] == pidns { @@ -104,8 +104,8 @@ func (v *TreeVisitor) Get(node reflect.Value) ( } else { // The child nodes of a PID namespace tree node will be the "leader" // (or "topmost") processes inside the PID namespace. - leaders := model.ProcessListByPID(node.Interface().(model.Namespace).Leaders()) - sort.Sort(leaders) + leaders := slices.Clone(node.Interface().(model.Namespace).Leaders()) + slices.SortFunc(leaders, model.SortProcessByPID) for _, proc := range leaders { clist = append(clist, proc) } diff --git a/model/process.go b/model/process.go index 0ebbb644..2378801f 100644 --- a/model/process.go +++ b/model/process.go @@ -381,16 +381,6 @@ func (t ProcessTable) ProcessesByPIDs(pid ...PIDType) []*Process { return procs } -// ProcessListByPID is a type alias for sorting slices of *[model.Process] by -// their PIDs in numerically ascending order. -type ProcessListByPID []*Process - -func (l ProcessListByPID) Len() int { return len(l) } -func (l ProcessListByPID) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func (l ProcessListByPID) Less(i, j int) bool { - return l[i].PID < l[j].PID -} - // newTaskFromStatline parses a task (process) status line (as read from // /proc/[PID]/task/[TID]/status) into a Task object. func newTaskFromStatline(procstat string, proc *Process) (task *Task) { diff --git a/model/process_sort.go b/model/process_sort.go new file mode 100644 index 00000000..9d40bbbe --- /dev/null +++ b/model/process_sort.go @@ -0,0 +1,83 @@ +// Copyright 2024 Harald Albrecht. +// +// 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 model + +import ( + "os" + "strconv" + "strings" +) + +// SortProcessByPID sorts processes by increasing PID numbers (no interval +// arithmetics though). +func SortProcessByPID(a, b *Process) int { + return int(a.PID) - int(b.PID) +} + +// SortProcessByAgeThenPIDDistance sorts processes first by their “age” +// (starttime) and then by their PIDs, taking PID number wrap-arounds into +// consideration. +// +// As PIDs are monotonously increasing, wrapping around at “N” (which defaults +// to 1<<22 on Linux 64 bit systems), we consider a PID “B” to be after PID “A” +// if the “positive” distance from “A” to “B” (in increasing PIDs, distance +// taken modulo N) is at most N/2. +// +// For a nice write-up see also [The ryg blog: Intervals in modular arithmetic]. +// +// [The ryg blog: Intervals in modular arithmetic]: https://fgiesen.wordpress.com/2015/09/24/intervals-in-modular-arithmetic/ +func SortProcessByAgeThenPIDDistance(a, b *Process) int { + switch { + case a.Starttime < b.Starttime: + return -1 + case a.Starttime > b.Starttime: + return 1 + } + pidA := uint64(a.PID) + pidB := uint64(b.PID) + switch dist := (pidB - pidA) & pidMaxMask; { + case dist == 0: + return 0 + case dist <= pidMaxDist: + return -1 + default: + return 1 + } +} + +var pidMaxMask uint64 // N-1 +var pidMaxDist uint64 // N/2 + +// pidWrapping reads the PID interval “N” set for this system (which must be to +// the power of two) and then returns N-1 and N/2, falling back to the specified +// default N in case the system configuration cannot be read. +func pidWrapping(defaultMax uint64) (mask, maxdist uint64) { + mask = defaultMax - 1 + maxdist = defaultMax >> 1 + // https://www.man7.org/linux/man-pages/man5/proc_sys_kernel.5.html + pidmaxb, err := os.ReadFile("/proc/sys/kernel/pid_max") + if err != nil { + return + } + pidmax, err := strconv.ParseUint(strings.TrimSuffix(string(pidmaxb), "\n"), 10, 32) + if err != nil { + return + } + return pidmax - 1, pidmax >> 1 +} + +func init() { + pidMaxMask, pidMaxDist = pidWrapping((uint64(1) << 22)) +} diff --git a/model/process_sort_test.go b/model/process_sort_test.go new file mode 100644 index 00000000..ed24258e --- /dev/null +++ b/model/process_sort_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 Harald Albrecht. +// +// 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 model + +import ( + deco "github.com/onsi/ginkgo/v2/dsl/decorators" + + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/ginkgo/v2/dsl/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Mr Wehrli sorting processes", func() { + + It("detects the system's PID wrap-around", func() { + mask, dist := pidWrapping(0) + Expect(mask).NotTo(BeZero()) + Expect(dist).NotTo(BeZero()) + Expect((dist << 1) - 1).To(Equal(mask)) + }) + + Context("interval arithmetic", deco.Ordered, func() { + + BeforeAll(func() { + pidMaxMask, pidMaxDist = 8-1, 8>>1 + DeferCleanup(func() { + pidMaxMask, pidMaxDist = pidWrapping((uint64(1) << 22)) + }) + }) + + DescribeTable("sorting by age and PID distance", + func(ageA int, pidA int, ageB int, pidB int, expect int) { + delta := SortProcessByAgeThenPIDDistance( + &Process{PID: PIDType(pidA), ProTaskCommon: ProTaskCommon{Starttime: uint64(ageA)}}, + &Process{PID: PIDType(pidB), ProTaskCommon: ProTaskCommon{Starttime: uint64(ageB)}}) + Expect(delta).To(Equal(expect), + "(%d-%d)&%x=%x ?? %x", pidA, pidB, pidMaxMask, (pidB-pidA)&int(pidMaxMask), pidMaxDist) + }, + Entry("a older than b", 100, 1, 200, 2, -1), + Entry("a younger than b", 200, 1, 100, 2, 1), + Entry("a same age as b, PID a before PID b, nowrap", 100, 4, 100, 5, -1), + Entry("a same age as b, PID b before PID a, nowrap", 100, 5, 100, 4, 1), + Entry("a same age as b, PID a before PID b, wrap", 100, 7, 100, 1, -1), + Entry("a same age as b, PID b before PID a, wrap", 100, 1, 100, 7, 1), + ) + + }) + +}) diff --git a/model/process_test.go b/model/process_test.go index 3c795c7b..d1193616 100644 --- a/model/process_test.go +++ b/model/process_test.go @@ -17,7 +17,7 @@ package model import ( "os" "runtime" - "sort" + "slices" "strconv" "time" @@ -226,7 +226,7 @@ var _ = Describe("process lists", func() { {p42, p1}, } for _, pl := range pls { - sort.Sort(ProcessListByPID(pl)) + slices.SortFunc(pl, SortProcessByPID) Expect(pl[0].PID).To(Equal(PIDType(1))) Expect(pl[1].PID).To(Equal(PIDType(42))) }