13

I have a program that uses popen() in order to open and read the output from a shell command. The problem is, as far as I can tell, there is no easy way to get the PID of the running process, and hence, you can't kill it if it gets stuck. So the question is, how can you retrieve the PID from a process opened with popen?

Gillespie
  • 5,780
  • 3
  • 32
  • 54
  • 2
    Are you wanting the PID of the `/bin/sh` process that gets spawned or one of the possibly several other processes that are spawned as a result of whatever command line is being passed to `/bin/sh`? In the general case, there isn't necessarily only one PID here, which is probably at least part of the reason it isn't readily available... – twalberg Nov 10 '14 at 20:46
  • Yes, `/bin/sh` should suffice - I really just wanted something--anything--that would allow me to kill processes such as iperf server mode. – Gillespie Nov 10 '14 at 20:49
  • why don't you use system("killall -9 iperf")? and what is the disadvantage on this? Regards, ugesh – Ugesh Reddy Apr 19 '16 at 12:25
  • @UgeshReddy `killall -9 iperf` would kill all iperf processes system-wide, even if it has nothing to do with my particular application. Plus, it's not just iperf I need to kill, but also commands like `ping`, or `curl`. – Gillespie Apr 19 '16 at 18:20

2 Answers2

17

The solution I came up with (and the general consensus) is to create a new popen function that allows me to retrieve the PID. Since I was unable to find a simple example of this on SO, I wanted to post my implementation in the hopes that it helps somebody else. Feedback and alternate solutions are welcome.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <errno.h>
#include <string>
#include <sstream>

using namespace std;

#define READ   0
#define WRITE  1
FILE * popen2(string command, string type, int & pid)
{
    pid_t child_pid;
    int fd[2];
    pipe(fd);

    if((child_pid = fork()) == -1)
    {
        perror("fork");
        exit(1);
    }

    /* child process */
    if (child_pid == 0)
    {
        if (type == "r")
        {
            close(fd[READ]);    //Close the READ end of the pipe since the child's fd is write-only
            dup2(fd[WRITE], 1); //Redirect stdout to pipe
        }
        else
        {
            close(fd[WRITE]);    //Close the WRITE end of the pipe since the child's fd is read-only
            dup2(fd[READ], 0);   //Redirect stdin to pipe
        }

        setpgid(child_pid, child_pid); //Needed so negative PIDs can kill children of /bin/sh
        execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL);
        exit(0);
    }
    else
    {
        if (type == "r")
        {
            close(fd[WRITE]); //Close the WRITE end of the pipe since parent's fd is read-only
        }
        else
        {
            close(fd[READ]); //Close the READ end of the pipe since parent's fd is write-only
        }
    }

    pid = child_pid;

    if (type == "r")
    {
        return fdopen(fd[READ], "r");
    }

    return fdopen(fd[WRITE], "w");
}

int pclose2(FILE * fp, pid_t pid)
{
    int stat;

    fclose(fp);
    while (waitpid(pid, &stat, 0) == -1)
    {
        if (errno != EINTR)
        {
            stat = -1;
            break;
        }
    }

    return stat;
}

int main()
{
    int pid;
    string command = "ping 8.8.8.8"; 
    FILE * fp = popen2(command, "r", pid);
    char command_out[100] = {0};
    stringstream output;

    //Using read() so that I have the option of using select() if I want non-blocking flow
    while (read(fileno(fp), command_out, sizeof(command_out)-1) != 0)
    {
        output << string(command_out);
        kill(-pid, 9);
        memset(&command_out, 0, sizeof(command_out));
    }

    string token;
    while (getline(output, token, '\n'))
        printf("OUT: %s\n", token.c_str());

    pclose2(fp, pid);

    return 0;
}
Gillespie
  • 5,780
  • 3
  • 32
  • 54
  • You could have returned a `FILE*` by using `fdopen` – Basile Starynkevitch Nov 10 '14 at 20:19
  • Great suggestion, since the original `popen` returns `FILE *` as well. I updated my code. – Gillespie Nov 10 '14 at 20:24
  • You also need a `pclose2` doing the `waitpid` and the `fclose` – Basile Starynkevitch Nov 10 '14 at 20:25
  • True - I updated. It makes me wonder how the orignal `pclose()` is able to retrieve the pid of the `FILE *` provided. – Gillespie Nov 10 '14 at 20:39
  • 1
    You can study the implementation of `popen` in some free software libc e.g. [popen.c](http://git.musl-libc.org/cgit/musl/tree/src/stdio/popen.c) from [musl-libc](http://musl-libc.org/) – Basile Starynkevitch Nov 10 '14 at 20:55
  • 1
    Since you were curious, glibc saves the pid in the file struct :) – dzlatkov May 11 '15 at 06:19
  • I checked your test main and it never stops as it kill the sh but not the ping `13446 root 928 S /bin/sh -c ping 8.8.8.8` `13447 root 924 S ping 8.8.8.8` and when kill is called in your test code: `13446 root 0 Z [sh]` `13447 root 924 S ping 8.8.8.8` and while loop never finishes, but it does read ping response all the time – Lonko Oct 13 '15 at 09:06
  • @Lonko What OS are you using? It works for me on both Ubuntu and Yocto Project Embedded Linux. You may have to modify popen2 slightly to suit your needs. – Gillespie Oct 13 '15 at 21:30
  • @RPGillespie busybox debian flavor embedded system... but tried few times and no luck... Child of a popen2 started program still continues to work... – Lonko Oct 14 '15 at 08:08
  • @Lonko Kill the negative of the PID - that will send the kill signal to all children of `/bin/sh` as well. – Gillespie Oct 26 '16 at 19:54
  • @dzlatkov Actually based on `popen.c` it looks like the pids are stored in a static array which is inaccessible outside of `popen.c` – Gillespie Oct 26 '16 at 20:01
  • @Lonko and you'll also probably need to add `setpgid(child_pid, child_pid);` to popen2 (see above), so that the spawned process has its own group id. – Gillespie Oct 26 '16 at 20:27
  • I tried to use the defined functions but found out that the `pid` in the C program was different from the one returned by the terminal command `pgrep` and looking at the output of `ps -aux | grep myNameProc` it seemed the process of the C program was forked once more since `execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL);` is actually creating another process that does `/bin/sh cmd string`. I solved doing the following: `execl(command.c_str(), command.c_str(), (char*)NULL);` – roschach Sep 05 '19 at 13:50
  • @FrancescoBoi Yes, that will work, just keep in mind you won't be able to pass arguments to your program. `/bin/sh -c` lets you pass in a command with arguments. – Gillespie Sep 05 '19 at 13:54
  • 2
    Calling `exit()` after a failed `exec()` call in a child process is a bad idea. That will flush any buffered data copied from the parent process to the parent process's streams and will call all exit handlers that the parent process registered. In a C++ process, it will also cause all static destructors to run. The call to `exit()` should be replaced with `_exit()`. – Andrew Henle Sep 05 '19 at 14:42
  • @AndrewHenle Would the `exit()` due to fork failure have to be replaced as well, or does the fact that the fork failed mean that the parent's memory space was not copied to the child (including atexit handlers)? – Gillespie Sep 05 '19 at 18:05
1

CLARIFICATION

I tried to use the defined functions by @Gillespie's answer but found out that the pid in the C/C++ program was different from the one returned by the terminal command pgrep and looking at the output of ps -aux | grep myNameProc it seemed the process of the C program was forked once more.

I think because execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL); is actually equivalent to /bin/sh cmd string. So basically the child process of your C (or C++) program is creating a new process that does /bin/sh yourRealProcess where yourRealProcess is the one specified in the command string.

I solved doing the following: execl(command.c_str(), command.c_str(), (char*)NULL);. However, as specified by @Gillespie in the previous comments, in this way you will not be able to pass arguments to your process.

C IMPLEMENTATION

According to my needs I readapted @Gillespie's functions to include the above discussed modification and to work in the C programming language:

FILE * custom_popen(char* command, char type, pid_t* pid)
{
    pid_t child_pid;
    int fd[2];
    pipe(fd);

    if((child_pid = fork()) == -1)
    {
        perror("fork");
        exit(1);
    }

    /* child process */
    if (child_pid == 0)
    {
        if (type == 'r')
        {
            close(fd[0]);    //Close the READ end of the pipe since the child's fd is write-only
            dup2(fd[1], 1); //Redirect stdout to pipe
        }
        else
        {
            close(fd[1]);    //Close the WRITE end of the pipe since the child's fd is read-only
            dup2(fd[0], 0);   //Redirect stdin to pipe
        }

        setpgid(child_pid, child_pid); //Needed so negative PIDs can kill children of /bin/sh
        execl(command, command, (char*)NULL);
        exit(0);
    }
    else
    {
        printf("child pid %d\n", child_pid);
        if (type == 'r')
        {
            close(fd[1]); //Close the WRITE end of the pipe since parent's fd is read-only
        }
        else
        {
            close(fd[0]); //Close the READ end of the pipe since parent's fd is write-only
        }
    }

    *pid = child_pid;

    if (type == 'r')
    {
        return fdopen(fd[0], "r");
    }

    return fdopen(fd[1], "w");
}

int custom_pclose(FILE * fp, pid_t pid)
{
    int stat;

    fclose(fp);
    while (waitpid(pid, &stat, 0) == -1)
    {
        if (errno != EINTR)
        {
            stat = -1;
            break;
        }
    }

    return stat;
}
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
roschach
  • 8,390
  • 14
  • 74
  • 124
  • Note that you could very easily add support for arguments, you'd have to break down the strings at spaces or pass another argument to the functions with the argv array. Then you can pass that to `execv()` (so a couple of small changes.) Also, the `exec*()` functions may return if they are not able to start `command`. – Alexis Wilke Dec 23 '20 at 04:35