© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_3

3. Accessing proc File System

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 

In Chapter 2, you looked at the /sys filesystem in Linux and wrote a simple app to extract information from it. In this chapter, you are going to take a look at another system directory called /proc. The /proc directory is also known as procfs, and it contains useful information about processes that are currently running. The kernel uses it as an information center for all related processes.

In this chapter, you will learn how to do the following:
  • Look at the different information available inside procfs

  • Write an application to read procfs

  • Use an open source library to interface with procfs

Source Code

The source code for this chapter is available from the https://github.com/Apress/Software-Development-Go repository.

Peeking Inside procfs

In this section, you will look at procfs and see what it contains. Use the following command in your terminal to view what is available inside the /proc directory:
ls /proc -la
You will see output like the following:
dr-xr-xr-x 423 root            root       0 Jul 17 17:55 .
drwxr-xr-x  20 root            root       4096 May 25 13:21 ..
dr-xr-xr-x   9 root            root       0 Jul 17 17:55 1
dr-xr-xr-x   9 root            root       0 Jul 17 17:56 10
dr-xr-xr-x   9 nanik           nanik      0 Jul 17 18:02 10023
dr-xr-xr-x   9 nanik           nanik      0 Jul 17 18:02 10057
dr-xr-xr-x   9 nanik           nanik      0 Jul 17 18:02 10075
dr-xr-xr-x   9 root            root       0 Jul 17 17:56 101
...
-r--r--r--   1 root            root       0 Jul 17 17:56 execdomains
-r--r--r--   1 root            root       0 Jul 17 17:56 fb
-r--r--r--   1 root            root       0 Jul 17 17:55 filesystems
dr-xr-xr-x   5 root            root       0 Jul 17 17:56 fs
-r--r--r--   1 root            root       0 Jul 17 17:56 interrupts
-r--r--r--   1 root            root       0 Jul 17 17:56 iomem
-r--r--r--   1 root             root       0 Jul 17 17:56 ioports
dr-xr-xr-x  59 root            root       0 Jul 17 17:56 irq
-r--r--r--   1 root            root       0 Jul 17 17:56 kallsyms
-r--r--r--   1 root            root       0 Jul 17 17:56 keys
-r--r--r--   1 root            root       0 Jul 17 17:56 key-users
-r--------   1 root            root       0 Jul 17 17:56 kmsg
-r--------   1 root            root       0 Jul 17 17:56 kpagecgroup
-r--------   1 root            root       0 Jul 17 17:56 kpagecount
...
dr-xr-xr-x   5 root            root       0 Jul 17 17:56 sysvipc
lrwxrwxrwx   1 root            root       0 Jul 17 17:55 thread-self -> 17987/task/17987
-r--------   1 root            root       0 Jul 17 17:56 timer_list
dr-xr-xr-x   6 root            root       0 Jul 17 17:56 tty
-r--r--r--   1 root            root       0 Jul 17 17:55 uptime
-r--r--r--   1 root             root       0 Jul 17 17:56 version
-r--------   1 root            root       0 Jul 17 17:56 vmallocinfo
-r--r--r--   1 root            root       0 Jul 17 17:56 vmstat
-r--r--r--   1 root            root       0 Jul 17 17:56 zoneinfo

The output contains a lot of numerical directories. These directories correspond to the process id of applications running in the system, and inside these directories is more detailed information about the corresponding process, such as the command used to run the process, memory maps to executables and library files, and more.

Let’s take a look at one of the processes that is running on my system. I picked the process id that is allocated for the Goland IDE. In this case, the process id is 4280. Table 3-1 shows the information from inside /proc/4280.
Table 3-1

Information from /proc/4280

Directory

Content

/proc/4280/cmdline

/bin/sh./goland.sh

/proc/4280/cgroup

14:misc:/

13:rdma:/

11:hugetlb:/

10:net_prio:/

9:perf_event:/

8:net_cls:/

7:freezer:/

6:devices:/

4:blkio:/

3:cpuacct:/

2:cpu:/

1:cpuset:/

0::/user.slice/user-1000.slice/[email protected]/app.slice/app-org.gnome.Terminal.slice/vte-spawn-9c827742-8e1f-42d8-bb25-79119712b0d8.scope

/proc/4280/mountinfo

24 31 0:22 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw

...

27 26 0:24 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000

28 31 0:25 / /run rw,nosuid,nodev,noexec,relatime shared:5 - tmpfs tmpfs rw,size=1607888k,mode=755,inode64

...

As you can see from the table, there is much information that can be extracted that is relevant to the process id 4280. This information gives us better visibility about the application, resources the application uses, user and group information, and more.

Reading Memory Information

In the previous section, you learned what procfs is all about and looked at some of the process information that can be viewed. You looked at extracting the information by going into the /proc directory and using standard tools like ls and cat to view file and directory content.

In this section, you are going to write a simple application to read system memory information from procfs. The sample code can be found inside the chapter3/readingmemory directory. Run the application using the following command:
go run main.go
You will see output like the following:
MemTotal = 32320240 KB, MemFree = 3260144 KB, MemUsed = 29060096 KB
MemTotal = 32320240 KB, MemFree = 3146556 KB, MemUsed = 29173684 KB
MemTotal = 32320240 KB, MemFree = 3074524 KB, MemUsed = 29245716 KB
MemTotal = 32320240 KB, MemFree = 3068300 KB, MemUsed = 29251940 KB
MemTotal = 32320240 KB, MemFree = 3264940 KB, MemUsed = 29055300 KB
MemTotal = 32320240 KB, MemFree = 3269584 KB, MemUsed = 29050656 KB
MemTotal = 32320240 KB, MemFree = 3270340 KB, MemUsed = 29049900 KB
The application continuously prints memory information (total memory, free memory, and used memory) in kilobytes of the local device. Let’s look at the code to understand how it works.
func main() {
  sampler := &sampler{
     rate: 1 * time.Second,
  }
  ...
  for {
     select {
     case sampleSet := <-sampler.sample:
        ...
        fmt.Printf("total = %v KB, free = %v KB, used = %v KB ",
           s.total, s.free, s.used)
     }
  }
}

On startup, the code initializes the Sampler struct and goes into a loop waiting on the data to be made available from SampleSetChan. Once the data arrives, it prints out the memory information into the console.

The data sampling code that collects the data and sends it to the channel is seen below. The StartSampling function spins off a Go routine that calls GetMemSample to extract the memory information and sleep after sending the data to the SampleSetChan channel.
func (s *sampler) start() *sampler {
  ...
  go func() {
     for {
        var ss sample
        ss.memorySample = getMemorySample()
        s.sample <- ss
        time.Sleep(s.rate)
     }
  }()
  ...
}
The crux of reading the memory information can be seen in the following GetMemSample function:
func getMemorySample() (samp memory) {
  ...
  contents, err := ioutil.ReadFile(memInfo)
  if err != nil {
     return
  }
  reader := bufio.NewReader(bytes.NewBuffer(contents))
  for {
     line, _, err := reader.ReadLine()
     if err == io.EOF {
        break
     }
     ...
     if ok && len(fields) == 3 {
        ...
        switch fieldName {
        case "total:":
           samp.total = val
        case "free:":
           samp.free = val
        }
     }
  }
  ...
}

The memory information is collected from the /proc/meminfo directory. The collected data is parsed and only values that it is interested in are stored, namely total memory, free memory, and calculated value of memory used.

This is how the raw data looks like when reading the /proc/meminfo directory:
MemTotal:       32320240 kB
MemFree:          927132 kB
MemAvailable:    5961720 kB
...
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
...

Peeking Network Information

In this section, you will now look at network information that can be extracted out from procfs. There is a directory named /proc/net/sockstat that looks like the following in raw format:
sockets: used 3229
TCP: inuse 49 orphan 0 tw 82 alloc 64 mem 90
UDP: inuse 28 mem 139
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0
Table 3-2 explains the meaning of the different fields shown in the raw information above
Table 3-2.

/proc/net data breakdown

Sockets

Used

Total number of all protocol sockets used

TCP

inuse

Total number of TCP sockets listening

 

orphan

Total number of TCP sockets that do not belong to any process (a.k.a. orphans)

 

tw

Total number of TCP sockets that are time waiting or waiting to be closed

 

alloc

Total number of TCP sockets that have been allocated

 

mem

Total number of pages allocated to TCP

UDP

inuse

The number of UDP sockets in use

 

mem

Total number of pages allocated to UDP

UDPLITE

inuse

Total number of Lightweight UDP in use

RAW

inuse

Total number of raw protocols in use

FRAG

inuse

Number of IP segments used

 

memory

Total amount of memory in KB allocated for fragmentation reassembly

Now that you have a good idea of what the different values mean, let’s take a look at how to extract this information using Go. The sample code is inside the chapter3/sockstat directory. Open terminal and run the code using the following command:
go run main.go
Figure 3-1 shows the output.

A table explains the output in columns named U D P in use, U D P Nem, sockets, used, Top in use, T C P orphan, T C P T W, and T C P A L L O C.

Figure 3-1

sockstat sample output

Let’s explore the code to understand what it is doing. When the app starts up, it opens the /proc/net/sockstat directory. On success, the code reads and parses it to the format suitable for displaying to the console.
const (
  ...
  netstat = "/proc/net/sockstat"
)
  ...
func main() {
  fs, err := os.Open(netstat)
  ...
  m := make(map[string]int64)
  for {
     line, err := readLine(reader)
     if bytes.HasPrefix(line, []byte(sockets)) ||
        bytes.HasPrefix(line, []byte(tcp)) ||
        bytes.HasPrefix(line, []byte((udp))) {
        idx := bytes.Index(line, []byte((colon)))
        ...
     }
        ...
  }
  ...
}
As you can see, it is straightforward to write an application to read system-level information from procfs. To write an application to read procfs, the following is the information you will need to know beforehand:
  • In what directory is the required information located?

  • Do you need root access to access the information?

  • How will you parse the raw data properly and handle data parsing issues?

Using the procfs Library

You now understood what kind of information available inside the /proc directory and you’ve also seen how to write code and parse the information. In this section, you are going to take a look at an open source library that provides access to different information available in the /proc directory. The project can be found at https://github.com/jandre/procfs.

Code Sample

Open your terminal and change to the chapter3/jandreprocfs directory and run the code using the following command:
go run main.go
You will see output that looks like Figure 3-2

A screenshot of the output of the code on a dark screen with the address slash home slash Nanik.

Figure 3-2

Output running procfs sample code

The following code snippet uses the jandre/procfs library to read the information:
package main
import (
  "github.com/jandre/procfs"
  ...
)
func main() {
  processes, _ := procfs.Processes(false)
  table := tablewriter.NewWriter(os.Stdout)
  for _, p := range processes {
     table.Append([]string{strconv.Itoa(p.Pid), p.Exe, p.Cwd})
  }
  table.Render()
}

The sample code is simpler than the previous code that you looked at in the previous sections. It uses the procfs.Processes(..) function call to obtain all the current processes.

Inside the procfs Library

Let’s take a look a bit deeper into the library to investigate what exactly it is doing. You are going to dive into the following procfs.Processes(..) function call. The Processes function call inside the library looks like the following:
func Processes(lazy bool) (map[int]*Process, error) {
  ...
  files, err := ioutil.ReadDir("/proc")
  if err != nil {
     return nil, err
  }
  ...
  fetch := func(pid int) {
     proc, err := NewProcess(pid, lazy)
     if err != nil {
        ...
        done <- nil
     } else {
        done <- proc
     }
  }
  todo := len(pids)
  for _, pid := range pids {
     go fetch(pid)
  }
  ...
  for ;todo > 0; {
     proc := <-done
     todo--
     if proc != nil {
        processes[proc.Pid] = proc
     }
  }
  return processes, nil
}
At a high level, Figure 3-3 shows what the function is actually doing.

A flow diagram of the main dot go passes processes function is divided into 3 functions named fetch P I D and connected with the help of synchronous and asynchronous parallel to done processes.

Figure 3-3

Processes(..) function flow

The function reads processes information from the /proc directory, and it traverses through it by reading each process information in a separate Go routine that calls the fetch(pid) function. The function extracts and parses information of the process id that it is assigned. Once collected, it passes into the channel that the Processes(..) function is waiting on; in this case, it is called the done channel.

All the heavy lifting of opening and traversing through the /proc directory including parsing the results is taken care of by the library. The application can just focus on the output that it receives.

Summary

In this chapter, you looked at the /proc file system and learned about the system information that applications have access to. You looked at sample code to read information from inside the /proc directory that is related to the network and memory on the device. You also learned that the bulk of the code that needs to be written when extracting system information is in terms of reading and parsing the information. You also looked at an open source library that can provide functionality in reading the /proc directory that performs all the heavy lifting, leaving you to focus on writing simpler code to read all the system information that you need.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.21.190