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

问题描述 投票:0回答:1

我正在编写一个 C++ 守护进程,它将在 Linux 嵌入式系统上运行并执行 shell 命令。我试图获取命令的输出(stdout)以及所述 shell 命令的退出代码。我遵循以下关于如何在使用管道时获取 shell 命令的返回值或更确切地说是退出代码的问题: 如何在C++中执行命令并获取命令的返回码stdout和stderr

这会按预期工作,直到我守护进程为止。当我守护进程时,无论命令成功还是失败,

pclose()
始终返回“-1”。这里的预期结果是“0”表示成功执行,整数“>0”表示 shell 命令的错误代码。

命令的输出 - stdout - 可以正确读取和解释,但尝试 pclose 管道以获取退出代码失败,错误代码为“-1”(请参阅 pclose 的手册页以及如何解释返回值

我使用这个问题作为如何创建 Linux 守护进程的参考:“在 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 wizardy 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”。这将禁用所有守护进程相关代码。

我在运行 Arch Linux x64 和 g++ (GCC) 版本 11.2.0 的主开发机器上使用

g++ main.cpp -o pcloseTest -Wall -Werror
编译了此代码。然后我使用
./pcloseTest
直接从终端运行程序,并使用 journalctl (
journalctl -f
) 监视日志记录的输出。

我还使用我的arm交叉编译工具链编译了相同的代码,以测试该错误是否可以在另一个体系结构上重现。我的arm交叉编译工具链的GCC版本是7.3.0,运行程序的操作系统是

Yocto Linux
for 32Bit ARM的定制版本。

两者的行为相同并产生相同的输出 - 在非守护程序版本中产生预期输出,而在守护程序程序中产生意外输出“-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
执行管道会破坏某些内容。但我不知道哪个信号可能无法正确处理或可能丢失。

作为参考,我还尝试使用

pclose()
函数
 实现我自己的 
waitpid() 版本,如手册页 RATIONALE 所建议的那样,但我放弃了它,因为它的行为与
pclose()
类似,并且我假设存在底层问题是一样的。

提前非常感谢您提供的任何帮助,如果需要更多信息或更多详细信息,请告诉我。

c++ linux daemon pclose
1个回答
2
投票

我的程序的问题是,信号

SIGCHLD
未被我的程序处理。

评估

errno
并检查
pclose()
给出的错误后,我找到了解决方案。
errno
返回
ECHILD
,根据 errno 手册页,这意味着“没有子进程”。我通过向我的
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 occurred */
      syslog(LOG_ERR, "rc is negativ - %s", strerror(errno));
    }
  };
...

经过更多研究并查看 信号手册页,我发现我的程序忽略了之前提到的

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 wizardy 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”。

© www.soinside.com 2019 - 2024. All rights reserved.