Skip to content

Commit 8eec925

Browse files
committed
mount/mountinfo_linux: parser speed up
The mountinfo parser implemented via `fmt.Sscanf()` is slower than the one using `strings.Split()` and `strconv.Atoi()`. This rewrite helps to speed it up to a factor of 8x, here is a result from `go bench`: > BenchmarkParsingScanf-4 300 22294112 ns/op > BenchmarkParsingSplit-4 3000 2780703 ns/op I tried other approaches, such as using `fmt.Sscanf()` for the first three (integer) fields and `strings.Split()` for the rest, but it slows things down considerably: > BenchmarkParsingMixed-4 1000 8827058 ns/op Note the old code uses `fmt.Sscanf` first, then a linear search for the '-' field, then a split for the last 3 fields. The new code relies on a single split. One other thing is, the new code is more future proof as it skips extra optional fields before the separator (currently there are none). I have also added more comments to aid in future development. Finally, the test data is fixed to not have white space before the first field. Based on a similar change in Moby, moby/moby#36091 [v2: remove no-op break statement to silence staticcheck] Signed-off-by: Kir Kolyshkin <[email protected]>
1 parent d8aef11 commit 8eec925

2 files changed

Lines changed: 125 additions & 100 deletions

File tree

mount/mountinfo_linux.go

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,10 @@ import (
2323
"fmt"
2424
"io"
2525
"os"
26+
"strconv"
2627
"strings"
2728
)
2829

29-
const (
30-
/* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
31-
(1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
32-
33-
(1) mount ID: unique identifier of the mount (may be reused after umount)
34-
(2) parent ID: ID of parent (or of self for the top of the mount tree)
35-
(3) major:minor: value of st_dev for files on filesystem
36-
(4) root: root of the mount within the filesystem
37-
(5) mount point: mount point relative to the process's root
38-
(6) mount options: per mount options
39-
(7) optional fields: zero or more fields of the form "tag[:value]"
40-
(8) separator: marks the end of the optional fields
41-
(9) filesystem type: name of filesystem of the form "type[.subtype]"
42-
(10) mount source: filesystem specific information or "none"
43-
(11) super options: per super block options*/
44-
mountinfoFormat = "%d %d %d:%d %s %s %s %s"
45-
)
46-
4730
// Self retrieves a list of mounts for the current running process.
4831
func Self() ([]Info, error) {
4932
f, err := os.Open("/proc/self/mountinfo")
@@ -56,41 +39,83 @@ func Self() ([]Info, error) {
5639
}
5740

5841
func parseInfoFile(r io.Reader) ([]Info, error) {
59-
var (
60-
s = bufio.NewScanner(r)
61-
out = []Info{}
62-
)
42+
s := bufio.NewScanner(r)
43+
out := []Info{}
6344

6445
for s.Scan() {
6546
if err := s.Err(); err != nil {
6647
return nil, err
6748
}
6849

69-
var (
70-
p = Info{}
71-
text = s.Text()
72-
optionalFields string
73-
)
74-
75-
if _, err := fmt.Sscanf(text, mountinfoFormat,
76-
&p.ID, &p.Parent, &p.Major, &p.Minor,
77-
&p.Root, &p.Mountpoint, &p.Options, &optionalFields); err != nil {
78-
return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err)
50+
/*
51+
36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
52+
(1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
53+
(1) mount ID: unique identifier of the mount (may be reused after umount)
54+
(2) parent ID: ID of parent (or of self for the top of the mount tree)
55+
(3) major:minor: value of st_dev for files on filesystem
56+
(4) root: root of the mount within the filesystem
57+
(5) mount point: mount point relative to the process's root
58+
(6) mount options: per mount options
59+
(7) optional fields: zero or more fields of the form "tag[:value]"
60+
(8) separator: marks the end of the optional fields
61+
(9) filesystem type: name of filesystem of the form "type[.subtype]"
62+
(10) mount source: filesystem specific information or "none"
63+
(11) super options: per super block options
64+
*/
65+
66+
text := s.Text()
67+
fields := strings.Split(text, " ")
68+
numFields := len(fields)
69+
if numFields < 10 {
70+
// should be at least 10 fields
71+
return nil, fmt.Errorf("Parsing '%s' failed: not enough fields (%d)", text, numFields)
7972
}
80-
// Safe as mountinfo encodes mountpoints with spaces as \040.
81-
index := strings.Index(text, " - ")
82-
postSeparatorFields := strings.Fields(text[index+3:])
83-
if len(postSeparatorFields) < 3 {
84-
return nil, fmt.Errorf("Error found less than 3 fields post '-' in %q", text)
73+
p := Info{}
74+
// ignore any numbers parsing errors, as there should not be any
75+
p.ID, _ = strconv.Atoi(fields[0])
76+
p.Parent, _ = strconv.Atoi(fields[1])
77+
mm := strings.Split(fields[2], ":")
78+
if len(mm) != 2 {
79+
return nil, fmt.Errorf("Parsing '%s' failed: unexpected minor:major pair %s", text, mm)
8580
}
86-
87-
if optionalFields != "-" {
88-
p.Optional = optionalFields
81+
p.Major, _ = strconv.Atoi(mm[0])
82+
p.Minor, _ = strconv.Atoi(mm[1])
83+
84+
p.Root = fields[3]
85+
p.Mountpoint = fields[4]
86+
p.Options = fields[5]
87+
88+
// one or more optional fields, when a separator (-)
89+
i := 6
90+
for ; i < numFields && fields[i] != "-"; i++ {
91+
switch i {
92+
case 6:
93+
p.Optional = fields[6]
94+
default:
95+
/* NOTE there might be more optional fields before the separator
96+
such as fields[7]...fields[N] (where N < separatorIndex),
97+
although as of Linux kernel 4.15 the only known ones are
98+
mount propagation flags in fields[6]. The correct
99+
behavior is to ignore any unknown optional fields.
100+
*/
101+
}
102+
}
103+
if i == numFields {
104+
return nil, fmt.Errorf("Parsing '%s' failed: missing separator ('-')", text)
105+
}
106+
// There should be 3 fields after the separator...
107+
if i+4 > numFields {
108+
return nil, fmt.Errorf("Parsing '%s' failed: not enough fields after a separator", text)
89109
}
110+
// ... but in Linux <= 3.9 mounting a cifs with spaces in a share name
111+
// (like "//serv/My Documents") _may_ end up having a space in the last field
112+
// of mountinfo (like "unc=//serv/My Documents"). Since kernel 3.10-rc1, cifs
113+
// option unc= is ignored, so a space should not appear. In here we ignore
114+
// those "extra" fields caused by extra spaces.
115+
p.FSType = fields[i+1]
116+
p.Source = fields[i+2]
117+
p.VFSOptions = fields[i+3]
90118

91-
p.FSType = postSeparatorFields[0]
92-
p.Source = postSeparatorFields[1]
93-
p.VFSOptions = strings.Join(postSeparatorFields[2:], " ")
94119
out = append(out, p)
95120
}
96121
return out, nil

0 commit comments

Comments
 (0)