An eBPF-based profiler for process-lifecycle events.
zig ebpf profilers
proctrace dev-docs syscalls.md
2.9 kB
Markdown
at main

System calls #

In order to track process lifecycle we need to know:

  • When a new process is created
  • When an existing process starts executing a new program
  • When an existing process terminates

If you're not familiar with how that works, here's the short version:

  • The fork system call (syscall) creates a copy of the running program.
  • The exec syscall tells the current process to start executing a different program.
  • The exit syscall tells the current process to terminate.

It turns out that it's more complicated than that, so let's get into the details.

Fork #

Under the hood a process and a thread are roughly the same thing: a "task". A process is modeled as a group of tasks. Confusingly, each task has a "PID", and a process has a "thread group ID" (tgid).

It follows that since there's a more generic primitive underlying both threads and processes, there's a more generic syscall for creating them. Actually there's two:

  • clone
  • clone3

The fork system call is actually implemented via clone (kernel_clone in the kernel, to be precise).

In playing around with a previous iteration of proctrace that was based on bpftrace I found that instrumenting the fork syscall didn't collect all events that I'd expect, but instrumenting both clone and clone3 did. This is why we inspect these two syscalls instead of fork.

However, since the clone syscalls are also used to create threads, if you collect all clone syscalls you'll end up collecting events that are really just new threads being created. Luckily, both clone syscalls take a flags argument in one way or another, and one of these flags is CLONE_THREAD, which indicates whether the call is creating a new thread or not.

In the program that calls fork the return value is the PID (tgid) of the newly created process. In the newly created process, execution proceeds from where fork was called, and the return value of fork is 0. You can use this return value to track the process tree.

Exec #

There's actually a number of exec-like syscalls or libc functions, but in terms of tracepoints we instrument both execve and execveat, the difference being that execve takes a path to the program to execute, and execveat takes a file descriptor and a path relative to that file descriptor that points at the program to execute.

We care about the filename and the arguments to the program.

Exit #

Again, there's actually more than one exit-like syscall:

  • exit
  • exit_group

Calling exit will terminate the thread in which it's called. If that thread is the main thread, it will also terminate the process. Calling exit_group will terminate the process regardless of which thread calls it.

For this reason we record all exit_group calls, and only the exit calls that come from the main thread.