How to properly read a process's file descriptors?

Viewed 187

I am writing a small program to list the file descriptors of a specific process, and I am having a hard time understanding the results. The process I'm inspecting looks like this:

int main() {
    int fds1[2];
    int fds2[2];
    pipe(fds1);
    pipe(fds2);
    pid_t pid = fork();
    if (pid == 0) {
        dup2(fds1[0], STDIN_FILENO);
        dup2(fds2[1], STDOUT_FILENO);
        close(fds1[0]);
        close(fds1[1]);
        close(fds2[0]);
        close(fds2[1]);
        sleep(2);
        return 0;
    }
    close(fds1[0]);
    close(fds2[1]);
    waitpid(pid, NULL, 0);
    return 0;
}

The fd inspection program code looks like:

let path_str = format!("/proc/{}/fd", self.pid);
let dir = Path::new(&path_str);

let mut fds = Vec::new();
for entry in fs::read_dir(dir).ok()? {
    let path = entry.ok()?.path();
    let filename = path.file_name()?;
    let fd = fname.to_str()?.to_string().parse::<usize>().ok()?
    fds.push(fd);
}

The results of ls -l /proc/{pid}/fd while running the above programs together give me this list:

0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
22 -> /dev/pts/1
30 -> /dev/pts/4

4 -> /home/{user}/.spectrwm.conf
5 -> 'pipe:[168640]'
6 -> 'pipe:[168641]'

I'm confused as to why there are 5 symlinks to /dev/pts/x and why the bottom 3 fds are included in this process's file descriptors, especially my WM's config file. I have a basic understanding of how pipe and fork work, but I can't seem to understand what's going on here.

Any help or insight would be appreciated, thanks!

1 Answers

The first thing I'd note is the file descriptor associated with spectrwm.conf is likely opened somehow by your window manager (perhaps through an LD_PRELOAD or something similar). You can see which file descriptors are opened, and how, by using strace (however, you should use the flag -ff in this case, because you are forking a new process).

You can also use GDB to identify when certain system calls are made (i.e. open) to identify at what point in execution your process opens the file descriptor associated with /home/{user}/.spectrwm.conf (see catchpoints).

As for the other file descriptors, to help you understand what's going on, I ran your code again. Here are the parent process's file descriptors:

user@pop-os:~$ ls -l /proc/39783/fd
total 0
lrwx------ 1 user user 64 Mar 21 14:03 0 -> /dev/pts/0
lrwx------ 1 user user 64 Mar 21 14:03 1 -> /dev/pts/0
lrwx------ 1 user user 64 Mar 21 14:03 2 -> /dev/pts/0
l-wx------ 1 user user 64 Mar 21 14:03 4 -> 'pipe:[544306]'
lr-x------ 1 user user 64 Mar 21 14:03 5 -> 'pipe:[544307]'

The file descriptors I see for the parent process all make sense. Recall that the file descriptors for STDIN, STDOUT, and STDERR are 0, 1, and 2 respectively. All of these file descriptors point to /dev/pts/0 which is a pseudo terminal (the terminal that you are using to run the code with). Next, file descriptors 4 and 5 in the parent process correspond with the reading and writing end of the pipe respectively. Recall that the libc pipe function fills an array of length two for the reading and writing ends (but you close two of the file descriptors associated with your pipes -- the reading end of pipe fds1 and the writing end of pipe fds2 so the open file descriptors make sense in the parent).

And here are the child process's file descriptors:

user@pop-os:~$ ls -l /proc/39784/fd
total 0
lr-x------ 1 user user 64 Mar 21 14:03 0 -> 'pipe:[544306]'
l-wx------ 1 user user 64 Mar 21 14:03 1 -> 'pipe:[544307]'
lrwx------ 1 user user 64 Mar 21 14:03 2 -> /dev/pts/0

These file descriptors also make sense. Recall that dup2 creates a copy of the file descriptor, but assigns the copied file descriptor to a specific number. This allows you to perform inter-process communication (and is also what shells use to implement the pipe operator | -- just fork n processes, create n - 1 pipes, and create the appropriate dup2 calls).

In your child process, we can see that you duplicate the pipe fds to STDIN and STDOUT, which is why you see the reading end of the first pipe associated with STDIN_FILENO and the writing end of the second pipe associated with STDOUT_FILENO. The last file descriptor, STDERR_FILENO in this instance, /dev/pts/0 is still connected to the terminal, as you don't have a dup2 call for that.

I don't know what you're trying to do with your program, but I do have a few questions / notes for you:

  • Why are you using two pipes when you are forking one child process? For two processes to communicate with each other, you only really need one pipe in most cases (recall that one pipe has two file descriptors for reading and writing).
Related