Repository: prometheus/procfs
Pull request #235 (2019-11-19 12:50:44)

Add /proc/net/udp parsing especially for the tx_queue and rx_queue lengths
Hi @pgier,

this belongs to the requested of @discordianfish in https://github.com/prometheus/node_exporter/pull/1503.

Signed-off-by: Peter Bueschel peter.bueschel@logmein.com

peterbueschel: I would update https://github.com/prometheus/node_exporter/pull/1503 as soon as this PR is merged.
SuperQ: Nice. It would also be good to include /proc/net/udp6 as well.
mdlayher: You can see how I handled sockstat/sockstat6 in https://github.com/prometheus/procfs/pull/237.

Patch:
diff --git a/crypto.go b/crypto.go
index 19d4041..a958933 100644
--- a/crypto.go
+++ b/crypto.go
@@ -14,10 +14,10 @@
 package procfs
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
-	"io/ioutil"
-	"strconv"
+	"io"
 	"strings"
 
 	"github.com/prometheus/procfs/internal/util"
@@ -52,80 +52,102 @@ type Crypto struct {
 // structs containing the relevant info.  More information available here:
 // https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html
 func (fs FS) Crypto() ([]Crypto, error) {
-	data, err := ioutil.ReadFile(fs.proc.Path("crypto"))
+	path := fs.proc.Path("crypto")
+	b, err := util.ReadFileNoStat(path)
 	if err != nil {
-		return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err)
+		return nil, fmt.Errorf("error reading crypto %s: %s", path, err)
 	}
-	crypto, err := parseCrypto(data)
+
+	crypto, err := parseCrypto(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err)
+		return nil, fmt.Errorf("error parsing crypto %s: %s", path, err)
 	}
+
 	return crypto, nil
 }
 
-func parseCrypto(cryptoData []byte) ([]Crypto, error) {
-	crypto := []Crypto{}
-
-	cryptoBlocks := bytes.Split(cryptoData, []byte("\n\n"))
-
-	for _, block := range cryptoBlocks {
-		var newCryptoElem Crypto
-
-		lines := strings.Split(string(block), "\n")
-		for _, line := range lines {
-			if strings.TrimSpace(line) == "" || line[0] == ' ' {
-				continue
-			}
-			fields := strings.Split(line, ":")
-			key := strings.TrimSpace(fields[0])
-			value := strings.TrimSpace(fields[1])
-			vp := util.NewValueParser(value)
-
-			switch strings.TrimSpace(key) {
-			case "async":
-				b, err := strconv.ParseBool(value)
-				if err == nil {
-					newCryptoElem.Async = b
-				}
-			case "blocksize":
-				newCryptoElem.Blocksize = vp.PUInt64()
-			case "chunksize":
-				newCryptoElem.Chunksize = vp.PUInt64()
-			case "digestsize":
-				newCryptoElem.Digestsize = vp.PUInt64()
-			case "driver":
-				newCryptoElem.Driver = value
-			case "geniv":
-				newCryptoElem.Geniv = value
-			case "internal":
-				newCryptoElem.Internal = value
-			case "ivsize":
-				newCryptoElem.Ivsize = vp.PUInt64()
-			case "maxauthsize":
-				newCryptoElem.Maxauthsize = vp.PUInt64()
-			case "max keysize":
-				newCryptoElem.MaxKeysize = vp.PUInt64()
-			case "min keysize":
-				newCryptoElem.MinKeysize = vp.PUInt64()
-			case "module":
-				newCryptoElem.Module = value
-			case "name":
-				newCryptoElem.Name = value
-			case "priority":
-				newCryptoElem.Priority = vp.PInt64()
-			case "refcnt":
-				newCryptoElem.Refcnt = vp.PInt64()
-			case "seedsize":
-				newCryptoElem.Seedsize = vp.PUInt64()
-			case "selftest":
-				newCryptoElem.Selftest = value
-			case "type":
-				newCryptoElem.Type = value
-			case "walksize":
-				newCryptoElem.Walksize = vp.PUInt64()
-			}
+// parseCrypto parses a /proc/crypto stream into Crypto elements.
+func parseCrypto(r io.Reader) ([]Crypto, error) {
+	var out []Crypto
+
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		text := s.Text()
+		switch {
+		case strings.HasPrefix(text, "name"):
+			// Each crypto element begins with its name.
+			out = append(out, Crypto{})
+		case text == "":
+			continue
+		}
+
+		kv := strings.Split(text, ":")
+		if len(kv) != 2 {
+			return nil, fmt.Errorf("malformed crypto line: %q", text)
+		}
+
+		k := strings.TrimSpace(kv[0])
+		v := strings.TrimSpace(kv[1])
+
+		// Parse the key/value pair into the currently focused element.
+		c := &out[len(out)-1]
+		if err := c.parseKV(k, v); err != nil {
+			return nil, err
 		}
-		crypto = append(crypto, newCryptoElem)
 	}
-	return crypto, nil
+
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return out, nil
+}
+
+// parseKV parses a key/value pair into the appropriate field of c.
+func (c *Crypto) parseKV(k, v string) error {
+	vp := util.NewValueParser(v)
+
+	switch k {
+	case "async":
+		// Interpret literal yes as true.
+		c.Async = v == "yes"
+	case "blocksize":
+		c.Blocksize = vp.PUInt64()
+	case "chunksize":
+		c.Chunksize = vp.PUInt64()
+	case "digestsize":
+		c.Digestsize = vp.PUInt64()
+	case "driver":
+		c.Driver = v
+	case "geniv":
+		c.Geniv = v
+	case "internal":
+		c.Internal = v
+	case "ivsize":
+		c.Ivsize = vp.PUInt64()
+	case "maxauthsize":
+		c.Maxauthsize = vp.PUInt64()
+	case "max keysize":
+		c.MaxKeysize = vp.PUInt64()
+	case "min keysize":
+		c.MinKeysize = vp.PUInt64()
+	case "module":
+		c.Module = v
+	case "name":
+		c.Name = v
+	case "priority":
+		c.Priority = vp.PInt64()
+	case "refcnt":
+		c.Refcnt = vp.PInt64()
+	case "seedsize":
+		c.Seedsize = vp.PUInt64()
+	case "selftest":
+		c.Selftest = v
+	case "type":
+		c.Type = v
+	case "walksize":
+		c.Walksize = vp.PUInt64()
+	}
+
+	return vp.Err()
 }
diff --git a/fixtures.ttar b/fixtures.ttar
index c50a18a..e4c17a2 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -189,7 +189,7 @@ Ngid:	0
 Pid:	26231
 PPid:	1
 TracerPid:	0
-Uid:	0	0	0	0
+Uid:	1000	1000	1000	0
 Gid:	0	0	0	0
 FDSize:	128
 Groups:
@@ -554,7 +554,7 @@ power management:
 Mode: 444
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/crypto
-Lines: 971
+Lines: 972
 name         : ccm(aes)
 driver       : ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))
 module       : ccm
@@ -588,6 +588,7 @@ refcnt       : 1
 selftest     : passed
 internal     : no
 type         : kpp
+async        : yes
 
 name         : ecb(arc4)
 driver       : ecb(arc4)-generic
@@ -1825,6 +1826,27 @@ Lines: 1
 00015c73 00020e76 F0000769 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/udp
+Lines: 4
+  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
+   0: 00000000:0016 00000000:0000 0A 00000000:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0                      
+   1: 00000000:0016 00000000:0000 0A 00000001:00000000 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0                      
+   2: 00000000:0016 00000000:0000 0A 00000001:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0                      
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/udp6
+Lines: 3
+  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode ref pointer drops
+ 1315: 00000000000000000000000000000000:14EB 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000   981        0 21040 2 0000000013726323 0
+ 6073: 00000000000000000000000000000000:C781 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000  1000        0 11337031 2 00000000b9256fdd 0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/udp_broken
+Lines: 2
+  sl  local_address rem_address   st
+   1: 00000000:0016 00000000:0000 0A
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/unix
 Lines: 6
 Num       RefCount Protocol Flags    Type St Inode Path
diff --git a/net_udp.go b/net_udp.go
new file mode 100644
index 0000000..1799119
--- /dev/null
+++ b/net_udp.go
@@ -0,0 +1,227 @@
+// Copyright 2019 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"
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+)
+
+const (
+	// readLimit is used by io.LimitReader while reading the content of the
+	// /proc/net/udp{,6} files. The number of lines inside such a file is dynamic
+	// as each line represents a single used socket.
+	// In theory, the number of available sockets is 65535 (2^16 - 1) per IP.
+	// With e.g. 150 Byte per line and the maximum number of 65535,
+	// the reader needs to handle 150 Byte * 65535 =~ 10 MB for a single IP.
+	readLimit = 4294967296 // Byte -> 4 GiB
+)
+
+type (
+	// NetUDP represents the contents of /proc/net/udp{,6} file without the header.
+	NetUDP []*netUDPLine
+
+	// NetUDPSummary provides already computed values like the total queue lengths or
+	// the total number of used sockets. In contrast to NetUDP it does not collect
+	// the parsed lines into a slice.
+	NetUDPSummary struct {
+		// TxQueueLength shows the total queue length of all parsed tx_queue lengths.
+		TxQueueLength uint64
+		// RxQueueLength shows the total queue length of all parsed rx_queue lengths.
+		RxQueueLength uint64
+		// UsedSockets shows the total number of parsed lines representing the
+		// number of used sockets.
+		UsedSockets uint64
+	}
+
+	// netUDPLine represents the fields parsed from a single line
+	// in /proc/net/udp{,6}. Fields which are not used by UDP are skipped.
+	// For the proc file format details, see https://linux.die.net/man/5/proc.
+	netUDPLine struct {
+		Sl        uint64
+		LocalAddr uint64
+		LocalPort uint64
+		RemAddr   uint64
+		RemPort   uint64
+		St        uint64
+		TxQueue   uint64
+		RxQueue   uint64
+		UID       uint64
+	}
+)
+
+// NetUDP returns the IPv4 kernel/networking statistics for UDP datagrams
+// read from /proc/net/udp.
+func (fs FS) NetUDP() (NetUDP, error) {
+	return newNetUDP(fs.proc.Path("net/udp"))
+}
+
+// NetUDP6 returns the IPv6 kernel/networking statistics for UDP datagrams
+// read from /proc/net/udp6.
+func (fs FS) NetUDP6() (NetUDP, error) {
+	return newNetUDP(fs.proc.Path("net/udp6"))
+}
+
+// NetUDPSummary returns already computed statistics like the total queue lengths
+// for UDP datagrams read from /proc/net/udp.
+func (fs FS) NetUDPSummary() (*NetUDPSummary, error) {
+	return newNetUDPSummary(fs.proc.Path("net/udp"))
+}
+
+// NetUDP6Summary returns already computed statistics like the total queue lengths
+// for UDP datagrams read from /proc/net/udp6.
+func (fs FS) NetUDP6Summary() (*NetUDPSummary, error) {
+	return newNetUDPSummary(fs.proc.Path("net/udp6"))
+}
+
+// newNetUDP creates a new NetUDP{,6} from the contents of the given file.
+func newNetUDP(file string) (NetUDP, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	netUDP := NetUDP{}
+
+	lr := io.LimitReader(f, readLimit)
+	s := bufio.NewScanner(lr)
+	s.Scan() // skip first line with headers
+	for s.Scan() {
+		fields := strings.Fields(s.Text())
+		line, err := parseNetUDPLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		netUDP = append(netUDP, line)
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return netUDP, nil
+}
+
+// newNetUDPSummary creates a new NetUDP{,6} from the contents of the given file.
+func newNetUDPSummary(file string) (*NetUDPSummary, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	netUDPSummary := &NetUDPSummary{}
+
+	lr := io.LimitReader(f, readLimit)
+	s := bufio.NewScanner(lr)
+	s.Scan() // skip first line with headers
+	for s.Scan() {
+		fields := strings.Fields(s.Text())
+		line, err := parseNetUDPLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		netUDPSummary.TxQueueLength += line.TxQueue
+		netUDPSummary.RxQueueLength += line.RxQueue
+		netUDPSummary.UsedSockets++
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return netUDPSummary, nil
+}
+
+// parseNetUDPLine parses a single line, represented by a list of fields.
+func parseNetUDPLine(fields []string) (*netUDPLine, error) {
+	line := &netUDPLine{}
+	if len(fields) < 8 {
+		return nil, fmt.Errorf(
+			"cannot parse net udp socket line as it has less then 8 columns: %s",
+			strings.Join(fields, " "),
+		)
+	}
+	var err error // parse error
+
+	// sl
+	s := strings.Split(fields[0], ":")
+	if len(s) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse sl field in udp socket line: %s", fields[0])
+	}
+
+	if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse sl value in udp socket line: %s", err)
+	}
+	// local_address
+	l := strings.Split(fields[1], ":")
+	if len(l) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse local_address field in udp socket line: %s", fields[1])
+	}
+	if line.LocalAddr, err = strconv.ParseUint(l[0], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse local_address value in udp socket line: %s", err)
+	}
+	if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse local_address port value in udp socket line: %s", err)
+	}
+
+	// remote_address
+	r := strings.Split(fields[2], ":")
+	if len(r) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse rem_address field in udp socket line: %s", fields[1])
+	}
+	if line.RemAddr, err = strconv.ParseUint(r[0], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse rem_address value in udp socket line: %s", err)
+	}
+	if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse rem_address port value in udp socket line: %s", err)
+	}
+
+	// st
+	if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse st value in udp socket line: %s", err)
+	}
+
+	// tx_queue and rx_queue
+	q := strings.Split(fields[4], ":")
+	if len(q) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse tx/rx queues in udp socket line as it has a missing colon: %s",
+			fields[4],
+		)
+	}
+	if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse tx_queue value in udp socket line: %s", err)
+	}
+	if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse rx_queue value in udp socket line: %s", err)
+	}
+
+	// uid
+	if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse uid value in udp socket line: %s", err)
+	}
+
+	return line, nil
+}
diff --git a/net_unix.go b/net_unix.go
index 93bd58f..c55b4b1 100644
--- a/net_unix.go
+++ b/net_unix.go
@@ -15,7 +15,6 @@ package procfs
 
 import (
 	"bufio"
-	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -27,25 +26,15 @@ import (
 // see https://elixir.bootlin.com/linux/v4.17/source/net/unix/af_unix.c#L2815
 // and https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/net.h#L48.
 
-const (
-	netUnixKernelPtrIdx = iota
-	netUnixRefCountIdx
-	_
-	netUnixFlagsIdx
-	netUnixTypeIdx
-	netUnixStateIdx
-	netUnixInodeIdx
-
-	// Inode and Path are optional.
-	netUnixStaticFieldsCnt = 6
-)
-
+// Constants for the various /proc/net/unix enumerations.
+// TODO: match against x/sys/unix or similar?
 const (
 	netUnixTypeStream    = 1
 	netUnixTypeDgram     = 2
 	netUnixTypeSeqpacket = 5
 
-	netUnixFlagListen = 1 << 16
+	netUnixFlagDefault = 0
+	netUnixFlagListen  = 1 << 16
 
 	netUnixStateUnconnected  = 1
 	netUnixStateConnecting   = 2
@@ -53,129 +42,127 @@ const (
 	netUnixStateDisconnected = 4
 )
 
-var errInvalidKernelPtrFmt = errors.New("Invalid Num(the kernel table slot number) format")
+// NetUNIXType is the type of the type field.
+type NetUNIXType uint64
 
-// NetUnixType is the type of the type field.
-type NetUnixType uint64
+// NetUNIXFlags is the type of the flags field.
+type NetUNIXFlags uint64
 
-// NetUnixFlags is the type of the flags field.
-type NetUnixFlags uint64
+// NetUNIXState is the type of the state field.
+type NetUNIXState uint64
 
-// NetUnixState is the type of the state field.
-type NetUnixState uint64
-
-// NetUnixLine represents a line of /proc/net/unix.
-type NetUnixLine struct {
+// NetUNIXLine represents a line of /proc/net/unix.
+type NetUNIXLine struct {
 	KernelPtr string
 	RefCount  uint64
 	Protocol  uint64
-	Flags     NetUnixFlags
-	Type      NetUnixType
-	State     NetUnixState
+	Flags     NetUNIXFlags
+	Type      NetUNIXType
+	State     NetUNIXState
 	Inode     uint64
 	Path      string
 }
 
-// NetUnix holds the data read from /proc/net/unix.
-type NetUnix struct {
-	Rows []*NetUnixLine
+// NetUNIX holds the data read from /proc/net/unix.
+type NetUNIX struct {
+	Rows []*NetUNIXLine
 }
 
-// NewNetUnix returns data read from /proc/net/unix.
-func NewNetUnix() (*NetUnix, error) {
-	fs, err := NewFS(DefaultMountPoint)
-	if err != nil {
-		return nil, err
-	}
-
-	return fs.NewNetUnix()
+// NetUNIX returns data read from /proc/net/unix.
+func (fs FS) NetUNIX() (*NetUNIX, error) {
+	return readNetUNIX(fs.proc.Path("net/unix"))
 }
 
-// NewNetUnix returns data read from /proc/net/unix.
-func (fs FS) NewNetUnix() (*NetUnix, error) {
-	return NewNetUnixByPath(fs.proc.Path("net/unix"))
-}
-
-// NewNetUnixByPath returns data read from /proc/net/unix by file path.
-// It might returns an error with partial parsed data, if an error occur after some data parsed.
-func NewNetUnixByPath(path string) (*NetUnix, error) {
-	f, err := os.Open(path)
+// readNetUNIX reads data in /proc/net/unix format from the specified file.
+func readNetUNIX(file string) (*NetUNIX, error) {
+	// This file could be quite large and a streaming read is desirable versus
+	// reading the entire contents at once.
+	f, err := os.Open(file)
 	if err != nil {
 		return nil, err
 	}
 	defer f.Close()
-	return NewNetUnixByReader(f)
+
+	return parseNetUNIX(f)
 }
 
-// NewNetUnixByReader returns data read from /proc/net/unix by a reader.
-// It might returns an error with partial parsed data, if an error occur after some data parsed.
-func NewNetUnixByReader(reader io.Reader) (*NetUnix, error) {
-	nu := &NetUnix{
-		Rows: make([]*NetUnixLine, 0, 32),
-	}
-	scanner := bufio.NewScanner(reader)
-	// Omit the header line.
-	scanner.Scan()
-	header := scanner.Text()
+// parseNetUNIX creates a NetUnix structure from the incoming stream.
+func parseNetUNIX(r io.Reader) (*NetUNIX, error) {
+	// Begin scanning by checking for the existence of Inode.
+	s := bufio.NewScanner(r)
+	s.Scan()
+
 	// From the man page of proc(5), it does not contain an Inode field,
-	// but in actually it exists.
-	// This code works for both cases.
-	hasInode := strings.Contains(header, "Inode")
+	// but in actually it exists. This code works for both cases.
+	hasInode := strings.Contains(s.Text(), "Inode")
 
-	minFieldsCnt := netUnixStaticFieldsCnt
+	// Expect a minimum number of fields, but Inode and Path are optional:
+	// Num       RefCount Protocol Flags    Type St Inode Path
+	minFields := 6
 	if hasInode {
-		minFieldsCnt++
+		minFields++
 	}
-	for scanner.Scan() {
-		line := scanner.Text()
-		item, err := nu.parseLine(line, hasInode, minFieldsCnt)
+
+	var nu NetUNIX
+	for s.Scan() {
+		line := s.Text()
+		item, err := nu.parseLine(line, hasInode, minFields)
 		if err != nil {
-			return nu, err
+			return nil, fmt.Errorf("failed to parse /proc/net/unix data %q: %v", line, err)
 		}
+
 		nu.Rows = append(nu.Rows, item)
 	}
 
-	return nu, scanner.Err()
+	if err := s.Err(); err != nil {
+		return nil, fmt.Errorf("failed to scan /proc/net/unix data: %v", err)
+	}
+
+	return &nu, nil
 }
 
-func (u *NetUnix) parseLine(line string, hasInode bool, minFieldsCnt int) (*NetUnixLine, error) {
+func (u *NetUNIX) parseLine(line string, hasInode bool, min int) (*NetUNIXLine, error) {
 	fields := strings.Fields(line)
-	fieldsLen := len(fields)
-	if fieldsLen < minFieldsCnt {
-		return nil, fmt.Errorf(
-			"Parse Unix domain failed: expect at least %d fields but got %d",
-			minFieldsCnt, fieldsLen)
-	}
-	kernelPtr, err := u.parseKernelPtr(fields[netUnixKernelPtrIdx])
-	if err != nil {
-		return nil, fmt.Errorf("Parse Unix domain num(%s) failed: %s", fields[netUnixKernelPtrIdx], err)
+
+	l := len(fields)
+	if l < min {
+		return nil, fmt.Errorf("expected at least %d fields but got %d", min, l)
 	}
-	users, err := u.parseUsers(fields[netUnixRefCountIdx])
+
+	// Field offsets are as follows:
+	// Num       RefCount Protocol Flags    Type St Inode Path
+
+	kernelPtr := strings.TrimSuffix(fields[0], ":")
+
+	users, err := u.parseUsers(fields[1])
 	if err != nil {
-		return nil, fmt.Errorf("Parse Unix domain ref count(%s) failed: %s", fields[netUnixRefCountIdx], err)
+		return nil, fmt.Errorf("failed to parse ref count(%s): %v", fields[1], err)
 	}
-	flags, err := u.parseFlags(fields[netUnixFlagsIdx])
+
+	flags, err := u.parseFlags(fields[3])
 	if err != nil {
-		return nil, fmt.Errorf("Parse Unix domain flags(%s) failed: %s", fields[netUnixFlagsIdx], err)
+		return nil, fmt.Errorf("failed to parse flags(%s): %v", fields[3], err)
 	}
-	typ, err := u.parseType(fields[netUnixTypeIdx])
+
+	typ, err := u.parseType(fields[4])
 	if err != nil {
-		return nil, fmt.Errorf("Parse Unix domain type(%s) failed: %s", fields[netUnixTypeIdx], err)
+		return nil, fmt.Errorf("failed to parse type(%s): %v", fields[4], err)
 	}
-	state, err := u.parseState(fields[netUnixStateIdx])
+
+	state, err := u.parseState(fields[5])
 	if err != nil {
-		return nil, fmt.Errorf("Parse Unix domain state(%s) failed: %s", fields[netUnixStateIdx], err)
+		return nil, fmt.Errorf("failed to parse state(%s): %v", fields[5], err)
 	}
+
 	var inode uint64
 	if hasInode {
-		inodeStr := fields[netUnixInodeIdx]
-		inode, err = u.parseInode(inodeStr)
+		inode, err = u.parseInode(fields[6])
 		if err != nil {
-			return nil, fmt.Errorf("Parse Unix domain inode(%s) failed: %s", inodeStr, err)
+			return nil, fmt.Errorf("failed to parse inode(%s): %v", fields[6], err)
 		}
 	}
 
-	nuLine := &NetUnixLine{
+	n := &NetUNIXLine{
 		KernelPtr: kernelPtr,
 		RefCount:  users,
 		Type:      typ,
@@ -185,57 +172,56 @@ func (u *NetUnix) parseLine(line string, hasInode bool, minFieldsCnt int) (*NetU
 	}
 
 	// Path field is optional.
-	if fieldsLen > minFieldsCnt {
-		pathIdx := netUnixInodeIdx + 1
+	if l > min {
+		// Path occurs at either index 6 or 7 depending on whether inode is
+		// already present.
+		pathIdx := 7
 		if !hasInode {
 			pathIdx--
 		}
-		nuLine.Path = fields[pathIdx]
-	}
-
-	return nuLine, nil
-}
 
-func (u NetUnix) parseKernelPtr(str string) (string, error) {
-	if !strings.HasSuffix(str, ":") {
-		return "", errInvalidKernelPtrFmt
+		n.Path = fields[pathIdx]
 	}
-	return str[:len(str)-1], nil
+
+	return n, nil
 }
 
-func (u NetUnix) parseUsers(hexStr string) (uint64, error) {
-	return strconv.ParseUint(hexStr, 16, 32)
+func (u NetUNIX) parseUsers(s string) (uint64, error) {
+	return strconv.ParseUint(s, 16, 32)
 }
 
-func (u NetUnix) parseType(hexStr string) (NetUnixType, error) {
-	typ, err := strconv.ParseUint(hexStr, 16, 16)
+func (u NetUNIX) parseType(s string) (NetUNIXType, error) {
+	typ, err := strconv.ParseUint(s, 16, 16)
 	if err != nil {
 		return 0, err
 	}
-	return NetUnixType(typ), nil
+
+	return NetUNIXType(typ), nil
 }
 
-func (u NetUnix) parseFlags(hexStr string) (NetUnixFlags, error) {
-	flags, err := strconv.ParseUint(hexStr, 16, 32)
+func (u NetUNIX) parseFlags(s string) (NetUNIXFlags, error) {
+	flags, err := strconv.ParseUint(s, 16, 32)
 	if err != nil {
 		return 0, err
 	}
-	return NetUnixFlags(flags), nil
+
+	return NetUNIXFlags(flags), nil
 }
 
-func (u NetUnix) parseState(hexStr string) (NetUnixState, error) {
-	st, err := strconv.ParseInt(hexStr, 16, 8)
+func (u NetUNIX) parseState(s string) (NetUNIXState, error) {
+	st, err := strconv.ParseInt(s, 16, 8)
 	if err != nil {
 		return 0, err
 	}
-	return NetUnixState(st), nil
+
+	return NetUNIXState(st), nil
 }
 
-func (u NetUnix) parseInode(inodeStr string) (uint64, error) {
-	return strconv.ParseUint(inodeStr, 10, 64)
+func (u NetUNIX) parseInode(s string) (uint64, error) {
+	return strconv.ParseUint(s, 10, 64)
 }
 
-func (t NetUnixType) String() string {
+func (t NetUNIXType) String() string {
 	switch t {
 	case netUnixTypeStream:
 		return "stream"
@@ -247,7 +233,7 @@ func (t NetUnixType) String() string {
 	return "unknown"
 }
 
-func (f NetUnixFlags) String() string {
+func (f NetUNIXFlags) String() string {
 	switch f {
 	case netUnixFlagListen:
 		return "listen"
@@ -256,7 +242,7 @@ func (f NetUnixFlags) String() string {
 	}
 }
 
-func (s NetUnixState) String() string {
+func (s NetUNIXState) String() string {
 	switch s {
 	case netUnixStateUnconnected:
 		return "unconnected"
diff --git a/proc_status.go b/proc_status.go
index e30c2b8..17b4516 100644
--- a/proc_status.go
+++ b/proc_status.go
@@ -71,6 +71,9 @@ type ProcStatus struct {
 	VoluntaryCtxtSwitches uint64
 	// Number of involuntary context switches.
 	NonVoluntaryCtxtSwitches uint64
+
+	// UIDs of the process (Real, effective, saved set, and filesystem UIDs (GIDs))
+	UIDs [4]string
 }
 
 // NewStatus returns the current status information of the process.
@@ -114,6 +117,8 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt
 		s.TGID = int(vUint)
 	case "Name":
 		s.Name = vString
+	case "Uid":
+		copy(s.UIDs[:], strings.Split(vString, "\t"))
 	case "VmPeak":
 		s.VmPeak = vUintBytes
 	case "VmSize":


Test patch:
diff --git a/crypto_test.go b/crypto_test.go
index 4d45824..a5b0478 100644
--- a/crypto_test.go
+++ b/crypto_test.go
@@ -19,12 +19,6 @@ import (
 	"github.com/google/go-cmp/cmp"
 )
 
-func newint64(i int64) *int64 {
-	return &i
-}
-func newuint64(i uint64) *uint64 {
-	return &i
-}
 func TestFS_Crypto(t *testing.T) {
 	fs := getProcFixtures(t)
 	crypto, err := fs.Crypto()
@@ -34,12 +28,86 @@ func TestFS_Crypto(t *testing.T) {
 	}
 
 	refs := []Crypto{
-		{Name: "ccm(aes)", Driver: "ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))", Module: "ccm", Priority: newint64(300), Refcnt: newint64(4), Selftest: "passed", Internal: "no", Type: "aead", Async: false, Blocksize: newuint64(1), Ivsize: newuint64(16), Maxauthsize: newuint64(16), Geniv: "<none>"},
-		{Name: "cbcmac(aes)", Driver: "cbcmac(aes-aesni)", Module: "ccm", Priority: newint64(300), Refcnt: newint64(7), Selftest: "passed", Internal: "no", Type: "shash", Blocksize: newuint64(1), Digestsize: newuint64(16)},
-		{Name: "ecdh", Driver: "ecdh-generic", Module: "ecdh_generic", Priority: newint64(100), Refcnt: newint64(1), Selftest: "passed", Internal: "no", Type: "kpp"},
-		{Name: "ecb(arc4)", Driver: "ecb(arc4)-generic", Module: "arc4", Priority: newint64(100), Refcnt: newint64(1), Selftest: "passed", Internal: "no", Type: "skcipher", Async: false, Blocksize: newuint64(1), MinKeysize: newuint64(1), MaxKeysize: newuint64(256), Ivsize: newuint64(0), Chunksize: newuint64(1), Walksize: newuint64(1)},
-		{Name: "arc4", Driver: "arc4-generic", Module: "arc4", Priority: newint64(0), Refcnt: newint64(3), Selftest: "passed", Internal: "no", Type: "cipher", Blocksize: newuint64(1), MinKeysize: newuint64(1), MaxKeysize: newuint64(256)},
-		{Name: "crct10dif", Driver: "crct10dif-pclmul", Module: "crct10dif_pclmul", Priority: newint64(200), Refcnt: newint64(2), Selftest: "passed", Internal: "no", Type: "shash", Blocksize: newuint64(1), Digestsize: newuint64(2)},
+		{
+			Name:        "ccm(aes)",
+			Driver:      "ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))",
+			Module:      "ccm",
+			Priority:    newint64(300),
+			Refcnt:      newint64(4),
+			Selftest:    "passed",
+			Internal:    "no",
+			Type:        "aead",
+			Async:       false,
+			Blocksize:   newuint64(1),
+			Ivsize:      newuint64(16),
+			Maxauthsize: newuint64(16),
+			Geniv:       "<none>",
+		},
+		{
+			Name:       "cbcmac(aes)",
+			Driver:     "cbcmac(aes-aesni)",
+			Module:     "ccm",
+			Priority:   newint64(300),
+			Refcnt:     newint64(7),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "shash",
+			Blocksize:  newuint64(1),
+			Digestsize: newuint64(16),
+		},
+		{
+			Name:     "ecdh",
+			Driver:   "ecdh-generic",
+			Module:   "ecdh_generic",
+			Priority: newint64(100),
+			Refcnt:   newint64(1),
+			Selftest: "passed",
+			Internal: "no",
+			Type:     "kpp",
+			Async:    true,
+		},
+		{
+			Name:       "ecb(arc4)",
+			Driver:     "ecb(arc4)-generic",
+			Module:     "arc4",
+			Priority:   newint64(100),
+			Refcnt:     newint64(1),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "skcipher",
+			Async:      false,
+			Blocksize:  newuint64(1),
+			MinKeysize: newuint64(1),
+			MaxKeysize: newuint64(256),
+			Ivsize:     newuint64(0),
+			Chunksize:  newuint64(1),
+			Walksize:   newuint64(1),
+		},
+		{
+			Name:       "arc4",
+			Driver:     "arc4-generic",
+			Module:     "arc4",
+			Priority:   newint64(0),
+			Refcnt:     newint64(3),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "cipher",
+			Blocksize:  newuint64(1),
+			MinKeysize: newuint64(1),
+			MaxKeysize: newuint64(256),
+		},
+		{
+			Name:       "crct10dif",
+			Driver:     "crct10dif-pclmul",
+			Module:     "crct10dif_pclmul",
+			Priority:   newint64(200),
+			Refcnt:     newint64(2),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "shash",
+			Blocksize:  newuint64(1),
+			Digestsize: newuint64(2),
+		},
 	}
 
 	if want, have := len(refs), len(crypto); want > have {
@@ -52,3 +120,11 @@ func TestFS_Crypto(t *testing.T) {
 		}
 	}
 }
+
+func newint64(i int64) *int64 {
+	return &i
+}
+
+func newuint64(i uint64) *uint64 {
+	return &i
+}
diff --git a/net_udp_test.go b/net_udp_test.go
new file mode 100644
index 0000000..563530c
--- /dev/null
+++ b/net_udp_test.go
@@ -0,0 +1,227 @@
+// Copyright 2019 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 Test_parseNetUDPLine(t *testing.T) {
+	tests := []struct {
+		fields  []string
+		name    string
+		want    *netUDPLine
+		wantErr bool
+	}{
+		{
+			name:   "reading valid lines, no issue should happened",
+			fields: []string{"11:", "00000000:0000", "00000000:0000", "0A", "00000017:0000002A", "0:0", "0", "1000"},
+			want: &netUDPLine{
+				Sl:        11,
+				LocalAddr: 0,
+				LocalPort: 0,
+				RemAddr:   0,
+				RemPort:   0,
+				St:        10,
+				TxQueue:   23,
+				RxQueue:   42,
+				UID:       1000,
+			},
+		},
+		{
+			name:    "error case - invalid line - number of fields/columns < 8",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0:0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse sl - not a valid uint",
+			fields:  []string{"a:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse local_address - not a valid hex",
+			fields:  []string{"1:", "0000000O:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse rem_address - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "0000000O:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - cannot parse line - missing colon",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0000000000000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse tx_queue - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "DEADCODE:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse rx_queue - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:FEEDCODE", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse UID - not a valid uint",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "-10"},
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseNetUDPLine(tt.fields)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseNetUDPLine() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if tt.want == nil && got != nil {
+				t.Errorf("parseNetUDPLine() = %v, want %v", got, tt.want)
+			}
+			if got != nil {
+				if (got.Sl != tt.want.Sl) ||
+					(got.LocalAddr != tt.want.LocalAddr) ||
+					(got.LocalPort != tt.want.LocalPort) ||
+					(got.RemAddr != tt.want.RemAddr) ||
+					(got.RemPort != tt.want.RemPort) ||
+					(got.St != tt.want.St) ||
+					(got.TxQueue != tt.want.TxQueue) ||
+					(got.RxQueue != tt.want.RxQueue) ||
+					(got.UID != tt.want.UID) {
+
+					t.Errorf("parseNetUDPLine() = %#v, want %#v", got, tt.want)
+				}
+			}
+		})
+	}
+}
+
+func Test_newNetUDP(t *testing.T) {
+	tests := []struct {
+		name    string
+		file    string
+		want    NetUDP
+		wantErr bool
+	}{
+		{
+			name: "udp file found, no error should come up",
+			file: "fixtures/proc/net/udp",
+			want: []*netUDPLine{
+				&netUDPLine{
+					Sl: 0, LocalAddr: 0, LocalPort: 22, RemAddr: 0, RemPort: 0, St: 10, TxQueue: 0, RxQueue: 1, UID: 0,
+				},
+				&netUDPLine{
+					Sl: 1, LocalAddr: 0, LocalPort: 22, RemAddr: 0, RemPort: 0, St: 10, TxQueue: 1, RxQueue: 0, UID: 0,
+				},
+				&netUDPLine{
+					Sl: 2, LocalAddr: 0, LocalPort: 22, RemAddr: 0, RemPort: 0, St: 10, TxQueue: 1, RxQueue: 1, UID: 0,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "udp6 file found, no error should come up",
+			file: "fixtures/proc/net/udp6",
+			want: []*netUDPLine{
+				&netUDPLine{
+					Sl: 1315, LocalAddr: 0, LocalPort: 5355, RemAddr: 0, RemPort: 0, St: 7, TxQueue: 0, RxQueue: 0, UID: 981,
+				},
+				&netUDPLine{
+					Sl: 6073, LocalAddr: 0, LocalPort: 51073, RemAddr: 0, RemPort: 0, St: 7, TxQueue: 0, RxQueue: 0, UID: 1000,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:    "error case - file not found",
+			file:    "somewhere over the rainbow",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse error",
+			file:    "fixtures/proc/net/udp_broken",
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := newNetUDP(tt.file)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newNetUDP() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newNetUDP() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_newNetUDPSummary(t *testing.T) {
+	tests := []struct {
+		name    string
+		file    string
+		want    *NetUDPSummary
+		wantErr bool
+	}{
+		{
+			name:    "udp file found, no error should come up",
+			file:    "fixtures/proc/net/udp",
+			want:    &NetUDPSummary{TxQueueLength: 2, RxQueueLength: 2, UsedSockets: 3},
+			wantErr: false,
+		},
+		{
+			name:    "udp6 file found, no error should come up",
+			file:    "fixtures/proc/net/udp6",
+			want:    &NetUDPSummary{TxQueueLength: 0, RxQueueLength: 0, UsedSockets: 2},
+			wantErr: false,
+		},
+		{
+			name:    "error case - file not found",
+			file:    "somewhere over the rainbow",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse error",
+			file:    "fixtures/proc/net/udp_broken",
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := newNetUDPSummary(tt.file)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newNetUDPSummary() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newNetUDPSummary() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/net_unix_test.go b/net_unix_test.go
index 4643ced..2de59bb 100644
--- a/net_unix_test.go
+++ b/net_unix_test.go
@@ -15,21 +15,51 @@ package procfs
 
 import (
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+// Whether or not to run tests with inode fixtures.
+const (
+	checkInode   = true
+	noCheckInode = false
 )
 
-func TestNewNetUnix(t *testing.T) {
+func TestNetUnix(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	got, err := fs.NetUNIX()
+	if err != nil {
+		t.Fatalf("failed to get UNIX socket data: %v", err)
+	}
+
+	testNetUNIX(t, checkInode, got)
+}
+
+func TestNetUnixNoInode(t *testing.T) {
 	fs, err := NewFS(procTestFixtures)
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("failed to open procfs: %v", err)
 	}
 
-	nu, err := fs.NewNetUnix()
+	got, err := readNetUNIX(fs.proc.Path("net/unix_without_inode"))
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("failed to read UNIX socket data: %v", err)
 	}
 
-	lines := []*NetUnixLine{
-		&NetUnixLine{
+	testNetUNIX(t, noCheckInode, got)
+}
+
+func testNetUNIX(t *testing.T, testInode bool, got *NetUNIX) {
+	t.Helper()
+
+	// First verify that the input data matches a prepopulated structure.
+
+	want := []*NetUNIXLine{
+		{
 			KernelPtr: "0000000000000000",
 			RefCount:  2,
 			Flags:     1 << 16,
@@ -38,7 +68,7 @@ func TestNewNetUnix(t *testing.T) {
 			Inode:     3442596,
 			Path:      "/var/run/postgresql/.s.PGSQL.5432",
 		},
-		&NetUnixLine{
+		{
 			KernelPtr: "0000000000000000",
 			RefCount:  10,
 			Flags:     1 << 16,
@@ -47,7 +77,7 @@ func TestNewNetUnix(t *testing.T) {
 			Inode:     10061,
 			Path:      "/run/udev/control",
 		},
-		&NetUnixLine{
+		{
 			KernelPtr: "0000000000000000",
 			RefCount:  7,
 			Flags:     0,
@@ -56,7 +86,7 @@ func TestNewNetUnix(t *testing.T) {
 			Inode:     12392,
 			Path:      "/dev/log",
 		},
-		&NetUnixLine{
+		{
 			KernelPtr: "0000000000000000",
 			RefCount:  3,
 			Flags:     0,
@@ -65,7 +95,7 @@ func TestNewNetUnix(t *testing.T) {
 			Inode:     4787297,
 			Path:      "/var/run/postgresql/.s.PGSQL.5432",
 		},
-		&NetUnixLine{
+		{
 			KernelPtr: "0000000000000000",
 			RefCount:  3,
 			Flags:     0,
@@ -75,105 +105,66 @@ func TestNewNetUnix(t *testing.T) {
 		},
 	}
 
-	if want, have := len(lines), len(nu.Rows); want != have {
-		t.Errorf("want %d parsed net/unix lines, have %d", want, have)
-	}
-	for i, gotLine := range nu.Rows {
-		if i >= len(lines) {
-			continue
-		}
-		line := lines[i]
-		if *line != *gotLine {
-			t.Errorf("%d item: got %v, want %v", i, *gotLine, *line)
+	// Enable the fixtures to be used for multiple tests by clearing the inode
+	// field when appropriate.
+	if !testInode {
+		for i := 0; i < len(want); i++ {
+			want[i].Inode = 0
 		}
 	}
 
-	wantedFlags := "listen"
-	flags := lines[0].Flags.String()
-	if wantedFlags != flags {
-		t.Errorf("unexpected flag str: want %s, got %s", wantedFlags, flags)
-	}
-	wantedFlags = "default"
-	flags = lines[3].Flags.String()
-	if wantedFlags != flags {
-		t.Errorf("unexpected flag str: wanted %s, got %s", wantedFlags, flags)
+	if diff := cmp.Diff(want, got.Rows); diff != "" {
+		t.Fatalf("unexpected /proc/net/unix data (-want +got):\n%s", diff)
 	}
 
-	wantedType := "stream"
-	typ := lines[0].Type.String()
-	if wantedType != typ {
-		t.Errorf("unexpected type str: wanted %s, got %s", wantedType, typ)
+	// Now test the field enumerations and ensure they match up correctly
+	// with the constants used to generate readable strings.
+
+	wantFlags := []NetUNIXFlags{
+		netUnixFlagListen,
+		netUnixFlagListen,
+		netUnixFlagDefault,
+		netUnixFlagDefault,
+		netUnixFlagDefault,
 	}
 
-	wantedType = "seqpacket"
-	typ = lines[1].Type.String()
-	if wantedType != typ {
-		t.Errorf("unexpected type str: want %s, got %s", wantedType, typ)
+	wantType := []NetUNIXType{
+		netUnixTypeStream,
+		netUnixTypeSeqpacket,
+		netUnixTypeDgram,
+		netUnixTypeStream,
+		netUnixTypeStream,
 	}
-}
 
-func TestNewNetUnixWithoutInode(t *testing.T) {
-	fs, err := NewFS(procTestFixtures)
-	if err != nil {
-		t.Fatal(err)
+	wantState := []NetUNIXState{
+		netUnixStateUnconnected,
+		netUnixStateUnconnected,
+		netUnixStateUnconnected,
+		netUnixStateConnected,
+		netUnixStateConnected,
 	}
-	nu, err := NewNetUnixByPath(fs.proc.Path("net/unix_without_inode"))
-	if err != nil {
-		t.Fatal(err)
+
+	var (
+		gotFlags []NetUNIXFlags
+		gotType  []NetUNIXType
+		gotState []NetUNIXState
+	)
+
+	for _, r := range got.Rows {
+		gotFlags = append(gotFlags, r.Flags)
+		gotType = append(gotType, r.Type)
+		gotState = append(gotState, r.State)
 	}
 
-	lines := []*NetUnixLine{
-		&NetUnixLine{
-			KernelPtr: "0000000000000000",
-			RefCount:  2,
-			Flags:     1 << 16,
-			Type:      1,
-			State:     1,
-			Path:      "/var/run/postgresql/.s.PGSQL.5432",
-		},
-		&NetUnixLine{
-			KernelPtr: "0000000000000000",
-			RefCount:  10,
-			Flags:     1 << 16,
-			Type:      5,
-			State:     1,
-			Path:      "/run/udev/control",
-		},
-		&NetUnixLine{
-			KernelPtr: "0000000000000000",
-			RefCount:  7,
-			Flags:     0,
-			Type:      2,
-			State:     1,
-			Path:      "/dev/log",
-		},
-		&NetUnixLine{
-			KernelPtr: "0000000000000000",
-			RefCount:  3,
-			Flags:     0,
-			Type:      1,
-			State:     3,
-			Path:      "/var/run/postgresql/.s.PGSQL.5432",
-		},
-		&NetUnixLine{
-			KernelPtr: "0000000000000000",
-			RefCount:  3,
-			Flags:     0,
-			Type:      1,
-			State:     3,
-		},
+	if diff := cmp.Diff(wantFlags, gotFlags); diff != "" {
+		t.Fatalf("unexpected /proc/net/unix flags (-want +got):\n%s", diff)
 	}
 
-	if want, have := len(lines), len(nu.Rows); want != have {
-		t.Errorf("want %d parsed net/unix lines, have %d", want, have)
+	if diff := cmp.Diff(wantType, gotType); diff != "" {
+		t.Fatalf("unexpected /proc/net/unix types (-want +got):\n%s", diff)
 	}
-	for i, gotLine := range nu.Rows {
-		if i >= len(lines) {
-			continue
-		}
-		line := lines[i]
-		if *line != *gotLine {
-			t.Errorf("%d item: got %v, want %v", i, *gotLine, *line)
-		}
+
+	if diff := cmp.Diff(wantState, gotState); diff != "" {
+		t.Fatalf("unexpected /proc/net/unix states (-want +got):\n%s", diff)
 	}
 }
diff --git a/proc_status_test.go b/proc_status_test.go
index 494a34d..828c1c9 100644
--- a/proc_status_test.go
+++ b/proc_status_test.go
@@ -75,3 +75,19 @@ func TestProcStatusName(t *testing.T) {
 		t.Errorf("want name %s, have %s", want, have)
 	}
 }
+
+func TestProcStatusUIDs(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 := [4]string{"1000", "1000", "1000", "0"}, s.UIDs; want != have {
+		t.Errorf("want uids %s, have %s", want, have)
+	}
+}