Skip to content

Commit 2e3f430

Browse files
committed
kmodule: insert independent modules in parallel
kernel modules are allowed to run thier initialization code in parallel, and for modules that take a long time (e.g. probing hardware, etc.), we can noticably reduce boot times by loading modules in parallel. We want to avoid calling loadModule() for the same dep multiple times as the kernel will return EEXIST for the duplicate module load (plus the wasted cycles loading a duplicate image). Adds a concurrent-safe parallelProbeDep() loading implementation, and exports ParallelProbeOptions() which loads a list of modules in parallel. Also update ProbeOptions() to use the new loading impl. Probe(), ProbeOptions(), and the new ParallelProbeOptions() themselves remain concurrent-unsafe. There is a concurrent-safe API possible here if we choose to export depMap & parallelProbe[Dep]. Fixes #3424 Signed-off-by: Khazhismel Kumykov <khazhy@google.com>
1 parent 5e77210 commit 2e3f430

2 files changed

Lines changed: 166 additions & 45 deletions

File tree

pkg/kmodule/kmodule_linux.go

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616
"path"
1717
"path/filepath"
1818
"strings"
19+
"sync"
1920

21+
"golang.org/x/sync/errgroup"
2022
"golang.org/x/sys/unix"
2123
)
2224

@@ -39,12 +41,20 @@ const (
3941
)
4042

4143
type dependency struct {
42-
state modState
43-
deps []string
44+
state modState
45+
deps []string
46+
loadOnce sync.Once
47+
err error
4448
}
4549

4650
type depMap map[string]*dependency
4751

52+
// ModOpts describes a module load request.
53+
type ModOpts struct {
54+
Name string
55+
Params string
56+
}
57+
4858
// ProbeOpts contains optional parameters to Probe.
4959
//
5060
// An empty ProbeOpts{} should lead to the default behavior.
@@ -103,6 +113,64 @@ func Delete(name string, flags uintptr) error {
103113
return unix.DeleteModule(name, int(flags))
104114
}
105115

116+
// parallelProbeDep loads a module and its dependencies
117+
// Returns once module is loaded.
118+
func parallelProbeDep(deps depMap, modPath string, params string, opts ProbeOpts, modStack []string) error {
119+
dep, present := deps[modPath]
120+
if !present {
121+
return fmt.Errorf("Module not in depmap %s", modPath)
122+
}
123+
124+
for _, seenMod := range modStack {
125+
if seenMod == modPath {
126+
return fmt.Errorf("Circular dependency detected.")
127+
}
128+
}
129+
130+
dep.loadOnce.Do(func() {
131+
var eg errgroup.Group
132+
133+
// If it's already loaded, don't try to load again
134+
if dep.state == builtin || dep.state == loaded {
135+
return
136+
}
137+
138+
dep.state = loading
139+
140+
for _, dep := range dep.deps {
141+
dep := dep
142+
eg.Go(func() error {
143+
return parallelProbeDep(deps, dep, "", opts, append(modStack, modPath))
144+
})
145+
}
146+
147+
err := eg.Wait()
148+
if err != nil {
149+
dep.err = err
150+
return
151+
}
152+
153+
err = loadModule(modPath, params, opts)
154+
if err != nil {
155+
dep.err = err
156+
return
157+
}
158+
dep.state = loaded
159+
dep.err = nil
160+
})
161+
return dep.err
162+
}
163+
164+
// parallelProbe translates to modPath then calls parallelProbeDep
165+
func parallelProbe(deps depMap, name string, params string, opts ProbeOpts) error {
166+
modPath, err := findModPath(name, deps)
167+
if err != nil {
168+
return fmt.Errorf("could not find module path %q: %w", name, err)
169+
}
170+
171+
return parallelProbeDep(deps, modPath, params, opts, []string{})
172+
}
173+
106174
// Probe loads the given kernel module and its dependencies.
107175
// It is calls ProbeOptions with the default ProbeOpts.
108176
func Probe(name string, modParams string) error {
@@ -117,24 +185,24 @@ func ProbeOptions(name, modParams string, opts ProbeOpts) error {
117185
return fmt.Errorf("could not generate dependency map %w", err)
118186
}
119187

120-
modPath, err := findModPath(name, deps)
121-
if err != nil {
122-
return fmt.Errorf("could not find module path %q: %w", name, err)
123-
}
124-
125-
dep := deps[modPath]
188+
return parallelProbe(deps, name, modParams, opts)
189+
}
126190

127-
if dep.state == builtin || dep.state == loaded {
128-
return nil
191+
// ProbeAllOptions loads the given modules in parallel.
192+
func ProbeAllOptions(modules []ModOpts, opts ProbeOpts) error {
193+
deps, err := genDeps(opts)
194+
if err != nil {
195+
return fmt.Errorf("could not generate dependency map %w", err)
129196
}
130197

131-
dep.state = loading
132-
for _, d := range dep.deps {
133-
if err := loadDeps(d, deps, opts); err != nil {
134-
return err
135-
}
198+
var eg errgroup.Group
199+
for _, modOpts := range modules {
200+
m := modOpts
201+
eg.Go(func() error {
202+
return parallelProbe(deps, m.Name, m.Params, opts)
203+
})
136204
}
137-
return loadModule(modPath, modParams, opts)
205+
return eg.Wait()
138206
}
139207

140208
func checkBuiltin(moduleDir string, deps depMap) error {
@@ -239,35 +307,6 @@ func findModPath(name string, m depMap) (string, error) {
239307
return "", fmt.Errorf("could not find path for module %q", name)
240308
}
241309

242-
func loadDeps(path string, m depMap, opts ProbeOpts) error {
243-
dependency, ok := m[path]
244-
if !ok {
245-
return fmt.Errorf("could not find dependency %q", path)
246-
}
247-
248-
if dependency.state == loading {
249-
return fmt.Errorf("circular dependency! %q already LOADING", path)
250-
} else if (dependency.state == loaded) || (dependency.state == builtin) {
251-
return nil
252-
}
253-
254-
m[path].state = loading
255-
256-
for _, dep := range dependency.deps {
257-
if err := loadDeps(dep, m, opts); err != nil {
258-
return err
259-
}
260-
}
261-
262-
// done with dependencies, load module
263-
if err := loadModule(path, "", opts); err != nil {
264-
return err
265-
}
266-
m[path].state = loaded
267-
268-
return nil
269-
}
270-
271310
func loadModule(path, modParams string, opts ProbeOpts) error {
272311
if opts.DryRunCB != nil {
273312
opts.DryRunCB(path)

pkg/kmodule/kmodule_linux_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
"os"
1212
"path"
1313
"testing"
14+
"time"
1415

1516
"github.com/klauspost/compress/zstd"
1617
"github.com/ulikunitz/xz"
18+
"golang.org/x/sync/errgroup"
1719
)
1820

1921
var procModsMock = `hid_generic 16384 0 - Live 0x0000000000000000
@@ -39,6 +41,86 @@ func TestGenLoadedMods(t *testing.T) {
3941
}
4042
}
4143

44+
func TestParallelLoad(t *testing.T) {
45+
loadTime := 100 * time.Millisecond
46+
47+
m := depMap{
48+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/hid-generic.ko": &dependency{},
49+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/usbhid/usbhid.ko": &dependency{},
50+
"/lib/modules/6.6.6-generic/kernel/crypto/ccm.ko": &dependency{},
51+
"/lib/modules/6.6.6-generic/kernel/tests/depmod.ko": &dependency{
52+
deps: []string{"/lib/modules/6.6.6-generic/kernel/crypto/ccm.ko",
53+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/usbhid/usbhid.ko",
54+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/hid-generic.ko",
55+
},
56+
},
57+
"/lib/modules/6.6.6-generic/kernel/tests/depmod2.ko": &dependency{
58+
deps: []string{"/lib/modules/6.6.6-generic/kernel/crypto/ccm.ko",
59+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/usbhid/usbhid.ko",
60+
},
61+
},
62+
}
63+
64+
var eg errgroup.Group
65+
66+
// Wait time encourages racing between dependencies.
67+
slow_opts := ProbeOpts{DryRunCB: func(path string) { time.Sleep(loadTime) }}
68+
69+
eg.Go(func() error {
70+
return parallelProbeDep(
71+
m, "/lib/modules/6.6.6-generic/kernel/tests/depmod.ko", "", slow_opts, []string{})
72+
})
73+
eg.Go(func() error {
74+
return parallelProbeDep(
75+
m, "/lib/modules/6.6.6-generic/kernel/tests/depmod2.ko", "", slow_opts, []string{})
76+
})
77+
78+
start := time.Now()
79+
err := eg.Wait()
80+
81+
if err != nil {
82+
t.Fatalf("Probing failed: %v", err)
83+
}
84+
85+
// Racy... longest parallel chain is 2, and we load 5 total modules.
86+
if time.Since(start) > loadTime*3 {
87+
t.Fatalf("module loading slow")
88+
}
89+
90+
for mod, d := range m {
91+
if d.state != loaded {
92+
t.Fatalf("mod %q should have been loaded", path.Base(mod))
93+
}
94+
}
95+
}
96+
97+
func TestInvalidCircularLoad(t *testing.T) {
98+
m := depMap{
99+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/hid-generic.ko": &dependency{},
100+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/usbhid/usbhid.ko": &dependency{},
101+
"/lib/modules/6.6.6-generic/kernel/crypto/ccm.ko": &dependency{},
102+
"/lib/modules/6.6.6-generic/kernel/tests/circlemod.ko": &dependency{
103+
deps: []string{"/lib/modules/6.6.6-generic/kernel/tests/depmod.ko"},
104+
},
105+
"/lib/modules/6.6.6-generic/kernel/tests/depmod.ko": &dependency{
106+
deps: []string{"/lib/modules/6.6.6-generic/kernel/crypto/ccm.ko",
107+
"/lib/modules/6.6.6-generic/kernel/drivers/hid/usbhid/usbhid.ko",
108+
"/lib/modules/6.6.6-generic/kernel/tests/circlemod.ko",
109+
},
110+
},
111+
}
112+
113+
noop_opts := ProbeOpts{DryRunCB: func(path string) {}}
114+
115+
err := parallelProbeDep(
116+
m, "/lib/modules/6.6.6-generic/kernel/tests/depmod.ko", "", noop_opts, []string{})
117+
118+
if err == nil {
119+
// If we reach this, we're probably hung.
120+
t.Fatalf("Circular dep should have errored...")
121+
}
122+
}
123+
42124
// Helper function to generate compression test data for TestCompression.
43125
// Generates a map with the name of the file as key and the compressed data as value.
44126
// The data is compressed using xz, gzip, and zstd.

0 commit comments

Comments
 (0)