Retrieving the full path of a process on MacOS (and exploring procfs)
Tags: programming, go, linux
Published: 2018-09-30 18:50:06

---
title: Retrieving the full path of a process on MacOS (and exploring procfs)
published: true
description: Getting the absolute path of process on a MacOS machine might not be as simple as in Linux
tags: programming,go,linux
canonical_url: https://ops.tips/blog/macos-pid-absolute-path-and-procfs-exploration/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/hmxsylkw5jgiwgo44kr8.png
description: 
---

Hey,

Another day I was trying to make sure that a given process that I was running was using a specific binary that I had built, but I couldn’t figure out: `ps` would only show me the non-absolute path.

```sh
# How could I know what is the absolute path of the
# `hugo` binary, assuming that I could have multiple
# `hugo` binaries in `$PATH`?
ps
  PID TTY TIME CMD
 4153 ttys000 5:14.98 hugo serve <<<
 9035 ttys001 0:00.04 /Applications/iTerm.app/Content...
 9037 ttys001 0:00.10 -bash
 9086 ttys001 0:02.27 /usr/local/Cellar/macvim/8.1-15...
 9236 ttys002 0:00.04 /Applications/iTerm.app/Content...
 9238 ttys002 0:00.10 -bash
```

If I were using Linux though, I thought, that’d be easy: head to `/proc`, search for the `pid` of the process and then check what `exe` links to; done.

```sh
# (on a Linux machine ...)
#
# See `hugo` will still not show up with the absolute path
# like on MacOS.
ps aux | grep hugo
ubuntu 2275 0.0 0.0 101852 748 pts/0 Sl+ 00:26 0:00 hugo serve

# Given that the proc filesystem can provide us with some
# more information about the process, check out the `exe`
# link (which should provide a link to the actual executable).
stat /proc/2275/exe
  File: /proc/2275/exe -> /usr/local/bin/hugo
  Size: 0 Blocks: 0 IO Block: 1024 symbolic link
Device: 4h/4d	Inode: 140106 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 1001/ ubuntu) Gid: ( 1001/ ubuntu)
Access: 2018-09-24 00:26:37.167004005 +0000
Modify: 2018-09-24 00:26:27.391004005 +0000
Change: 2018-09-24 00:26:27.391004005 +0000
 Birth: -
```

In this post, I go through how we can gather such information on a MacOS, and what the `procfs` in Linux is all about.

_tl;dr: `/proc` on Linux is dope; on MacoS: compile a little code that uses [`proc_pidpath`](https://opensource.apple.com/source/xnu/xnu-2422.1.72/libsyscall/wrappers/libproc/libproc.h.auto.html) from `libproc`, or install [`pidpath`](https://github.com/cirocosta/pidpath)._


### The /proc filesystem in Linux

In “Linux land”, there’s this thing called “procfs”.

It’s a virtual filesystem - in the sense that there are no real regular files in your disk that map to the filesystem representation - that allows a user (in userspace) to perform some introspection about its current running process and others as well.

From [the kernel docs](https://www.kernel.org/doc/Documentation/filesystems/proc.txt):

> The proc file system acts as an interface to internal data structures in the kernel.
> 
> It can be used to obtain information about the system and to change certain kernel parameters at runtime ([sysctl](https://linux.die.net/man/8/sysctl)).

The way the interaction with it is set up is pretty nifty:

- each process receives a given path under `/proc` (like, `/proc/<pid>`), and then
- as subdirectories of this path, various other files and subdirectories are present to allow deeper introspection about the specific pid.

```sh
# Display the files and directories present at the
# very root of `/proc`.
#
# Here we can find the list of PIDs that we can access,
# as well as some more system-wide information and 
# settings that we can tweak.
ls -lah /proc
total 4.0K
dr-xr-xr-x 124 root root 0 Sep 24 23:56 .
drwxr-xr-x 24 root root 4.0K Sep 24 23:57 ..
dr-xr-xr-x 9 root root 0 Sep 24 23:56 1
dr-xr-xr-x 9 root root 0 Sep 25 00:54 2016
dr-xr-xr-x 9 root root 0 Sep 24 23:57 417
...
-r--r--r-- 1 root root 0 Sep 25 01:25 sched_debug
-r--r--r-- 1 root root 0 Sep 25 01:25 schedstat
dr-xr-xr-x 4 root root 0 Sep 25 01:25 scsi
lrwxrwxrwx 1 root root 0 Sep 24 23:56 self -> 2574
...

# Getting into a specific pid path, we're able to
# gather more information about the specifics of
# a given process.
ls -lah /proc/472
total 0
dr-xr-xr-x 9 root root 0 Sep 24 23:57 .
dr-xr-xr-x 123 root root 0 Sep 24 23:56 ..
...
lrwxrwxrwx 1 root root 0 Sep 25 01:30 cwd -> /
-r-------- 1 root root 0 Sep 25 01:30 environ
lrwxrwxrwx 1 root root 0 Sep 24 23:57 exe -> /lib/systemd/systemd-udevd
dr-x------ 2 root root 0 Sep 24 23:57 fd
lrwxrwxrwx 1 root root 0 Sep 25 01:30 root -> /
-rw-r--r-- 1 root root 0 Sep 25 01:30 sched
...
```

Not only that, `procfs` is very helpful when you’re not sure if a process is blocked on something you didn’t expect (like a `write(2)` to an `nfs` mount point that is malformed due to a bad set of servers not responding), or something simple as your process sleeping when you didn’t want to:

```sh
# Sleep for 33 days on the background
sleep 33d &
[1] 2786

# Check what's the state of the process
cat /proc/2786/stat
2786 (sleep) S ...
 | | |
 | | `-> state (interruptible sleep)
 | `-> command being run (sleep command)
 `-> process id (the pid we used before)

# Check what's the stack trace (from the kernel
# perspective) that led the process to this 
# sleep state
cat /proc/2786/stack
[<0>] hrtimer_nanosleep+0xd8/0x1d0
[<0>] SyS_nanosleep+0x72/0xa0
[<0>] do_syscall_64+0x73/0x130
[<0>] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[<0>] 0xffffffffffffffff
```

### procfs under the hood

What’s interesting about being virtual is that the implementation of `procfs` is able to generate the representation of the filesystem on the fly - whenever you issue an I/O call like `read(2)`, Linux answers back with what you asked for, be it the list of file descriptors opened by a given process, or the list of environment variables that were set at process startup time.

For instance, if tracing the execution of `cat /proc/<pid>/meminfo` down, we can find the path that the `read(2)` syscall takes:

```sh
# stack trace of `cat /proc/<pid>/meminfo`
        meminfo_proc_show
        proc_reg_read
        __vfs_read
        vfs_read
        sys_read
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

# stack trace of `cat /file.txt` 
# on an ext4 mount point
        ext4_file_read_iter
        __vfs_read
        vfs_read
        sys_read
        do_syscall_64
        entry_SYSCALL_64_after_hwframe
```

Very different from a regular `read` (as shown in the second stack trace), there’s no real file on disk being accessed - just `meminfo_proc_show` returning the contents related to what the user asked for: virtual memory stuff.

By the way, if you’re interested in knowing more about related subjects, a **great** reference for this type of knowledge is [The Linux Programming Interface: A Linux and UNIX System Programming Handbook](https://amzn.to/2QWyXp9).

Now to MacOS.

### The libproc library in MacOS

Differently from Linux, it feels like we can’t know all that much about how things work on MacOS.

After searching a bit on how to accomplish how to gather information about a process, `libproc` showed up.

As mentioned in [`libproc.h`](https://opensource.apple.com/source/xnu/xnu-2422.1.72/libsyscall/wrappers/libproc/libproc.h.auto.html):

```c
/*
 * This header file contains private interfaces 
 * to obtain process information.
 *
 * These interfaces are subject to change in future releases.
 */
```

One thing to note:

- the interfaces are **private** - no guaranteed compatibility with future releases.

This has been elucidated by an Apple staff member on [post](https://forums.developer.apple.com/thread/46963) at the Apple’s developer forum regarding gathering process information:

> […] Apple has not put a lot of effort into providing APIs for getting this sort of information.
> 
> What APIs that do exist were either inherited from OS X’s predecessor OSs or were added **primarily to meet our internal requirements rather than the needs for third-party developers**.
> 
> Thus, you will find a lot of places where these APIs are: incomplete; incorrect; poorly documented and aren’t as binary compatible as they should be.

Anyway, we can still make use of it - more specifically, we can make use of `proc_pidpath`, a method that takes a `pid` (the `pid` of the process that we want to know more about), a buffer where the path should be written to, and the buffer size.

```c
int proc_pidpath(
  int pid, // pid of the process to know more about
  void * buffer, // buffer to fill with the abs path
  uint32_t buffersize // size of the buffer
);
```

That said, we can go ahead and create our Go binary that can handle both Linux and MacOS by specifying two different compilation targets.

### A Golang binary that suits Linux and MacOS

Given that `libproc` will not be a thing under Linux, we can start by creating a `pidpath_linux.go` file that is meant to be compiled only on Linux, and another file, `pidpath_darwin.go`, aimed at MacOS machines.

The Linux one is rather simple: it follows the `/proc/<pid>/exe` symlink, and that’s it:

```go
// +build linux
package main

import (
	"os"
	"strconv"
)

func GetExePathFromPid(pid int) (path string, err error) {
	path, err = os.Readlink("/proc/" + strconv.Itoa(pid) + "/exe")
	return
}
```

The MacOS version though, needs a little bit more.

Given that we’d access `libproc` via C, we can leverage [CGO](https://golang.org/cmd/cgo/).

```go
// +build darwin
package main

// #include <libproc.h>
// #include <stdlib.h>
// #include <errno.h>
import "C"

import (
	"fmt"
	"unsafe"
)

// bufSize references the constant that the implementation
// of proc_pidpath uses under the hood to make sure that
// no overflows happen.
//
// See https://opensource.apple.com/source/xnu/xnu-2782.40.9/libsyscall/wrappers/libproc/libproc.c
const bufSize = C.PROC_PIDPATHINFO_MAXSIZE

func GetExePathFromPid(pid int) (path string, err error) {
        // Allocate in the C heap a string (char* terminated
        // with `/0`) of size `bufSize` and then make sure
        // that we free that memory that gets allocated
        // in C (see the `defer` below).
	buf := C.CString(string(make([]byte, bufSize)))
	defer C.free(unsafe.Pointer(buf))

        // Call the C function `proc_pidpath` from the included
        // header file (libproc.h).
	ret, err := C.proc_pidpath(C.int(pid), unsafe.Pointer(buf), bufSize)
	if ret <= 0 {
		err = fmt.Errorf("failed to retrieve pid path: %v", err)
		return
	}

        // Convert the C string back to a Go string.
	path = C.GoString(buf)
	return
}
```

That done, we can now consume `GetExePathFromPid` in our application.

To see that in place, check out [cirocosta/pidpath](https://github.com/cirocosta/pidpath).

### Closing thoughts

It was interesting to me to check out how different things are in MacOS land.

Although I use a Macbook Pro as a personal computer (and a Mac at work), I’ve not really paid attention to these little details.

Also, `/proc` is just **so** valuable! Definitely worth knowing more about other functionality over there. Make sure you check out [The Linux Programming Interface: A Linux and UNIX System Programming Handbook](https://amzn.to/2QWyXp9).

If you have any questions, or suggestions to improve this blog post, please let me know! I’m [cirowrc](https://twitter.com/cirowrc), and I’d love to chat.

Have a good one!