Repository: prometheus/procfs
Pull request #490 (2023-01-25 07:33:06)

add support CpusAllowedList for /proc/[pid]/status
Hi, 

It may be usefull to collect CPU affinity info about processes. 

For example, we can monitor:

- Is out processes running on the same group of cores
- Is there too many proceeses running on the same group of cores

Tests passes.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d3ad0f9..25a6795 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,7 +4,7 @@ version: 2.1
 jobs:
   lint:
     docker:
-    - image: cimg/go:1.19
+    - image: cimg/go:1.20
     steps:
     - checkout
     - run: make check_license
@@ -55,8 +55,8 @@ workflows:
         matrix:
           parameters:
             go_version:
-            - "1.18"
             - "1.19"
+            - "1.20"
     - test:
         name: test-windows
         os: windows
@@ -64,6 +64,6 @@ workflows:
         matrix:
           parameters:
             go_version:
-            - "1.18"
             - "1.19"
+            - "1.20"
     - codespell
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 2ba5e88..100cf93 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -18,13 +18,13 @@ jobs:
       - name: Checkout repository
         uses: actions/checkout@v3
       - name: install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          go-version: 1.18.x
+          go-version: 1.20.x
       - name: Install snmp_exporter/generator dependencies
         run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
         if: github.repository == 'prometheus/snmp_exporter'
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.3.1
+        uses: golangci/golangci-lint-action@v3.4.0
         with:
-          version: v1.50.1
+          version: v1.51.2
diff --git a/Makefile.common b/Makefile.common
index 1985fcd..b111d25 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,7 +61,7 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.50.1
+GOLANGCI_LINT_VERSION ?= v1.51.2
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
@@ -91,6 +91,8 @@ BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
 PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
 TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))
 
+SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))
+
 ifeq ($(GOHOSTARCH),amd64)
         ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))
                 # Only supported on amd64
@@ -205,7 +207,7 @@ common-tarball: promu
 .PHONY: common-docker $(BUILD_DOCKER_ARCHS)
 common-docker: $(BUILD_DOCKER_ARCHS)
 $(BUILD_DOCKER_ARCHS): common-docker-%:
-	docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" \
+	docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
 		-f $(DOCKERFILE_PATH) \
 		--build-arg ARCH="$*" \
 		--build-arg OS="linux" \
@@ -214,19 +216,19 @@ $(BUILD_DOCKER_ARCHS): common-docker-%:
 .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
 common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
 $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
-	docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)"
+	docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
 
 DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
 .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
 common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
 $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
-	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
-	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
+	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
+	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
 
 .PHONY: common-docker-manifest
 common-docker-manifest:
-	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(DOCKER_IMAGE_TAG))
-	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)"
+	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
+	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
 
 .PHONY: promu
 promu: $(PROMU)
diff --git a/fs.go b/fs.go
index 0102ab0..df9044e 100644
--- a/fs.go
+++ b/fs.go
@@ -14,6 +14,8 @@
 package procfs
 
 import (
+	"syscall"
+
 	"github.com/prometheus/procfs/internal/fs"
 )
 
@@ -21,6 +23,7 @@ import (
 // kernel data structures.
 type FS struct {
 	proc fs.FS
+	real bool
 }
 
 // DefaultMountPoint is the common mount point of the proc filesystem.
@@ -39,5 +42,23 @@ func NewFS(mountPoint string) (FS, error) {
 	if err != nil {
 		return FS{}, err
 	}
-	return FS{fs}, nil
+
+	real, err := isRealProc(mountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+
+	return FS{fs, real}, nil
+}
+
+// isRealProc determines whether supplied mountpoint is really a proc filesystem.
+func isRealProc(mountPoint string) (bool, error) {
+	stat := syscall.Statfs_t{}
+	err := syscall.Statfs(mountPoint, &stat)
+	if err != nil {
+		return false, err
+	}
+
+	// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
+	return stat.Type == 0x9fa0, nil
 }
diff --git a/go.mod b/go.mod
index d88b439..6013506 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
 module github.com/prometheus/procfs
 
-go 1.18
+go 1.19
 
 require (
 	github.com/google/go-cmp v0.5.9
-	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.3.0
+	golang.org/x/sync v0.2.0
+	golang.org/x/sys v0.8.0
 )
diff --git a/go.sum b/go.sum
index f7fa868..4530cf5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/internal/util/parse.go b/internal/util/parse.go
index b030951..14272dc 100644
--- a/internal/util/parse.go
+++ b/internal/util/parse.go
@@ -64,6 +64,21 @@ func ParsePInt64s(ss []string) ([]*int64, error) {
 	return us, nil
 }
 
+// Parses a uint64 from given hex in string.
+func ParseHexUint64s(ss []string) ([]*uint64, error) {
+	us := make([]*uint64, 0, len(ss))
+	for _, s := range ss {
+		u, err := strconv.ParseUint(s, 16, 64)
+		if err != nil {
+			return nil, err
+		}
+
+		us = append(us, &u)
+	}
+
+	return us, nil
+}
+
 // ReadUintFromFile reads a file and attempts to parse a uint64 from it.
 func ReadUintFromFile(path string) (uint64, error) {
 	data, err := os.ReadFile(path)
diff --git a/mountstats.go b/mountstats.go
index 0c482c1..7f68890 100644
--- a/mountstats.go
+++ b/mountstats.go
@@ -186,6 +186,8 @@ type NFSOperationStats struct {
 	CumulativeTotalResponseMilliseconds uint64
 	// Duration from when a request was enqueued to when it was completely handled.
 	CumulativeTotalRequestMilliseconds uint64
+	// The average time from the point the client sends RPC requests until it receives the response.
+	AverageRTTMilliseconds float64
 	// The count of operations that complete with tk_status < 0.  These statuses usually indicate error conditions.
 	Errors uint64
 }
@@ -534,7 +536,6 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
 
 			ns = append(ns, n)
 		}
-
 		opStats := NFSOperationStats{
 			Operation:                           strings.TrimSuffix(ss[0], ":"),
 			Requests:                            ns[0],
@@ -546,6 +547,9 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
 			CumulativeTotalResponseMilliseconds: ns[6],
 			CumulativeTotalRequestMilliseconds:  ns[7],
 		}
+		if ns[0] != 0 {
+			opStats.AverageRTTMilliseconds = float64(ns[6]) / float64(ns[0])
+		}
 
 		if len(ns) > 8 {
 			opStats.Errors = ns[8]
diff --git a/net_conntrackstat.go b/net_conntrackstat.go
index 8300dac..64a0e94 100644
--- a/net_conntrackstat.go
+++ b/net_conntrackstat.go
@@ -18,7 +18,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"strconv"
 	"strings"
 
 	"github.com/prometheus/procfs/internal/util"
@@ -28,9 +27,13 @@ import (
 // and contains netfilter conntrack statistics at one CPU core.
 type ConntrackStatEntry struct {
 	Entries       uint64
+	Searched      uint64
 	Found         uint64
+	New           uint64
 	Invalid       uint64
 	Ignore        uint64
+	Delete        uint64
+	DeleteList    uint64
 	Insert        uint64
 	InsertFailed  uint64
 	Drop          uint64
@@ -81,73 +84,34 @@ func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
 
 // Parses a ConntrackStatEntry from given array of fields.
 func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
-	if len(fields) != 17 {
-		return nil, fmt.Errorf("invalid conntrackstat entry, missing fields")
-	}
-	entry := &ConntrackStatEntry{}
-
-	entries, err := parseConntrackStatField(fields[0])
-	if err != nil {
-		return nil, err
-	}
-	entry.Entries = entries
-
-	found, err := parseConntrackStatField(fields[2])
-	if err != nil {
-		return nil, err
-	}
-	entry.Found = found
-
-	invalid, err := parseConntrackStatField(fields[4])
-	if err != nil {
-		return nil, err
-	}
-	entry.Invalid = invalid
-
-	ignore, err := parseConntrackStatField(fields[5])
-	if err != nil {
-		return nil, err
-	}
-	entry.Ignore = ignore
-
-	insert, err := parseConntrackStatField(fields[8])
+	entries, err := util.ParseHexUint64s(fields)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("invalid conntrackstat entry, couldn't parse fields: %s", err)
 	}
-	entry.Insert = insert
-
-	insertFailed, err := parseConntrackStatField(fields[9])
-	if err != nil {
-		return nil, err
+	numEntries := len(entries)
+	if numEntries < 16 || numEntries > 17 {
+		return nil, fmt.Errorf("invalid conntrackstat entry, invalid number of fields: %d", numEntries)
 	}
-	entry.InsertFailed = insertFailed
 
-	drop, err := parseConntrackStatField(fields[10])
-	if err != nil {
-		return nil, err
+	stats := &ConntrackStatEntry{
+		Entries:      *entries[0],
+		Searched:     *entries[1],
+		Found:        *entries[2],
+		New:          *entries[3],
+		Invalid:      *entries[4],
+		Ignore:       *entries[5],
+		Delete:       *entries[6],
+		DeleteList:   *entries[7],
+		Insert:       *entries[8],
+		InsertFailed: *entries[9],
+		Drop:         *entries[10],
+		EarlyDrop:    *entries[11],
 	}
-	entry.Drop = drop
 
-	earlyDrop, err := parseConntrackStatField(fields[11])
-	if err != nil {
-		return nil, err
+	// Ignore missing search_restart on Linux < 2.6.35.
+	if numEntries == 17 {
+		stats.SearchRestart = *entries[16]
 	}
-	entry.EarlyDrop = earlyDrop
 
-	searchRestart, err := parseConntrackStatField(fields[16])
-	if err != nil {
-		return nil, err
-	}
-	entry.SearchRestart = searchRestart
-
-	return entry, nil
-}
-
-// Parses a uint64 from given hex in string.
-func parseConntrackStatField(field string) (uint64, error) {
-	val, err := strconv.ParseUint(field, 16, 64)
-	if err != nil {
-		return 0, fmt.Errorf("couldn't parse %q field: %w", field, err)
-	}
-	return val, err
+	return stats, nil
 }
diff --git a/net_wireless.go b/net_wireless.go
new file mode 100644
index 0000000..c80fb15
--- /dev/null
+++ b/net_wireless.go
@@ -0,0 +1,182 @@
+// Copyright 2023 The Prometheus 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 procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// Wireless models the content of /proc/net/wireless.
+type Wireless struct {
+	Name string
+
+	// Status is the current 4-digit hex value status of the interface.
+	Status uint64
+
+	// QualityLink is the link quality.
+	QualityLink int
+
+	// QualityLevel is the signal gain (dBm).
+	QualityLevel int
+
+	// QualityNoise is the signal noise baseline (dBm).
+	QualityNoise int
+
+	// DiscardedNwid is the number of discarded packets with wrong nwid/essid.
+	DiscardedNwid int
+
+	// DiscardedCrypt is the number of discarded packets with wrong code/decode (WEP).
+	DiscardedCrypt int
+
+	// DiscardedFrag is the number of discarded packets that can't perform MAC reassembly.
+	DiscardedFrag int
+
+	// DiscardedRetry is the number of discarded packets that reached max MAC retries.
+	DiscardedRetry int
+
+	// DiscardedMisc is the number of discarded packets for other reasons.
+	DiscardedMisc int
+
+	// MissedBeacon is the number of missed beacons/superframe.
+	MissedBeacon int
+}
+
+// Wireless returns kernel wireless statistics.
+func (fs FS) Wireless() ([]*Wireless, error) {
+	b, err := util.ReadFileNoStat(fs.proc.Path("net/wireless"))
+	if err != nil {
+		return nil, err
+	}
+
+	m, err := parseWireless(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse wireless: %w", err)
+	}
+
+	return m, nil
+}
+
+// parseWireless parses the contents of /proc/net/wireless.
+/*
+Inter-| sta-|   Quality        |   Discarded packets               | Missed | WE
+face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon | 22
+ eth1: 0000    5.  -256.  -10.       0      1      0     3      0        0
+ eth2: 0000    5.  -256.  -20.       0      2      0     4      0        0
+*/
+func parseWireless(r io.Reader) ([]*Wireless, error) {
+	var (
+		interfaces []*Wireless
+		scanner    = bufio.NewScanner(r)
+	)
+
+	for n := 0; scanner.Scan(); n++ {
+		// Skip the 2 header lines.
+		if n < 2 {
+			continue
+		}
+
+		line := scanner.Text()
+
+		parts := strings.Split(line, ":")
+		if len(parts) != 2 {
+			return nil, fmt.Errorf("expected 2 parts after splitting line by ':', got %d for line %q", len(parts), line)
+		}
+
+		name := strings.TrimSpace(parts[0])
+		stats := strings.Fields(parts[1])
+
+		if len(stats) < 10 {
+			return nil, fmt.Errorf("invalid number of fields in line %d, expected at least 10, got %d: %q", n, len(stats), line)
+		}
+
+		status, err := strconv.ParseUint(stats[0], 16, 16)
+		if err != nil {
+			return nil, fmt.Errorf("invalid status in line %d: %q", n, line)
+		}
+
+		qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:link as integer %q: %w", qlink, err)
+		}
+
+		qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:level as integer %q: %w", qlevel, err)
+		}
+
+		qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:noise as integer %q: %w", qnoise, err)
+		}
+
+		dnwid, err := strconv.Atoi(stats[4])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:nwid as integer %q: %w", dnwid, err)
+		}
+
+		dcrypt, err := strconv.Atoi(stats[5])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:crypt as integer %q: %w", dcrypt, err)
+		}
+
+		dfrag, err := strconv.Atoi(stats[6])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:frag as integer %q: %w", dfrag, err)
+		}
+
+		dretry, err := strconv.Atoi(stats[7])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:retry as integer %q: %w", dretry, err)
+		}
+
+		dmisc, err := strconv.Atoi(stats[8])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:misc as integer %q: %w", dmisc, err)
+		}
+
+		mbeacon, err := strconv.Atoi(stats[9])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Missed:beacon as integer %q: %w", mbeacon, err)
+		}
+
+		w := &Wireless{
+			Name:           name,
+			Status:         status,
+			QualityLink:    qlink,
+			QualityLevel:   qlevel,
+			QualityNoise:   qnoise,
+			DiscardedNwid:  dnwid,
+			DiscardedCrypt: dcrypt,
+			DiscardedFrag:  dfrag,
+			DiscardedRetry: dretry,
+			DiscardedMisc:  dmisc,
+			MissedBeacon:   mbeacon,
+		}
+
+		interfaces = append(interfaces, w)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, fmt.Errorf("failed to scan /proc/net/wireless: %w", err)
+	}
+
+	return interfaces, nil
+}
diff --git a/netstat.go b/netstat.go
index 5cc40ae..742dff4 100644
--- a/netstat.go
+++ b/netstat.go
@@ -15,7 +15,6 @@ package procfs
 
 import (
 	"bufio"
-	"io"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -38,12 +37,7 @@ func (fs FS) NetStat() ([]NetStat, error) {
 	var netStatsTotal []NetStat
 
 	for _, filePath := range statFiles {
-		file, err := os.Open(filePath)
-		if err != nil {
-			return nil, err
-		}
-
-		procNetstat, err := parseNetstat(file)
+		procNetstat, err := parseNetstat(filePath)
 		if err != nil {
 			return nil, err
 		}
@@ -56,14 +50,17 @@ func (fs FS) NetStat() ([]NetStat, error) {
 
 // parseNetstat parses the metrics from `/proc/net/stat/` file
 // and returns a NetStat structure.
-func parseNetstat(r io.Reader) (NetStat, error) {
-	var (
-		scanner = bufio.NewScanner(r)
-		netStat = NetStat{
-			Stats: make(map[string][]uint64),
-		}
-	)
+func parseNetstat(filePath string) (NetStat, error) {
+	netStat := NetStat{
+		Stats: make(map[string][]uint64),
+	}
+	file, err := os.Open(filePath)
+	if err != nil {
+		return netStat, err
+	}
+	defer file.Close()
 
+	scanner := bufio.NewScanner(file)
 	scanner.Scan()
 
 	// First string is always a header for stats
diff --git a/proc.go b/proc.go
index c30223a..48f39da 100644
--- a/proc.go
+++ b/proc.go
@@ -21,7 +21,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -30,7 +29,7 @@ type Proc struct {
 	// The process ID.
 	PID int
 
-	fs fs.FS
+	fs FS
 }
 
 // Procs represents a list of Proc structs.
@@ -92,7 +91,7 @@ func (fs FS) Proc(pid int) (Proc, error) {
 	if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: pid, fs: fs.proc}, nil
+	return Proc{PID: pid, fs: fs}, nil
 }
 
 // AllProcs returns a list of all currently available processes.
@@ -114,7 +113,7 @@ func (fs FS) AllProcs() (Procs, error) {
 		if err != nil {
 			continue
 		}
-		p = append(p, Proc{PID: int(pid), fs: fs.proc})
+		p = append(p, Proc{PID: int(pid), fs: fs})
 	}
 
 	return p, nil
@@ -237,6 +236,19 @@ func (p Proc) FileDescriptorTargets() ([]string, error) {
 // FileDescriptorsLen returns the number of currently open file descriptors of
 // a process.
 func (p Proc) FileDescriptorsLen() (int, error) {
+	// Use fast path if available (Linux v6.2): https://github.com/torvalds/linux/commit/f1f1f2569901
+	if p.fs.real {
+		stat, err := os.Stat(p.path("fd"))
+		if err != nil {
+			return 0, err
+		}
+
+		size := stat.Size()
+		if size > 0 {
+			return int(size), nil
+		}
+	}
+
 	fds, err := p.fileDescriptors()
 	if err != nil {
 		return 0, err
@@ -285,7 +297,7 @@ func (p Proc) fileDescriptors() ([]string, error) {
 }
 
 func (p Proc) path(pa ...string) string {
-	return p.fs.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
+	return p.fs.proc.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
 }
 
 // FileDescriptorsInfo retrieves information about all file descriptors of
diff --git a/proc_stat.go b/proc_stat.go
index b278eb2..14b249f 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -18,7 +18,6 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -112,7 +111,7 @@ type ProcStat struct {
 	// Aggregated block I/O delays, measured in clock ticks (centiseconds).
 	DelayAcctBlkIOTicks uint64
 
-	proc fs.FS
+	proc FS
 }
 
 // NewStat returns the current status information of the process.
@@ -210,8 +209,7 @@ func (s ProcStat) ResidentMemory() int {
 
 // StartTime returns the unix timestamp of the process in seconds.
 func (s ProcStat) StartTime() (float64, error) {
-	fs := FS{proc: s.proc}
-	stat, err := fs.Stat()
+	stat, err := s.proc.Stat()
 	if err != nil {
 		return 0, err
 	}
diff --git a/proc_status.go b/proc_status.go
index 3d8c064..c055d07 100644
--- a/proc_status.go
+++ b/proc_status.go
@@ -15,6 +15,7 @@ package procfs
 
 import (
 	"bytes"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -76,6 +77,9 @@ type ProcStatus struct {
 	UIDs [4]string
 	// GIDs of the process (Real, effective, saved set, and filesystem GIDs)
 	GIDs [4]string
+
+	// CpusAllowedList: List of cpu cores processes are allowed to run on.
+	CpusAllowedList []uint64
 }
 
 // NewStatus returns the current status information of the process.
@@ -161,10 +165,38 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt
 		s.VoluntaryCtxtSwitches = vUint
 	case "nonvoluntary_ctxt_switches":
 		s.NonVoluntaryCtxtSwitches = vUint
+	case "Cpus_allowed_list":
+		s.CpusAllowedList = calcCpusAllowedList(vString)
 	}
+
 }
 
 // TotalCtxtSwitches returns the total context switch.
 func (s ProcStatus) TotalCtxtSwitches() uint64 {
 	return s.VoluntaryCtxtSwitches + s.NonVoluntaryCtxtSwitches
 }
+
+func calcCpusAllowedList(cpuString string) []uint64 {
+	s := strings.Split(cpuString, ",")
+
+	var g []uint64
+
+	for _, cpu := range s {
+		// parse cpu ranges, example: 1-3=[1,2,3]
+		if l := strings.Split(strings.TrimSpace(cpu), "-"); len(l) > 1 {
+			startCPU, _ := strconv.ParseUint(l[0], 10, 64)
+			endCPU, _ := strconv.ParseUint(l[1], 10, 64)
+
+			for i := startCPU; i <= endCPU; i++ {
+				g = append(g, i)
+			}
+		} else if len(l) == 1 {
+			cpu, _ := strconv.ParseUint(l[0], 10, 64)
+			g = append(g, cpu)
+		}
+
+	}
+
+	sort.Slice(g, func(i, j int) bool { return g[i] < g[j] })
+	return g
+}
diff --git a/sysfs/class_sas_device.go b/sysfs/class_sas_device.go
index 61361be..14014a7 100644
--- a/sysfs/class_sas_device.go
+++ b/sysfs/class_sas_device.go
@@ -17,7 +17,6 @@
 package sysfs
 
 import (
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -53,7 +52,7 @@ var (
 func (fs FS) parseSASDeviceClass(dir string) (SASDeviceClass, error) {
 	path := fs.sys.Path(dir)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -97,7 +96,7 @@ func (fs FS) parseSASDevice(name string) (*SASDevice, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasDeviceClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
@@ -139,7 +138,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasDeviceClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
@@ -148,7 +147,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 		if sasTargetDeviceRegexp.MatchString(d.Name()) {
 			targetdir := d.Name()
 
-			subtargets, err := ioutil.ReadDir(filepath.Join(devicepath, targetdir))
+			subtargets, err := os.ReadDir(filepath.Join(devicepath, targetdir))
 			if err != nil {
 				return nil, err
 			}
@@ -160,7 +159,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 					continue
 				}
 
-				blocks, err := ioutil.ReadDir(filepath.Join(devicepath, targetdir, targetsubdir.Name(), "block"))
+				blocks, err := os.ReadDir(filepath.Join(devicepath, targetdir, targetsubdir.Name(), "block"))
 				if err != nil {
 					if os.IsNotExist(err) {
 						continue
diff --git a/sysfs/class_sas_host.go b/sysfs/class_sas_host.go
index 783902b..fade1ca 100644
--- a/sysfs/class_sas_host.go
+++ b/sysfs/class_sas_host.go
@@ -17,7 +17,7 @@
 package sysfs
 
 import (
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"regexp"
 )
@@ -49,7 +49,7 @@ var (
 func (fs FS) SASHostClass() (SASHostClass, error) {
 	path := fs.sys.Path(sasHostClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +75,7 @@ func (fs FS) parseSASHost(name string) (*SASHost, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasHostClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/class_sas_phy.go b/sysfs/class_sas_phy.go
index 9e028db..e0e3a31 100644
--- a/sysfs/class_sas_phy.go
+++ b/sysfs/class_sas_phy.go
@@ -18,7 +18,6 @@ package sysfs
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -54,7 +53,7 @@ type SASPhyClass map[string]*SASPhy
 func (fs FS) SASPhyClass() (SASPhyClass, error) {
 	path := fs.sys.Path(sasPhyClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -88,7 +87,7 @@ func (fs FS) parseSASPhy(name string) (*SASPhy, error) {
 		}
 	}
 
-	files, err := ioutil.ReadDir(phypath)
+	files, err := os.ReadDir(phypath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/class_sas_port.go b/sysfs/class_sas_port.go
index 6d625bc..7afe251 100644
--- a/sysfs/class_sas_port.go
+++ b/sysfs/class_sas_port.go
@@ -17,7 +17,7 @@
 package sysfs
 
 import (
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"regexp"
 )
@@ -52,7 +52,7 @@ var (
 func (fs FS) SASPortClass() (SASPortClass, error) {
 	path := fs.sys.Path(sasPortClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -77,7 +77,7 @@ func (fs FS) parseSASPort(name string) (*SASPort, error) {
 
 	portpath := fs.sys.Path(filepath.Join(sasPortClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(portpath)
+	dirs, err := os.ReadDir(portpath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/mdraid.go b/sysfs/mdraid.go
new file mode 100644
index 0000000..ab9fb26
--- /dev/null
+++ b/sysfs/mdraid.go
@@ -0,0 +1,160 @@
+// Copyright 2023 The Prometheus 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.
+
+//go:build linux
+// +build linux
+
+package sysfs
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// Mdraid holds info parsed from relevant files in the /sys/block/md*/md directory.
+type Mdraid struct {
+	Device          string            // Kernel device name of array.
+	Level           string            // mdraid level.
+	ArrayState      string            // State of the array.
+	MetadataVersion string            // mdraid metadata version.
+	Disks           uint64            // Number of devices in a fully functional array.
+	Components      []MdraidComponent // mdraid component devices.
+	UUID            string            // UUID of the array.
+
+	// The following item is only valid for raid0, 4, 5, 6 and 10.
+	ChunkSize uint64 // Chunk size
+
+	// The following items are only valid for raid1, 4, 5, 6 and 10.
+	DegradedDisks uint64  // Number of degraded disks in the array.
+	SyncAction    string  // Current sync action.
+	SyncCompleted float64 // Fraction (0-1) representing the completion status of current sync operation.
+}
+
+type MdraidComponent struct {
+	Device string // Kernel device name.
+	State  string // Current state of device.
+}
+
+// Mdraids gathers information and statistics about mdraid devices present. Based on upstream
+// kernel documentation https://docs.kernel.org/admin-guide/md.html.
+func (fs FS) Mdraids() ([]Mdraid, error) {
+	matches, err := filepath.Glob(fs.sys.Path("block/md*/md"))
+	if err != nil {
+		return nil, err
+	}
+
+	mdraids := make([]Mdraid, 0)
+
+	for _, m := range matches {
+		md := Mdraid{Device: filepath.Base(filepath.Dir(m))}
+		path := fs.sys.Path("block", md.Device, "md")
+
+		if val, err := util.SysReadFile(filepath.Join(path, "level")); err == nil {
+			md.Level = val
+		} else {
+			return mdraids, err
+		}
+
+		// Array state can be one of: clear, inactive, readonly, read-auto, clean, active,
+		// write-pending, active-idle.
+		if val, err := util.SysReadFile(filepath.Join(path, "array_state")); err == nil {
+			md.ArrayState = val
+		} else {
+			return mdraids, err
+		}
+
+		if val, err := util.SysReadFile(filepath.Join(path, "metadata_version")); err == nil {
+			md.MetadataVersion = val
+		} else {
+			return mdraids, err
+		}
+
+		if val, err := util.ReadUintFromFile(filepath.Join(path, "raid_disks")); err == nil {
+			md.Disks = val
+		} else {
+			return mdraids, err
+		}
+
+		if val, err := util.SysReadFile(filepath.Join(path, "uuid")); err == nil {
+			md.UUID = val
+		} else {
+			return mdraids, err
+		}
+
+		if devs, err := filepath.Glob(filepath.Join(path, "dev-*")); err == nil {
+			for _, dev := range devs {
+				comp := MdraidComponent{Device: strings.TrimPrefix(filepath.Base(dev), "dev-")}
+
+				// Component state can be a comma-separated list of: faulty, in_sync, writemostly,
+				// blocked, spare, write_error, want_replacement, replacement.
+				if val, err := util.SysReadFile(filepath.Join(dev, "state")); err == nil {
+					comp.State = val
+				} else {
+					return mdraids, err
+				}
+
+				md.Components = append(md.Components, comp)
+			}
+		} else {
+			return mdraids, err
+		}
+
+		switch md.Level {
+		case "raid0", "raid4", "raid5", "raid6", "raid10":
+			if val, err := util.ReadUintFromFile(filepath.Join(path, "chunk_size")); err == nil {
+				md.ChunkSize = val
+			} else {
+				return mdraids, err
+			}
+		}
+
+		switch md.Level {
+		case "raid1", "raid4", "raid5", "raid6", "raid10":
+			if val, err := util.ReadUintFromFile(filepath.Join(path, "degraded")); err == nil {
+				md.DegradedDisks = val
+			} else {
+				return mdraids, err
+			}
+
+			// Array sync action can be one of: resync, recover, idle, check, repair.
+			if val, err := util.SysReadFile(filepath.Join(path, "sync_action")); err == nil {
+				md.SyncAction = val
+			} else {
+				return mdraids, err
+			}
+
+			if val, err := util.SysReadFile(filepath.Join(path, "sync_completed")); err == nil {
+				if val != "none" {
+					var a, b uint64
+
+					// File contains two values representing the fraction of number of completed
+					// sectors divided by number of total sectors to process.
+					if _, err := fmt.Sscanf(val, "%d / %d", &a, &b); err == nil {
+						md.SyncCompleted = float64(a) / float64(b)
+					} else {
+						return mdraids, err
+					}
+				}
+			} else {
+				return mdraids, err
+			}
+		}
+
+		mdraids = append(mdraids, md)
+	}
+
+	return mdraids, nil
+}
diff --git a/sysfs/net_class.go b/sysfs/net_class.go
index 0c8cb28..e1b1811 100644
--- a/sysfs/net_class.go
+++ b/sysfs/net_class.go
@@ -117,6 +117,77 @@ func (fs FS) NetClass() (NetClass, error) {
 	return netClass, nil
 }
 
+// ParseNetClassAttribute parses a given file in /sys/class/net/<iface>
+// and sets the value in a given NetClassIface object if the value was readable.
+// It returns an error if the file cannot be read.
+func ParseNetClassAttribute(devicePath, attrName string, interfaceClass *NetClassIface) error {
+	attrPath := filepath.Join(devicePath, attrName)
+	value, err := util.SysReadFile(attrPath)
+	if err != nil {
+		return fmt.Errorf("failed to read file %q: %w", attrPath, err)
+	}
+
+	vp := util.NewValueParser(value)
+	switch attrName {
+	case "addr_assign_type":
+		interfaceClass.AddrAssignType = vp.PInt64()
+	case "addr_len":
+		interfaceClass.AddrLen = vp.PInt64()
+	case "address":
+		interfaceClass.Address = value
+	case "broadcast":
+		interfaceClass.Broadcast = value
+	case "carrier":
+		interfaceClass.Carrier = vp.PInt64()
+	case "carrier_changes":
+		interfaceClass.CarrierChanges = vp.PInt64()
+	case "carrier_up_count":
+		interfaceClass.CarrierUpCount = vp.PInt64()
+	case "carrier_down_count":
+		interfaceClass.CarrierDownCount = vp.PInt64()
+	case "dev_id":
+		interfaceClass.DevID = vp.PInt64()
+	case "dormant":
+		interfaceClass.Dormant = vp.PInt64()
+	case "duplex":
+		interfaceClass.Duplex = value
+	case "flags":
+		interfaceClass.Flags = vp.PInt64()
+	case "ifalias":
+		interfaceClass.IfAlias = value
+	case "ifindex":
+		interfaceClass.IfIndex = vp.PInt64()
+	case "iflink":
+		interfaceClass.IfLink = vp.PInt64()
+	case "link_mode":
+		interfaceClass.LinkMode = vp.PInt64()
+	case "mtu":
+		interfaceClass.MTU = vp.PInt64()
+	case "name_assign_type":
+		interfaceClass.NameAssignType = vp.PInt64()
+	case "netdev_group":
+		interfaceClass.NetDevGroup = vp.PInt64()
+	case "operstate":
+		interfaceClass.OperState = value
+	case "phys_port_id":
+		interfaceClass.PhysPortID = value
+	case "phys_port_name":
+		interfaceClass.PhysPortName = value
+	case "phys_switch_id":
+		interfaceClass.PhysSwitchID = value
+	case "speed":
+		interfaceClass.Speed = vp.PInt64()
+	case "tx_queue_len":
+		interfaceClass.TxQueueLen = vp.PInt64()
+	case "type":
+		interfaceClass.Type = vp.PInt64()
+	default:
+		return nil
+	}
+
+	return nil
+}
+
 // parseNetClassIface scans predefined files in /sys/class/net/<iface>
 // directory and gets their contents.
 func parseNetClassIface(devicePath string) (*NetClassIface, error) {
@@ -131,68 +202,12 @@ func parseNetClassIface(devicePath string) (*NetClassIface, error) {
 		if !f.Type().IsRegular() {
 			continue
 		}
-		name := filepath.Join(devicePath, f.Name())
-		value, err := util.SysReadFile(name)
+		err := ParseNetClassAttribute(devicePath, f.Name(), &interfaceClass)
 		if err != nil {
-			if os.IsNotExist(err) || os.IsPermission(err) || err.Error() == "operation not supported" || errors.Is(err, os.ErrInvalid) {
-				continue
+			// Return fatal errors to caller
+			if !os.IsNotExist(err) && !os.IsPermission(err) && err.Error() != "operation not supported" && !errors.Is(err, os.ErrInvalid) {
+				return nil, err
 			}
-			return nil, fmt.Errorf("failed to read file %q: %w", name, err)
-		}
-		vp := util.NewValueParser(value)
-		switch f.Name() {
-		case "addr_assign_type":
-			interfaceClass.AddrAssignType = vp.PInt64()
-		case "addr_len":
-			interfaceClass.AddrLen = vp.PInt64()
-		case "address":
-			interfaceClass.Address = value
-		case "broadcast":
-			interfaceClass.Broadcast = value
-		case "carrier":
-			interfaceClass.Carrier = vp.PInt64()
-		case "carrier_changes":
-			interfaceClass.CarrierChanges = vp.PInt64()
-		case "carrier_up_count":
-			interfaceClass.CarrierUpCount = vp.PInt64()
-		case "carrier_down_count":
-			interfaceClass.CarrierDownCount = vp.PInt64()
-		case "dev_id":
-			interfaceClass.DevID = vp.PInt64()
-		case "dormant":
-			interfaceClass.Dormant = vp.PInt64()
-		case "duplex":
-			interfaceClass.Duplex = value
-		case "flags":
-			interfaceClass.Flags = vp.PInt64()
-		case "ifalias":
-			interfaceClass.IfAlias = value
-		case "ifindex":
-			interfaceClass.IfIndex = vp.PInt64()
-		case "iflink":
-			interfaceClass.IfLink = vp.PInt64()
-		case "link_mode":
-			interfaceClass.LinkMode = vp.PInt64()
-		case "mtu":
-			interfaceClass.MTU = vp.PInt64()
-		case "name_assign_type":
-			interfaceClass.NameAssignType = vp.PInt64()
-		case "netdev_group":
-			interfaceClass.NetDevGroup = vp.PInt64()
-		case "operstate":
-			interfaceClass.OperState = value
-		case "phys_port_id":
-			interfaceClass.PhysPortID = value
-		case "phys_port_name":
-			interfaceClass.PhysPortName = value
-		case "phys_switch_id":
-			interfaceClass.PhysSwitchID = value
-		case "speed":
-			interfaceClass.Speed = vp.PInt64()
-		case "tx_queue_len":
-			interfaceClass.TxQueueLen = vp.PInt64()
-		case "type":
-			interfaceClass.Type = vp.PInt64()
 		}
 	}
 
diff --git a/sysfs/system_cpu.go b/sysfs/system_cpu.go
index a8d0ac2..9e15858 100644
--- a/sysfs/system_cpu.go
+++ b/sysfs/system_cpu.go
@@ -142,6 +142,49 @@ func parseCPUThermalThrottle(cpuPath string) (*CPUThermalThrottle, error) {
 	return &t, nil
 }
 
+func binSearch(elem uint16, elemSlice *[]uint16) bool {
+	if len(*elemSlice) == 0 {
+		return false
+	}
+
+	if len(*elemSlice) == 1 {
+		return elem == (*elemSlice)[0]
+	}
+
+	start := 0
+	end := len(*elemSlice) - 1
+
+	var mid int
+
+	for start <= end {
+		mid = (start + end) / 2
+		if (*elemSlice)[mid] == elem {
+			return true
+		} else if (*elemSlice)[mid] > elem {
+			end = mid - 1
+		} else if (*elemSlice)[mid] < elem {
+			start = mid + 1
+		}
+	}
+
+	return false
+}
+
+func filterOfflineCPUs(offlineCpus *[]uint16, cpus *[]string) error {
+	for i, cpu := range *cpus {
+		cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu")
+		cpuNameUint16, err := strconv.Atoi(cpuName)
+		if err != nil {
+			return err
+		}
+		if binSearch(uint16(cpuNameUint16), offlineCpus) {
+			*cpus = append((*cpus)[:i], (*cpus)[i+1:]...)
+		}
+	}
+
+	return nil
+}
+
 // SystemCpufreq returns CPU frequency metrics for all CPUs.
 func (fs FS) SystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 	var g errgroup.Group
@@ -151,6 +194,25 @@ func (fs FS) SystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 		return nil, err
 	}
 
+	line, err := util.ReadFileNoStat(fs.sys.Path("devices/system/cpu/offline"))
+	if err != nil {
+		return nil, err
+	}
+
+	if strings.TrimSpace(string(line)) != "" {
+		offlineCPUs, err := parseCPURange(line)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(offlineCPUs) > 0 {
+			err = filterOfflineCPUs(&offlineCPUs, &cpus)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
 	systemCpufreq := make([]SystemCPUCpufreqStats, len(cpus))
 	for i, cpu := range cpus {
 		cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu")
@@ -251,12 +313,12 @@ func (fs FS) IsolatedCPUs() ([]uint16, error) {
 		return nil, err
 	}
 
-	return parseIsolatedCPUs(isolcpus)
+	return parseCPURange(isolcpus)
 }
 
-func parseIsolatedCPUs(data []byte) ([]uint16, error) {
+func parseCPURange(data []byte) ([]uint16, error) {
 
-	var isolcpusInt = []uint16{}
+	var cpusInt = []uint16{}
 
 	for _, cpu := range strings.Split(strings.TrimRight(string(data), "\n"), ",") {
 		if cpu == "" {
@@ -277,7 +339,7 @@ func parseIsolatedCPUs(data []byte) ([]uint16, error) {
 			}
 
 			for i := startRange; i <= endRange; i++ {
-				isolcpusInt = append(isolcpusInt, uint16(i))
+				cpusInt = append(cpusInt, uint16(i))
 			}
 			continue
 		}
@@ -286,7 +348,7 @@ func parseIsolatedCPUs(data []byte) ([]uint16, error) {
 		if err != nil {
 			return nil, err
 		}
-		isolcpusInt = append(isolcpusInt, uint16(cpuN))
+		cpusInt = append(cpusInt, uint16(cpuN))
 	}
-	return isolcpusInt, nil
+	return cpusInt, nil
 }
diff --git a/sysfs/vmstat_numa.go b/sysfs/vmstat_numa.go
index 8a6d095..038a0ee 100644
--- a/sysfs/vmstat_numa.go
+++ b/sysfs/vmstat_numa.go
@@ -145,7 +145,7 @@ func parseVMStatNUMA(r []byte) (VMStat, error) {
 		case "nr_zone_active_anon":
 			vmStat.NrZoneActiveAnon = fv
 		case "nr_zone_inactive_file":
-			vmStat.NrZoneActiveFile = fv
+			vmStat.NrZoneInactiveFile = fv
 		case "nr_zone_active_file":
 			vmStat.NrZoneActiveFile = fv
 		case "nr_zone_unevictable":
diff --git a/thread.go b/thread.go
index f08bfc7..490c147 100644
--- a/thread.go
+++ b/thread.go
@@ -54,7 +54,8 @@ func (fs FS) AllThreads(pid int) (Procs, error) {
 		if err != nil {
 			continue
 		}
-		t = append(t, Proc{PID: int(tid), fs: fsi.FS(taskPath)})
+
+		t = append(t, Proc{PID: int(tid), fs: FS{fsi.FS(taskPath), fs.real}})
 	}
 
 	return t, nil
@@ -66,13 +67,13 @@ func (fs FS) Thread(pid, tid int) (Proc, error) {
 	if _, err := os.Stat(taskPath); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: tid, fs: fsi.FS(taskPath)}, nil
+	return Proc{PID: tid, fs: FS{fsi.FS(taskPath), fs.real}}, nil
 }
 
 // Thread returns a process for a given TID of Proc.
 func (proc Proc) Thread(tid int) (Proc, error) {
-	tfs := fsi.FS(proc.path("task"))
-	if _, err := os.Stat(tfs.Path(strconv.Itoa(tid))); err != nil {
+	tfs := FS{fsi.FS(proc.path("task")), proc.fs.real}
+	if _, err := os.Stat(tfs.proc.Path(strconv.Itoa(tid))); err != nil {
 		return Proc{}, err
 	}
 	return Proc{PID: tid, fs: tfs}, nil


Test patch:
diff --git a/blockdevice/stats_test.go b/blockdevice/stats_test.go
index 7343a0f..c065c29 100644
--- a/blockdevice/stats_test.go
+++ b/blockdevice/stats_test.go
@@ -73,7 +73,7 @@ func TestBlockDevice(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	expectedNumOfDevices := 2
+	expectedNumOfDevices := 8
 	if len(devices) != expectedNumOfDevices {
 		t.Fatalf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(devices))
 	}
@@ -93,18 +93,18 @@ func TestBlockDevice(t *testing.T) {
 	if device0stats.WeightedIOTicks != 6088971 {
 		t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
 	}
-	device1stats, count, err := blockdevice.SysBlockDeviceStat(devices[1])
+	device7stats, count, err := blockdevice.SysBlockDeviceStat(devices[7])
 	if count != 15 {
 		t.Errorf(failMsgFormat, "Incorrect number of stats read", 15, count)
 	}
 	if err != nil {
 		t.Fatal(err)
 	}
-	if device1stats.WriteSectors != 286915323 {
-		t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device1stats.WriteSectors)
+	if device7stats.WriteSectors != 286915323 {
+		t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device7stats.WriteSectors)
 	}
-	if device1stats.DiscardTicks != 12 {
-		t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device1stats.DiscardTicks)
+	if device7stats.DiscardTicks != 12 {
+		t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device7stats.DiscardTicks)
 	}
 	blockQueueStatExpected := BlockQueueStats{
 		AddRandom:            1,
@@ -145,7 +145,7 @@ func TestBlockDevice(t *testing.T) {
 		WriteZeroesMaxBytes:  0,
 	}
 
-	blockQueueStat, err := blockdevice.SysBlockDeviceQueueStats(devices[1])
+	blockQueueStat, err := blockdevice.SysBlockDeviceQueueStats(devices[7])
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/mountstats_test.go b/mountstats_test.go
index 9fccf84..3d23671 100644
--- a/mountstats_test.go
+++ b/mountstats_test.go
@@ -339,6 +339,7 @@ func TestMountStats(t *testing.T) {
 								CumulativeQueueMilliseconds:         6,
 								CumulativeTotalResponseMilliseconds: 79386,
 								CumulativeTotalRequestMilliseconds:  79407,
+								AverageRTTMilliseconds:              61.16024653312789,
 							},
 							{
 								Operation: "WRITE",
@@ -352,6 +353,7 @@ func TestMountStats(t *testing.T) {
 								CumulativeQueueMilliseconds:         18446743919241604546,
 								CumulativeTotalResponseMilliseconds: 1667369447,
 								CumulativeTotalRequestMilliseconds:  1953587717,
+								AverageRTTMilliseconds:              0.5695744656983355,
 							},
 						},
 						Transport: NFSTransportStats{
diff --git a/net_conntrackstat_test.go b/net_conntrackstat_test.go
index 18fc066..b19222a 100644
--- a/net_conntrackstat_test.go
+++ b/net_conntrackstat_test.go
@@ -83,3 +83,36 @@ func TestParseConntrackStat(t *testing.T) {
 		t.Errorf("want %v, have %v", want, have)
 	}
 }
+
+func TestParseOldConntrackStat(t *testing.T) {
+	var nfConntrackStat = []byte(`entries  searched found new invalid ignore delete delete_list insert insert_failed drop early_drop icmp_error  expect_new expect_create expect_delete
+0000002b  0003159f 02e6786a 00142562 0001bf93 00e1a051 00142537 000b8fe0 000b900b 00000000 00000000 00000000 0001b46a  00000000 00000000 00000000
+`)
+	r := bytes.NewReader(nfConntrackStat)
+
+	have, err := parseConntrackStat(r)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := []ConntrackStatEntry{
+		ConntrackStatEntry{
+			Entries:       43,
+			Searched:      202143,
+			Found:         48658538,
+			New:           1320290,
+			Invalid:       114579,
+			Ignore:        14786641,
+			Delete:        1320247,
+			DeleteList:    757728,
+			Insert:        757771,
+			InsertFailed:  0,
+			Drop:          0,
+			EarlyDrop:     0,
+			SearchRestart: 0,
+		},
+	}
+	if !reflect.DeepEqual(want, have) {
+		t.Errorf("want %v, have %v", want, have)
+	}
+}
diff --git a/net_wireless_test.go b/net_wireless_test.go
new file mode 100644
index 0000000..25d6a65
--- /dev/null
+++ b/net_wireless_test.go
@@ -0,0 +1,70 @@
+// Copyright 2023 The Prometheus 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 procfs
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestWireless(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	got, err := fs.Wireless()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := []*Wireless{
+		{
+			Name:           "wlan0",
+			Status:         1,
+			QualityLink:    2,
+			QualityLevel:   3,
+			QualityNoise:   4,
+			DiscardedNwid:  5,
+			DiscardedCrypt: 6,
+			DiscardedFrag:  7,
+			DiscardedRetry: 8,
+			DiscardedMisc:  9,
+			MissedBeacon:   10,
+		},
+		{
+			Name:           "wlan1",
+			Status:         16,
+			QualityLink:    9,
+			QualityLevel:   8,
+			QualityNoise:   7,
+			DiscardedNwid:  6,
+			DiscardedCrypt: 5,
+			DiscardedFrag:  4,
+			DiscardedRetry: 3,
+			DiscardedMisc:  2,
+			MissedBeacon:   1,
+		},
+	}
+
+	if len(got) != len(expected) {
+		t.Fatalf("unexpected number of interfaces parsed %d, expected %d", len(got), len(expected))
+	}
+
+	for i, iface := range got {
+		if !reflect.DeepEqual(iface, expected[i]) {
+			t.Errorf("unexpected interface got %+v, expected %+v", iface, expected[i])
+		}
+	}
+}
diff --git a/proc_status_test.go b/proc_status_test.go
index 3c9ab59..2f0884f 100644
--- a/proc_status_test.go
+++ b/proc_status_test.go
@@ -14,6 +14,7 @@
 package procfs
 
 import (
+	"reflect"
 	"testing"
 )
 
@@ -121,3 +122,19 @@ func TestProcStatusGIDs(t *testing.T) {
 		t.Errorf("want uids %s, have %s", want, have)
 	}
 }
+
+func TestCpusAllowedList(t *testing.T) {
+	p, err := getProcFixtures(t).Proc(26231)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s, err := p.NewStatus()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if want, have := []uint64{0, 1, 2, 3, 4, 5, 6, 7}, s.CpusAllowedList; !reflect.DeepEqual(want, have) {
+		t.Errorf("want CpusAllowedList %v, have %v", want, have)
+	}
+}
diff --git a/sysfs/mdraid_test.go b/sysfs/mdraid_test.go
new file mode 100644
index 0000000..4cf279f
--- /dev/null
+++ b/sysfs/mdraid_test.go
@@ -0,0 +1,134 @@
+// Copyright 2023 The Prometheus 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.
+
+//go:build linux
+// +build linux
+
+package sysfs
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestMdraidStats(t *testing.T) {
+	fs, err := NewFS(sysTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	got, err := fs.Mdraids()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := []Mdraid{
+		{
+			Device:          "md0",
+			Level:           "raid0",
+			ArrayState:      "clean",
+			MetadataVersion: "1.2",
+			Disks:           2,
+			Components: []MdraidComponent{
+				{Device: "sdg", State: "in_sync"},
+				{Device: "sdh", State: "in_sync"},
+			},
+			UUID:      "155f29ff-1716-4107-b362-52307ef86cac",
+			ChunkSize: 524288,
+		},
+		{
+			Device:          "md1",
+			Level:           "raid1",
+			ArrayState:      "clean",
+			MetadataVersion: "1.2",
+			Disks:           2,
+			Components: []MdraidComponent{
+				{Device: "sdi", State: "in_sync"},
+				{Device: "sdj", State: "in_sync"},
+			},
+			UUID:       "0fbf5f2c-add2-43c2-bd78-a4be3ab709ef",
+			SyncAction: "idle",
+		},
+		{
+			Device:          "md10",
+			Level:           "raid10",
+			ArrayState:      "clean",
+			MetadataVersion: "1.2",
+			Disks:           4,
+			Components: []MdraidComponent{
+				{Device: "sdu", State: "in_sync"},
+				{Device: "sdv", State: "in_sync"},
+				{Device: "sdw", State: "in_sync"},
+				{Device: "sdx", State: "in_sync"},
+			},
+			UUID:       "0c15f7e7-b159-4b1f-a5cd-a79b5c04b6f5",
+			ChunkSize:  524288,
+			SyncAction: "idle",
+		},
+		{
+			Device:          "md4",
+			Level:           "raid4",
+			ArrayState:      "clean",
+			MetadataVersion: "1.2",
+			Disks:           3,
+			Components: []MdraidComponent{
+				{Device: "sdk", State: "in_sync"},
+				{Device: "sdl", State: "in_sync"},
+				{Device: "sdm", State: "in_sync"},
+			},
+			UUID:       "67f415d5-2c0c-4b69-8e0d-7e20ef553457",
+			ChunkSize:  524288,
+			SyncAction: "idle",
+		},
+		{
+			Device:          "md5",
+			Level:           "raid5",
+			ArrayState:      "clean",
+			MetadataVersion: "1.2",
+			Disks:           3,
+			Components: []MdraidComponent{
+				{Device: "sdaa", State: "spare"},
+				{Device: "sdn", State: "in_sync"},
+				{Device: "sdo", State: "in_sync"},
+				{Device: "sdp", State: "faulty"},
+			},
+			UUID:          "7615b98d-f2ba-4d99-bee8-6202d8e130b9",
+			ChunkSize:     524288,
+			DegradedDisks: 1,
+			SyncAction:    "idle",
+		},
+		{
+			Device:          "md6",
+			Level:           "raid6",
+			ArrayState:      "active",
+			MetadataVersion: "1.2",
+			Disks:           4,
+			Components: []MdraidComponent{
+				{Device: "sdq", State: "in_sync"},
+				{Device: "sdr", State: "in_sync"},
+				{Device: "sds", State: "in_sync"},
+				{Device: "sdt", State: "spare"},
+			},
+			UUID:          "5f529b25-6efd-46e4-99a2-31f6f597be6b",
+			ChunkSize:     524288,
+			DegradedDisks: 1,
+			SyncAction:    "recover",
+			SyncCompleted: 0.7500458659491194,
+		},
+	}
+
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("unexpected Mdraid (-want +got):\n%s", diff)
+	}
+}
diff --git a/sysfs/system_cpu_test.go b/sysfs/system_cpu_test.go
index afce58d..864e2ea 100644
--- a/sysfs/system_cpu_test.go
+++ b/sysfs/system_cpu_test.go
@@ -165,7 +165,7 @@ func TestIsolatedParsingCPU(t *testing.T) {
 	}
 	for _, params := range testParams {
 		t.Run("blabla", func(t *testing.T) {
-			res, err := parseIsolatedCPUs(params.in)
+			res, err := parseCPURange(params.in)
 			if !reflect.DeepEqual(res, params.res) {
 				t.Fatalf("should have %v result: got %v", params.res, res)
 			}
@@ -193,3 +193,24 @@ func TestIsolatedCPUs(t *testing.T) {
 		t.Errorf("Error not correct: want %v, have %v", nil, err)
 	}
 }
+
+func TestBinSearch(t *testing.T) {
+	var testParams = []struct {
+		elem  uint16
+		elems []uint16
+		res   bool
+	}{
+		{3, []uint16{1, 3, 5, 7, 9}, true},
+		{4, []uint16{1, 3, 5, 7, 9}, false},
+		{2, []uint16{}, false},
+	}
+
+	for _, param := range testParams {
+		res := binSearch(param.elem, &param.elems)
+
+		if res != param.res {
+			t.Fatalf("Result not correct: want %v, have %v", param.res, res)
+		}
+
+	}
+}
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index a965b11..fd2b9e5 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -2568,6 +2568,14 @@ Num       RefCount Protocol Flags    Type St Path
 0000000000000000: 00000003 00000000 00000000 0001 03
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/wireless
+Lines: 4
+Inter-| sta-|   Quality        |   Discarded packets               | Missed
+ face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon
+ wlan0: 0001    2.    3.    4.       5      6      7      8      9       10
+ wlan1: 0010    9     8.    7.       6      5      4      3      2        1
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/xfrm_stat
 Lines: 28
 XfrmInError                     1
@@ -3579,6 +3587,503 @@ Lines: 1
 6447303        0 710266738  1529043   953216        0 31201176  4557464        0   796160  6088971
 Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md0
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md0/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/array_state
+Lines: 1
+clean
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/chunk_size
+Lines: 1
+524288
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md0/md/dev-sdg
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/dev-sdg/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md0/md/dev-sdh
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/dev-sdh/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/level
+Lines: 1
+raid0
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/raid_disks
+Lines: 1
+2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/rd0
+SymlinkTo: dev-sdg
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/rd1
+SymlinkTo: dev-sdh
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md0/md/uuid
+Lines: 1
+155f29ff-1716-4107-b362-52307ef86cac
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md1
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md1/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/array_state
+Lines: 1
+clean
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/chunk_size
+Lines: 1
+0
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/degraded
+Lines: 1
+0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md1/md/dev-sdi
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/dev-sdi/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md1/md/dev-sdj
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/dev-sdj/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/level
+Lines: 1
+raid1
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/raid_disks
+Lines: 1
+2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/rd0
+SymlinkTo: dev-sdi
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/rd1
+SymlinkTo: dev-sdj
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/sync_action
+Lines: 1
+idle
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/sync_completed
+Lines: 1
+none
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md1/md/uuid
+Lines: 1
+0fbf5f2c-add2-43c2-bd78-a4be3ab709ef
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/array_state
+Lines: 1
+clean
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/chunk_size
+Lines: 1
+524288
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/degraded
+Lines: 1
+0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10/md/dev-sdu
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/dev-sdu/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10/md/dev-sdv
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/dev-sdv/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10/md/dev-sdw
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/dev-sdw/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md10/md/dev-sdx
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/dev-sdx/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/level
+Lines: 1
+raid10
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/raid_disks
+Lines: 1
+4
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/rd0
+SymlinkTo: dev-sdu
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/rd1
+SymlinkTo: dev-sdv
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/rd2
+SymlinkTo: dev-sdw
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/rd3
+SymlinkTo: dev-sdx
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/sync_action
+Lines: 1
+idle
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/sync_completed
+Lines: 1
+none
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md10/md/uuid
+Lines: 1
+0c15f7e7-b159-4b1f-a5cd-a79b5c04b6f5
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md4
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md4/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/array_state
+Lines: 1
+clean
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/chunk_size
+Lines: 1
+524288
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/degraded
+Lines: 1
+0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md4/md/dev-sdk
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/dev-sdk/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md4/md/dev-sdl
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/dev-sdl/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md4/md/dev-sdm
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/dev-sdm/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/level
+Lines: 1
+raid4
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/raid_disks
+Lines: 1
+3
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/rd0
+SymlinkTo: dev-sdk
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/rd1
+SymlinkTo: dev-sdl
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/rd2
+SymlinkTo: dev-sdm
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/sync_action
+Lines: 1
+idle
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/sync_completed
+Lines: 1
+none
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md4/md/uuid
+Lines: 1
+67f415d5-2c0c-4b69-8e0d-7e20ef553457
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/array_state
+Lines: 1
+clean
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/chunk_size
+Lines: 1
+524288
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/degraded
+Lines: 1
+1
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5/md/dev-sdaa
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/dev-sdaa/state
+Lines: 1
+spare
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5/md/dev-sdn
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/dev-sdn/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5/md/dev-sdo
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/dev-sdo/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md5/md/dev-sdp
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/dev-sdp/state
+Lines: 1
+faulty
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/level
+Lines: 1
+raid5
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/raid_disks
+Lines: 1
+3
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/rd0
+SymlinkTo: dev-sdn
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/rd1
+SymlinkTo: dev-sdo
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/rd2
+SymlinkTo: dev-sdp
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/sync_action
+Lines: 1
+idle
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/sync_completed
+Lines: 1
+none
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md5/md/uuid
+Lines: 1
+7615b98d-f2ba-4d99-bee8-6202d8e130b9
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6/md
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/array_state
+Lines: 1
+active
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/chunk_size
+Lines: 1
+524288
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/degraded
+Lines: 1
+1
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6/md/dev-sdq
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/dev-sdq/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6/md/dev-sdr
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/dev-sdr/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6/md/dev-sds
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/dev-sds/state
+Lines: 1
+in_sync
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/block/md6/md/dev-sdt
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/dev-sdt/state
+Lines: 1
+spare
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/level
+Lines: 1
+raid6
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/metadata_version
+Lines: 1
+1.2
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/raid_disks
+Lines: 1
+4
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/rd0
+SymlinkTo: dev-sdq
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/rd1
+SymlinkTo: dev-sdr
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/rd2
+SymlinkTo: dev-sds
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/rd3
+SymlinkTo: dev-sdt
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/sync_action
+Lines: 1
+recover
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/sync_completed
+Lines: 1
+1569888 / 2093056
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/block/md6/md/uuid
+Lines: 1
+5f529b25-6efd-46e4-99a2-31f6f597be6b
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/block/sda
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -12721,6 +13226,11 @@ Lines: 1
 1,2-7,9
 Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/offline
+Lines: 1
+2,12-15
+Mode: 664
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/node
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/thread_test.go b/thread_test.go
index 503be4f..1b83046 100644
--- a/thread_test.go
+++ b/thread_test.go
@@ -37,7 +37,7 @@ func TestAllThreads(t *testing.T) {
 			t.Errorf("want TID %d, have %d", wantTID, haveTID)
 		}
 		wantFS := fixFS.proc.Path(strconv.Itoa(testPID), "task")
-		haveFS := string(threads[i].fs)
+		haveFS := string(threads[i].fs.proc)
 		if wantFS != haveFS {
 			t.Errorf("want fs %q, have %q", wantFS, haveFS)
 		}