如何在守护进程中使用popen()和pclose()执行通过管道执行的shell命令并获取正确的退出码?

3
我正在编写一个C++守护进程,将在Linux嵌入式系统上运行并执行一个shell命令。我想获取该shell命令的输出(stdout)以及退出代码。我按照以下SO问题中的方法获取管道中shell命令的返回值或退出代码: How to execute a command and get return code stdout and stderr of command in C++ 这很好用,但当我将进程变成守护进程时,无论命令成功还是失败,pclose()始终返回“-1”。预期结果应该是“0”代表成功执行,而整数“>0”代表shell命令的错误代码。 命令的输出 - stdout - 可以正确地读取和解释,但尝试关闭管道以获取退出代码会失败,并显示错误代码“-1”(请参阅 pclose 的手册以了解如何解释返回值)。
我参考了这个SO问题,学习如何创建一个Linux守护进程:"Creating a daemon in Linux",以及一本名为"Linux-UNIX-Programmierung"的德语书籍,学习如何创建守护进程。到目前为止,这些方法都能正常工作,但会破坏pclose()函数的行为。
以下是重现此问题的示例:
#include <array>  // For std::array
#include <memory> // For std::unique_ptr
#include <string>
#include <sys/stat.h>
#include <sys/syslog.h> // For all syslog things
#include <sys/wait.h>
#include <unistd.h>

// Just a helper method for signal handling
void signalHandler(int sig) {
  switch (sig) {
  case SIGINT:
  case SIGTERM:
    break;
  }
}

int main(int argc, char *argv[]) {
  /* Open log file to be able to use syslog */
  setlogmask(LOG_UPTO(LOG_DEBUG));
  openlog("MyDemoProg", LOG_PID, LOG_DAEMON);

#if 1 // Set to 0 to disable the daemonizing
  /* Fork off the parent process */
  pid_t pid = fork();

  /* An error occurred */
  if (pid < 0)
    exit(EXIT_FAILURE);

  /* Success: Let the parent terminate */
  if (pid > 0)
    exit(EXIT_SUCCESS);

  /* On success: The child process becomes session leader */
  if (setsid() < 0)
    exit(EXIT_FAILURE);

  /* Catch, ignore and handle signals */
  signal(SIGCHLD, SIG_IGN);

  /* Set up a signal handler */
  struct sigaction newSigAction;
  newSigAction.sa_handler = signalHandler;
  sigemptyset(&newSigAction.sa_mask);
  newSigAction.sa_flags = 0;

  /* Signals to handle */
  sigaction(SIGHUP, &newSigAction, NULL);  /* catch hangup signal */
  sigaction(SIGTERM, &newSigAction, NULL); /* catch term signal */
  sigaction(SIGINT, &newSigAction, NULL);  /* catch interrupt signal */

  /* Fork off for the second time*/
  pid = fork();

  /* An error occurred */
  if (pid < 0)
    exit(EXIT_FAILURE);

  /* Success: Let the parent terminate */
  if (pid > 0)
    exit(EXIT_SUCCESS);

  /* Set new file permissions */
  umask(0);

  /* Change the working directory to the root directory */
  /* or another appropriate directory */
  chdir("/");

  /* Close all open file descriptors */
  int x;
  for (x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {
    close(x);
  }
#endif // end of daemonizing

  std::string command = "ls /var/bla/; sleep 2; echo test";
  syslog(LOG_DEBUG, "Command is: %s", command.c_str());

  int rc = -999; // the return code variable, set to some value to see if it's
                 // truly changed
  std::array<char, 16> buffer;
  std::string commandResult;

  // A wrapper function to be able to get the return code while still using the
  // automatic close function wizzardy of unique_ptr
  auto pclose_wrapper = [&rc](FILE *cmd) { rc = pclose(cmd); };
  {
    const std::unique_ptr<FILE, decltype(pclose_wrapper)> pipe(
        popen(command.c_str(), "r"), pclose_wrapper);

    if (!pipe) {
      syslog(LOG_ERR, "Could not open pipe! Exiting");
      return EXIT_FAILURE;
    }

    /* Read in the pipe and save the content to a buffer */
    while (::fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
      commandResult += buffer.data();
    }
  }
  syslog(LOG_DEBUG, "Command result is: %s", commandResult.c_str());
  syslog(LOG_DEBUG, "Return code is: %d", rc);

  return EXIT_SUCCESS;
}

将守护进程版本切换为常规版本,请将第22行的#if标志更改为“0”。这将禁用所有与守护进程相关的代码。
我在我的主开发机上使用g++ main.cpp -o pcloseTest -Wall -Werror编译了这段代码,该机器运行Arch Linux x64,使用的是g++(GCC)版本11.2.0。然后,我直接从终端运行程序,并使用journalctl(journalctl -f)监视日志输出。
我还使用我的arm-cross-compile工具链编译了相同的代码,以测试错误是否可以在另一种架构上重现。我的arm-cross-compile工具链的GCC版本为7.3.0,运行程序的操作系统是32位ARM的定制版本Yocto Linux。
两者的行为相同,并产生相同的输出-在非守护进程版本中产生预期的输出,在守护进程程序中产生意外的“-1”输出。
以下是启用守护进程的嵌入式“Yocto Linux”系统的系统日志文件输出(/var/log/message)。
May  9 15:43:08 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10255]: Command is: ls /var/bla/; sleep 2; echo test
May  9 15:43:10 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10255]: Command result is: test
May  9 15:43:10 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10255]: Return code is: -1

这是禁用后的效果:
May  9 15:43:26 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10262]: Command is: ls /var/bla/; sleep 2; echo test
May  9 15:43:28 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10262]: Command result is: test
May  9 15:43:28 MY-EMBEDDED-SYSTEM daemon.debug MyDemoProg[10262]: Return code is: 0

供比较,这是我的 x64 Linux(systemd 和 journalctl)守护进程代码:
May 09 16:06:02 aero15 MyDemoProg[76950]: Command is: ls /var/bla/; sleep 2; echo test
May 09 16:06:04 aero15 MyDemoProg[76950]: Command result is: test
May 09 16:06:04 aero15 MyDemoProg[76950]: Return code is: -1

"和普通的代码:"
May 09 16:05:34 aero15 MyDemoProg[76805]: Command is: ls /var/bla/; sleep 2; echo test
May 09 16:05:36 aero15 MyDemoProg[76805]: Command result is: test
May 09 16:05:36 aero15 MyDemoProg[76805]: Return code is: 0

如您所见,在四种情况下,命令结果都可以正确地读取为“test”,而在非守护程序中,pclose()的返回代码为“0”,如预期一样(表示命令成功执行),而在守护版本中为“-1”,表明pclose()发生了一些意外情况。
我试图研究一下是否有关于守护进程化进程与popen()pclose()之间的某些已知奇怪行为,但我没有找到任何具体信息。 我自己的怀疑是,可能守护进程未处理某个信号,该信号对于pclose()的工作是必需的,或者分叉两次将进程守护化,然后再使用popen执行管道时分叉会导致某些问题。 但我不确定哪个信号可能未被正确处理或可能丢失。
供参考,我也尝试使用waitpid()函数实现了自己的pclose()版本如man页面RATIONALE所建议的,但是我放弃了,因为它的行为与pclose()类似,而且我认为潜在的问题是相同的。
非常感谢您提前的任何帮助,并请让我知道是否需要进一步的信息或更多的细节。

2
pclose返回-1时,errno的值是多少?例如,perror(“pclose”)会输出什么信息? - Some programmer dude
你好@Someprogrammerdude。再次感谢您的建议!使用类似于auto pclose_wrapper = [&rc](FILE *cmd) { rc = pclose(cmd); if (rc < 0) { syslog(LOG_ERR, "rc is negativ - %s", strerror(errno)); } };将errno记录到syslog中似乎可以解决问题,并返回“无子进程”或根据errno man页面的说法是“ECHILD”。我认为通过将“SIGCHLD”添加到程序处理的信号中,我解决了这个问题。非常感谢!我应该编辑原始消息以显示已解决吗? - N0x
如果您认为该答案可能对其他遇到类似问题的人有用,请发布一个实际的答案。 - Some programmer dude
1个回答

2
我的程序出现问题的原因是,我的程序没有处理信号SIGCHLD
在评估errno并检查pclose()给出的错误后,我找到了解决方案。errno返回ECHILD,根据errno man page,这意味着“没有子进程”。通过为我的pclose_wrapper lambda函数添加更多的处理,我捕获了这个错误:
...
  auto pclose_wrapper = [&rc](FILE *cmd) {
    rc = pclose(cmd);
    if (rc < 0) {
      /* Log the error if pclose returns "-1" signaling an error occured */
      syslog(LOG_ERR, "rc is negativ - %s", strerror(errno));
    }
  };
...

经过更多的研究和查看signal的man页面,我发现我的程序像之前提到的那样忽略了SIGCHLD信号。这个信号通知一个进程子进程已停止或终止。
解决方法是添加sigaction(SIGCHLD, &newSigAction, NULL);并丢弃signal(SIGCHLD, SIG_IGN);这一行,它明确地忽略了该信号。

下面是可工作的代码:

#include <array> // For std::array
#include <cstring>
#include <memory> // For std::unique_ptr
#include <string>
#include <sys/stat.h>
#include <sys/syslog.h> // For all syslog things
#include <sys/wait.h>
#include <unistd.h>

void signalHandler(int sig) {
  switch (sig) {
  case SIGINT:
  case SIGTERM:
    break;
  case SIGCHLD:
    /* Some child related action */
    break;
  }
}

int main(int argc, char *argv[]) {
  /* Open log file to be able to use syslog */
  setlogmask(LOG_UPTO(LOG_DEBUG));
  openlog("MyDemoProg", LOG_PID, LOG_DAEMON);

#if 1 // Set to 0 to disable the daemonizing

  pid_t pid = fork();

  if (pid < 0)
    exit(EXIT_FAILURE);

  if (pid > 0)
    exit(EXIT_SUCCESS);

  if (setsid() < 0)
    exit(EXIT_FAILURE);

  struct sigaction newSigAction;
  newSigAction.sa_handler = signalHandler;
  sigemptyset(&newSigAction.sa_mask);
  newSigAction.sa_flags = 0;

  sigaction(SIGHUP, &newSigAction, NULL);  /* catch hangup signal */
  sigaction(SIGTERM, &newSigAction, NULL); /* catch term signal */
  sigaction(SIGINT, &newSigAction, NULL);  /* catch interrupt signal */
  sigaction(SIGCHLD, &newSigAction,
            NULL); /* catch child stopped or terminated signal */

  pid = fork();
  if (pid < 0)
    exit(EXIT_FAILURE);

  if (pid > 0)
    exit(EXIT_SUCCESS);

  umask(0);
  chdir("/");
  for (int x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {
    close(x);
  }
  syslog(LOG_DEBUG, "Daemonizing is enabled");
#else
  syslog(LOG_DEBUG, "Daemonizing is disabled");
#endif

  std::string command = "ls /var/bla/; sleep 2; echo test";
  syslog(LOG_DEBUG, "Command is: %s", command.c_str());

  int rc = -999;
  std::array<char, 16> buffer;
  std::string commandResult;

  // A wrapper function to be able to get the return code while still using the
  // automatic close function wizzardy of unique_ptr
  auto pclose_wrapper = [&rc](FILE *cmd) {
    rc = pclose(cmd);
    if (rc < 0) {
      /* Log the error if pclose returns "-1" signaling an error occured */
      syslog(LOG_ERR, "rc is negativ - %s", strerror(errno));
    }
  };
  {
    const std::unique_ptr<FILE, decltype(pclose_wrapper)> pipe(
        popen(command.c_str(), "r"), pclose_wrapper);

    if (!pipe) {
      syslog(LOG_ERR, "Could not open pipe! Exiting");
      return EXIT_FAILURE;
    }

    /* Read in the pipe and save the content to a buffer */
    while (::fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
      commandResult += buffer.data();
    }
  }
  syslog(LOG_DEBUG, "Command result is: %s", commandResult.c_str());
  syslog(LOG_DEBUG, "Return code is: %d", rc);

  return EXIT_SUCCESS;
}

以下是非守护进程和守护进程版本的输出:
(我添加了一个系统日志消息来指示是否启用了守护进程代码。)

May 10 09:24:30 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10872]: Daemonizing is disabled
May 10 09:24:30 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10872]: Command is: ls /var/bla/; sleep 2; echo test
May 10 09:24:32 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10872]: Command result is: test
May 10 09:24:32 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10872]: Return code is: 0
---
May 10 09:24:49 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10881]: Daemonizing is enabled
May 10 09:24:49 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10881]: Command is: ls /var/bla/; sleep 2; echo test
May 10 09:24:51 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10881]: Command result is: test
May 10 09:24:51 MY-EMBEDDED-DEVICE daemon.debug MyDemoProg[10881]: Return code is: 0

现在两个版本都返回了预期的“0”返回代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接