Last update: 04-Dec-2022
Author: R. Koucha
Monitoring /proc files efficiently
Introduction

The files in /proc file system are useful to check the health of the system. So, many software monitors them with basic open/read/close operations. Let's see how we can do it more efficiently.

Mapping a file from /proc in memory

Let's consider the following example program which maps the content of /proc/meminfo

#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define MEMINFO_FNAME "/proc/meminfo"


void *mmap_file(const char *name, size_t *sz)
{
  int fd;
  int rc;
  size_t size_in_bytes, page_size, s;
  void *mem;

  // Open the file
  fd = open(MEMINFO_FNAME, O_RDONLY);
  if (fd < 0) {
    fprintf(stderr, "open(%s): '%m' (%d)\n", name, errno);
    return NULL;
  }

  // Get the size of the file rounded in page size (fstat() would return 0 for the size)
  s = (size_t)lseek(fd, SEEK_END, 0);
  if ((off_t)s < 0 || !s) {
    fprintf(stderr, "lseek(%s, %zu, %d): '%m' (%d)\n", name, s, fd, errno);
    return NULL;
  }
		 
  page_size = (size_t)sysconf(_SC_PAGESIZE);
  size_in_bytes = (s / page_size);
  size_in_bytes += (s % page_size ? 1 : 0);
  size_in_bytes = size_in_bytes * page_size;

  // Map the file in memory
  mem = mmap(NULL, size_in_bytes, PROT_READ, MAP_SHARED, fd, 0);
  if (mem == MAP_FAILED) {
    fprintf(stderr, "mmap(%s, %zu, %zu, %d): '%m' (%d)\n", name, size_in_bytes, page_size, fd, errno);
    return NULL;
  }

  // File descriptor non longer needed
  close(fd);

  *sz = s;

  return mem;
}

int main(void)
{
  void *mem;
  size_t sz;
  char *str;

  mem = mmap_file(MEMINFO_FNAME, &sz);
  if (!mem) {
    return 1;
  }

  // Null terminated string
  str = (char *)malloc(sz + 1);
  if (!str) {
    fprintf(stderr, "malloc(%zu + 1): '%m' (%d)\n", sz, errno);
    return 1;
  }

  while (1) {
    memcpy(str, (char *)mem, sz);
    str[sz] = '\0';
    printf("%s", str);
    sleep(1);
  }

  return 0;
}

The execution shows an error from mmap() because procfs does not accept this operation:

$ gcc meminfo.c -o meminfo
$ ./meminfo
mmap(/proc/meminfo, 4096, 4096, 3): 'Input/output error' (5)
Keeping the file opened

The following example program keeps the file opened and rewinds the read pointer thanks to the call to lseek():

#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define MEMINFO_FNAME "/proc/meminfo"

int read_file(int fd)
{
  int rc;
  char buffer[512];
  off_t off;

  off = lseek(fd, 0, SEEK_SET);
  if (off == (off_t)-1) {
    fprintf(stderr, "lseek(%s): '%m' (%d)\n", MEMINFO_FNAME, errno);
    return -1;
  }

  do {

    rc = read(fd, buffer, sizeof(buffer));
    if (rc > 0) {
      buffer[rc] = '\0';
      printf("%s", buffer);
    }
    
  } while(rc > 0);

  return rc;
}

int main(void)
{
  int fd;
  int rc;

  // Open the file
  fd = open(MEMINFO_FNAME, O_RDONLY);
  if (fd < 0) {
    fprintf(stderr, "open(%s): '%m' (%d)\n", MEMINFO_FNAME, errno);
    return 1;
  }

  do {
    printf("==========\n");
    rc = read_file(fd);
    sleep(1);
  } while(rc == 0);

  close(fd);

  return 0;
}

The execution shows varying values after each reads in the file:

$ gcc meminfo.c -o meminfo
$ ./meminfo
==========
MemTotal:       131927796 kB
MemFree:         9305304 kB
MemAvailable:   125498348 kB
Buffers:        53300112 kB
[...]
==========
MemTotal:       131927796 kB
MemFree:         9323984 kB
MemAvailable:   125517048 kB
Buffers:        53300112 kB
[...]
==========
MemTotal:       131927796 kB
MemFree:         9324236 kB
MemAvailable:   125517300 kB
Buffers:        53300120 kB
[...]
Minimizing the number of system calls

Running the preceding program with strace points out 3 calls to read() to get the content of the whole file:

$ strace ./meminfo
openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
brk(NULL)                               = 0x559be3d21000
brk(0x559be3d42000)                     = 0x559be3d42000
write(1, "==========\n", 11==========
)            = 11
lseek(3, 0, SEEK_SET)                   = 0
read(3, "MemTotal:       131927796 kB\nMem"..., 512) = 512
write(1, "MemTotal:       131927796 kB\nMem"..., 506MemTotal:       131927796 kB
MemFree:        17196132 kB
MemAvailable:   125515556 kB
Buffers:        51164896 kB
Cached:          1804120 kB
SwapCached:        87472 kB
Active:         43977780 kB
Inactive:       10878860 kB
Active(anon):    1299996 kB
Inactive(anon):   641428 kB
Active(file):   42677784 kB
Inactive(file): 10237432 kB
Unevictable:         576 kB
Mlocked:             576 kB
SwapTotal:       4194300 kB
SwapFree:          74032 kB
Dirty:                56 kB
Writeback:             0 kB
) = 506
read(3, "ges:       1801660 kB\nMapped:   "..., 512) = 512
write(1, "AnonPages:       1801660 kB\nMapp"..., 507AnonPages:       1801660 kB
Mapped:           258984 kB
Shmem:             59168 kB
KReclaimable:   56462272 kB
Slab:           59304320 kB
SReclaimable:   56462272 kB
SUnreclaim:      2842048 kB
KernelStack:       29088 kB
PageTables:        90196 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    70158196 kB
Committed_AS:   15263204 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      226536 kB
VmallocChunk:          0 kB
Percpu:            53184 kB
) = 507
read(3, "rupted:     0 kB\nAnonHugePages: "..., 512) = 453
write(1, "HardwareCorrupted:     0 kB\nAnon"..., 464HardwareCorrupted:     0 kB
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:         0 kB
FilePmdMapped:         0 kB
CmaTotal:              0 kB
CmaFree:               0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:    57466820 kB
DirectMap2M:    76664832 kB
DirectMap1G:           0 kB
) = 464
read(3, "", 512)                        = 0
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffec60a68d0) = 0
write(1, "==========\n", 11==========
[...]

This is because of the size of the read buffer which is only 512 bytes. To auto-adapt the size of the read buffer to the size of the monitored file, at open time, the size could be obtained with a call to lseek() to move the pointer up to the end of the file. But a call like lseek(fd, 0, SEEK_END) would return an error as well as a call to stat() would return a size equal to 0 bytes. Depending on the content of the file, the size is not a fixed value. Its content is dynamically built upon the calls to read(). As a consequence, the new read procedure consists to allocate and adjust the read buffer to the last amount of read data:

#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define MEMINFO_FNAME "/proc/meminfo"

int open_file(const char *fname, size_t *sz) {

  int fd;
  off_t off;

  // Open the file
  fd = open(fname, O_RDONLY);
  if (fd < 0) {
    fprintf(stderr, "open(%s): '%m' (%d)\n", fname, errno);
    return -1;
  }

  *sz = (size_t)off;

  return fd;
}

int read_file(int fd, char **buffer, size_t *sz)
{
  int rc;
  size_t off;
  char *ptr;
  off_t offset;
  size_t max;

#define BUFFER_INCREMENT 512

  if (!(*buffer)) {
    *buffer = (char *)malloc(BUFFER_INCREMENT);
    if (!(*buffer)) {
      fprintf(stderr, "malloc(%zu): '%m' (%d)\n", (size_t)BUFFER_INCREMENT, errno);
      *sz = 0;
      return 1;
    }
    *sz = BUFFER_INCREMENT;
  }

  // Rewind the read pointer
  offset = lseek(fd, 0, SEEK_SET);
  if (offset == (off_t)-1) {
    fprintf(stderr, "lseek(%d): '%m' (%d)\n", fd, errno);
    return -1;
  }

  off = 0;
  do {

    max = *sz - off - 1;
    rc = read(fd, *buffer + off, max);

    if (rc < 0) {
      fprintf(stderr, "read(%d): '%m' (%d)\n", fd, errno);
      return -1;
    }

    // Last read?
    if (rc < max) {
      (*buffer + off)[rc] = '\0';
      return (int)(off + rc);
    }

    ptr = realloc(*buffer, *sz + BUFFER_INCREMENT);
    if (!ptr) {
      fprintf(stderr, "realloc(%p, %zu): '%m' (%d)\n", *buffer, (size_t)BUFFER_INCREMENT, errno);
      break;
    }

    off += rc;
    *sz += BUFFER_INCREMENT;
    *buffer = ptr;
  } while (rc > 0);

  return -1;
}

int main(void)
{
  int fd;
  int rc;
  size_t sz;
  char *buffer = NULL;
  
  // Open the file
  fd = open_file(MEMINFO_FNAME, &sz);
  if (fd < 0) {
    fprintf(stderr, "open(%s): '%m' (%d)\n", MEMINFO_FNAME, errno);
    return 1;
  }

  do {
    printf("==========\n");
    rc = read_file(fd, &buffer, &sz);
    if (rc > 0) {
      printf("%s", buffer);
      sleep(1);
    }
  } while(rc > 0);

  if (buffer) {
    free(buffer);
  }

  close(fd);

  return 0;
}

At the first display, there are several read() system calls in order to adjust the size of the read buffer. Then, the subsequent calls invoke read() only once:

$ strace ./meminfo
[...]
openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
brk(NULL)                               = 0x55656c585000
brk(0x55656c5a6000)                     = 0x55656c5a6000
write(1, "==========\n", 11==========
)            = 11
lseek(3, 0, SEEK_SET)                   = 0
read(3, "MemTotal:       131927796 kB\nMem"..., 511) = 511
read(3, "ages:       1877528 kB\nMapped:  "..., 512) = 512
read(3, "rrupted:     0 kB\nAnonHugePages:"..., 512) = 454
write(1, "MemTotal:       131927796 kB\nMem"..., 1024MemTotal:       131927796 kB
MemFree:        19769048 kB
MemAvailable:   125432008 kB
Buffers:        50652156 kB
Cached:          1641168 kB
SwapCached:        87460 kB
Active:         43863936 kB
Inactive:       10393316 kB
Active(anon):    1375796 kB
Inactive(anon):   643528 kB
Active(file):   42488140 kB
Inactive(file):  9749788 kB
Unevictable:         576 kB
Mlocked:             576 kB
SwapTotal:       4194300 kB
SwapFree:          74268 kB
Dirty:               104 kB
Writeback:             0 kB
AnonPages:       1877528 kB
Mapped:           255816 kB
Shmem:             61260 kB
KReclaimable:   54483096 kB
Slab:           57327396 kB
SReclaimable:   54483096 kB
SUnreclaim:      2844300 kB
KernelStack:       29040 kB
PageTables:        91048 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    70158196 kB
Committed_AS:   15320692 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      226424 kB
VmallocChunk:          0 kB
Percpu:            53184 kB
HardwareCor) = 1024
write(1, "rupted:     0 kB\n", 17rupted:     0 kB
)      = 17
write(1, "AnonHugePages:         0 kB\n", 28AnonHugePages:         0 kB
) = 28
write(1, "ShmemHugePages:        0 kB\n", 28ShmemHugePages:        0 kB
) = 28
write(1, "ShmemPmdMapped:        0 kB\n", 28ShmemPmdMapped:        0 kB
) = 28
write(1, "FileHugePages:         0 kB\n", 28FileHugePages:         0 kB
) = 28
write(1, "FilePmdMapped:         0 kB\n", 28FilePmdMapped:         0 kB
) = 28
write(1, "CmaTotal:              0 kB\n", 28CmaTotal:              0 kB
) = 28
write(1, "CmaFree:               0 kB\n", 28CmaFree:               0 kB
) = 28
write(1, "HugePages_Total:       0\n", 25HugePages_Total:       0
) = 25
write(1, "HugePages_Free:        0\n", 25HugePages_Free:        0
) = 25
write(1, "HugePages_Rsvd:        0\n", 25HugePages_Rsvd:        0
) = 25
write(1, "HugePages_Surp:        0\n", 25HugePages_Surp:        0
) = 25
write(1, "Hugepagesize:       2048 kB\n", 28Hugepagesize:       2048 kB
) = 28
write(1, "Hugetlb:               0 kB\n", 28Hugetlb:               0 kB
) = 28
write(1, "DirectMap4k:    57470916 kB\n", 28DirectMap4k:    57470916 kB
) = 28
write(1, "DirectMap2M:    76660736 kB\n", 28DirectMap2M:    76660736 kB
) = 28
write(1, "DirectMap1G:           0 kB\n", 28DirectMap1G:           0 kB
) = 28
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffcebc0b270) = 0
write(1, "==========\n", 11==========
)            = 11
lseek(3, 0, SEEK_SET)                   = 0
read(3, "MemTotal:       131927796 kB\nMem"..., 1535) = 1477
write(1, "MemTotal:       131927796 kB\nMem"..., 1024MemTotal:       131927796 kB
MemFree:        19788924 kB
MemAvailable:   125451896 kB
Buffers:        50652156 kB
Cached:          1641180 kB
SwapCached:        87460 kB
Active:         43843404 kB
Inactive:       10393328 kB
Active(anon):    1355264 kB
Inactive(anon):   643528 kB
Active(file):   42488140 kB
Inactive(file):  9749800 kB
Unevictable:         576 kB
Mlocked:             576 kB
SwapTotal:       4194300 kB
SwapFree:          74268 kB
Dirty:               116 kB
Writeback:             0 kB
AnonPages:       1856296 kB
Mapped:           255636 kB
Shmem:             61260 kB
KReclaimable:   54483096 kB
Slab:           57327008 kB
SReclaimable:   54483096 kB
SUnreclaim:      2843912 kB
KernelStack:       28960 kB
PageTables:        90512 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    70158196 kB
Committed_AS:   15343516 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      226408 kB
VmallocChunk:          0 kB
Percpu:            53184 kB
HardwareCor) = 1024
write(1, "rupted:     0 kB\n", 17rupted:     0 kB
)      = 17
write(1, "AnonHugePages:         0 kB\n", 28AnonHugePages:         0 kB
) = 28
write(1, "ShmemHugePages:        0 kB\n", 28ShmemHugePages:        0 kB
) = 28
write(1, "ShmemPmdMapped:        0 kB\n", 28ShmemPmdMapped:        0 kB
) = 28
write(1, "FileHugePages:         0 kB\n", 28FileHugePages:         0 kB
) = 28
write(1, "FilePmdMapped:         0 kB\n", 28FilePmdMapped:         0 kB
) = 28
write(1, "CmaTotal:              0 kB\n", 28CmaTotal:              0 kB
) = 28
write(1, "CmaFree:               0 kB\n", 28CmaFree:               0 kB
) = 28
write(1, "HugePages_Total:       0\n", 25HugePages_Total:       0
) = 25
write(1, "HugePages_Free:        0\n", 25HugePages_Free:        0
) = 25
write(1, "HugePages_Rsvd:        0\n", 25HugePages_Rsvd:        0
) = 25
write(1, "HugePages_Surp:        0\n", 25HugePages_Surp:        0
) = 25
write(1, "Hugepagesize:       2048 kB\n", 28Hugepagesize:       2048 kB
) = 28
write(1, "Hugetlb:               0 kB\n", 28Hugetlb:               0 kB
) = 28
write(1, "DirectMap4k:    57470916 kB\n", 28DirectMap4k:    57470916 kB
) = 28
write(1, "DirectMap2M:    76660736 kB\n", 28DirectMap2M:    76660736 kB
) = 28
write(1, "DirectMap1G:           0 kB\n", 28DirectMap1G:           0 kB
) = 28
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffcebc0b270) = 0
[...]
Conclusion

We showed that the monitoring of the files in /proc does not necessary need an open() operation before each read. To minimize the number of system calls, the size of the read buffer can be adapted to the average size of the file to have only one call to read().

About the author

The author is an engineer in computer sciences located in France. He can be contacted here.