pidfd_open

pidfd_open

pkill 打つたびに、「これpid再利用されたら地球が爆発するよな…」とか思っていたのだけど、今はpidfd_openがあるので助かった。

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>

static int  pidfd_send_signal(int pidfd, int  sig, siginfo_t *info,
                              unsigned int flags) {
    return syscall(__NR_pidfd_send_signal, pidfd, sig, info,
                   flags);
}


static int
pidfd_open(pid_t pid, unsigned int flags)
{
  return syscall(__NR_pidfd_open, pid, flags);
}

int
main(int argc, char **argv)
{
    int child;
    int use_pidfd = 1;
    if (argc > 1) {
        use_pidfd = atoi(argv[1]);
    }

    if (use_pidfd) {
        puts("use pidfd_send_signal");
    } else {
        puts("use kill");
    }

    if ((child=fork()) == 0) {
        exit(0);
    } else {
        printf("child = %d\n", child);
        int status;
        int pidfd = pidfd_open(child, 0);
        int r = pidfd_send_signal(pidfd, SIGUSR1, NULL, 0);
        if (r < 0) {
            /* プロセスが生きてるあいだはpidfd_send_signalが届く */
            perror("pidfd_send_signal");
            return 1;
        }
        char path[1024];
        sprintf(path, "/proc/%d", child);
        int pidfd2 = open(path, O_RDONLY);
        r = pidfd_send_signal(pidfd2, SIGUSR1, NULL, 0);
        if (r < 0) {
            /* /proc/PID も pidfd として使える */
            perror("pidfd_send_signal");
            return 1;
        }
        int fd2 = openat(pidfd2, "cmdline", O_RDONLY);
        if (fd2 < 0) {
            /* プロセスが生きてる間は openat で cmdline が開ける */
            perror("cmdline");
            return 1;
        }
        close(fd2);

        wait(&status);

        for (int i=0; i<1000000; i++) {
            int child2;
            if ((child2 = fork()) == 0) {
                int self = getpid();
                if (self == child) {
                    sleep(10);
                }
                exit(0);
            } else {
                if (child2 == child) {
                    if (use_pidfd) {
                        int r = pidfd_send_signal(pidfd, SIGUSR1, NULL, 0);
                        perror("send signal");

                        int fd = openat(pidfd2, "cmdline", O_RDONLY);
                        perror("openat");
                        close(fd);

                        exit(0);
                    } else {
                        int r = kill(child, SIGUSR1);
                        perror("kill");

                        sprintf(path, "/proc/%d/cmdline", child);

                        int fd = open(path, O_RDONLY);
                        perror("open");
                        close(fd);
                        exit(0);
                    }
                }

                wait(&status);
            }
        }
    }
}

この実験プログラムはpid一周するのを期待しているけど、今のLinuxデフォルト値だとpid一周するまで時間かかるので、pid最大値下げて実験するのをおすすめする。

 $ echo 65536 > /proc/sys/kernel/pid_max

最初に子プロセスを立ち上げて、即終了する。このpidに対して、シグナルを送った場合、どのプロセスにもシグナルが届いてほしくない。

しかし、シグナル送るまでに大量のforkを実行すると、pidが一周して、既存の kill(2) だと、新しくできたプロセスにシグナルを送ってしまう。

そこで、pidfdを使う。

pidfd_open で開いた FD は、そのプロセスがいなくなるとどのプロセスも指さなくなる。pid が一周したあと、pidfd に対してシグナルを送っても、意図しないプロセスには届かない。

$ ./a.out 0  # killを使う
use kill
child = 7416
kill: Success # PID一周するとシグナルが送れてしまう (Successと出てるが、ここは失敗してほしい)
open: Success # /proc/PID/cmdline も開ける


$ ./a.out 1  # pidfd_send_signalを使う
use pidfd_send_signal
child = 7418
send signal: No such process # PID 一周するとシグナル送れない
openat: No such process # /proc/PID/cmdine も開けない


まあでも今のpkillはstraceで見たけどそういう実装にはなってないっぽい?

/proc/PID を開いても、pidfd は取得できる。これで、安全なpkillができるはず。