mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
Introduce the new state 'unauthorized' to the three already existing states disabled, enabled, and unsupported to CPU-MF counters. CPU-MF counters are only available on LPARs. The intent is to differentiate whether a system simply does not support the CPU-MF counters like a z/VM guest or if they are supported like on LPAR but have to be authorized via HMC/SE. Signed-off-by: Finn Callies <fcallies@linux.ibm.com> Reviewed-by: Harald Freudenberger <freude@linux.ibm.com> [hoeppner@linux.ibm.com: Adapt commit message] Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
655 lines
15 KiB
C
655 lines
15 KiB
C
/*
|
|
* cpacfstats - display and maintain CPACF perf counters
|
|
*
|
|
* cpacfstatsd daemon implementation
|
|
*
|
|
* Copyright IBM Corp. 2015, 2022
|
|
*
|
|
* s390-tools is free software; you can redistribute it and/or modify
|
|
* it under the terms of the MIT license. See LICENSE for details.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#define __STDC_FORMAT_MACROS
|
|
#include <inttypes.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <syslog.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
|
|
#include "lib/zt_common.h"
|
|
#include "lib/util_file.h"
|
|
#include "cpacfstats.h"
|
|
|
|
static volatile int stopsig;
|
|
|
|
/*
|
|
* This list contains the counter numbers sorted by instruction
|
|
*/
|
|
static const unsigned int pai_idx[] = {
|
|
// KM
|
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 156, 157, 158,
|
|
159,
|
|
// KMC
|
|
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
|
|
// KMA
|
|
29, 30, 31, 32, 33, 34,
|
|
// KMF
|
|
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
|
|
// KMCTR
|
|
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
|
|
// KMO
|
|
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
|
|
// KIMD
|
|
71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
|
|
// KLMD
|
|
81, 82, 83, 84, 85, 86, 87, 88, 89,
|
|
// KMAC
|
|
90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 160, 161, 162, 163,
|
|
164, 165, 166, 167,
|
|
// PCC
|
|
102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
|
|
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
|
|
// PRNO
|
|
125, 126, 127,
|
|
// KDSA
|
|
128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
|
|
140, 141, 142,
|
|
// PCKMO
|
|
143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 168, 169, 170, 171,
|
|
// Reserved
|
|
154, 155
|
|
};
|
|
|
|
static const char *const name = "cpacfstatsd";
|
|
|
|
static const char *const usage =
|
|
"Usage: %s [OPTIONS]\n"
|
|
"\n"
|
|
"Daemon to provide access to CPACF perf counters\n"
|
|
"Use OPTIONS described below:\n"
|
|
"\n"
|
|
"\t-h, --help Print this help, then exit\n"
|
|
"\t-v, --version Print version information, then exit\n"
|
|
"\t-f, --foreground Run in foreground, do not detach\n";
|
|
|
|
static int daemonized;
|
|
|
|
static int recv_query(int s, enum ctr_e *ctr, enum cmd_e *cmd)
|
|
{
|
|
struct msg m;
|
|
int rc;
|
|
|
|
rc = recv_msg(s, &m, DEFAULT_RECV_TIMEOUT);
|
|
if (rc == 0) {
|
|
if (m.head.m_ver != VERSION) {
|
|
eprint("Received msg with wrong version %d != %d\n",
|
|
m.head.m_ver, VERSION);
|
|
return -1;
|
|
}
|
|
if (m.head.m_type != QUERY) {
|
|
eprint("Received msg with wrong type %d != %d\n",
|
|
m.head.m_type, QUERY);
|
|
return -1;
|
|
}
|
|
*ctr = m.query.m_ctr;
|
|
*cmd = m.query.m_cmd;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int send_answer(int s, int ctr, int state, uint64_t value)
|
|
{
|
|
struct msg m;
|
|
|
|
memset(&m, 0, sizeof(m));
|
|
|
|
m.head.m_ver = VERSION;
|
|
m.head.m_type = ANSWER;
|
|
m.answer.m_ctr = ctr;
|
|
m.answer.m_state = state;
|
|
m.answer.m_value = value;
|
|
|
|
return send_msg(s, &m, DEFAULT_SEND_TIMEOUT);
|
|
}
|
|
|
|
/*
|
|
* Print according to protocol for PAI:
|
|
* - first the state and the number of PAI counters that follow
|
|
* - if state is ENABLED:
|
|
* - for each PAI counter the value with state ENABLED
|
|
* Note that the PAI counters are 0-based, not 1 based as in PoP!
|
|
* Sending ends with the first error.
|
|
*/
|
|
static int do_send_pai(int s, int user, unsigned int *counter)
|
|
{
|
|
int ctr, state, i, rc = 0;
|
|
unsigned int current_ctr;
|
|
uint64_t value;
|
|
|
|
ctr = user ? PAI_USER : PAI_KERNEL;
|
|
|
|
state = perf_ctr_state(ctr);
|
|
if (state != ENABLED)
|
|
return rc;
|
|
for (i = 0; i < MAX_NUM_PAI; ++i) {
|
|
current_ctr = pai_idx[i];
|
|
if ((user && is_user_space(current_ctr) != KERNEL_AND_USER_COUNTER) ||
|
|
(!user && is_user_space(current_ctr) == SUPPRESS_COUNTER) ||
|
|
counter[current_ctr] != 1)
|
|
continue;
|
|
rc = perf_read_pai_ctr(current_ctr, user, &value);
|
|
if (rc != 0) {
|
|
send_answer(s, current_ctr, rc, 0);
|
|
break;
|
|
}
|
|
send_answer(s, current_ctr, state, value);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int do_enable(int s, enum ctr_e ctr, unsigned int *supported_counters)
|
|
{
|
|
uint64_t value = 0;
|
|
int i, rc = 0;
|
|
int state;
|
|
|
|
for (i = 0; i < NUM_COUNTER; i++) {
|
|
if (i == ALL_COUNTER)
|
|
continue;
|
|
if (i == (int) ctr || ctr == ALL_COUNTER) {
|
|
state = perf_ctr_state(i);
|
|
if (state == DISABLED) {
|
|
rc = perf_enable_ctr(i, supported_counters);
|
|
if (rc != 0) {
|
|
send_answer(s, i, rc, 0);
|
|
break;
|
|
}
|
|
state = ENABLED;
|
|
}
|
|
if (state != UNSUPPORTED && state != UNAUTHORIZED) {
|
|
rc = perf_read_ctr(i, &value, supported_counters);
|
|
if (rc != 0) {
|
|
send_answer(s, i, rc, 0);
|
|
break;
|
|
}
|
|
}
|
|
send_answer(s, i, state, value);
|
|
if (i == PAI_USER)
|
|
rc = do_send_pai(s, 1, supported_counters);
|
|
if (i == PAI_KERNEL)
|
|
rc = do_send_pai(s, 0, supported_counters);
|
|
}
|
|
}
|
|
if (rc == 0) {
|
|
rc = perf_read_ctr(HOTPLUG_DETECTED, &value, NULL);
|
|
send_answer(s, HOTPLUG_DETECTED, rc, value);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int do_disable(int s, enum ctr_e ctr, unsigned int *supported_counters)
|
|
{
|
|
int i, rc = 0;
|
|
uint64_t value;
|
|
|
|
for (i = 0; i < NUM_COUNTER; i++) {
|
|
if (i == ALL_COUNTER)
|
|
continue;
|
|
if (i == (int) ctr || ctr == ALL_COUNTER) {
|
|
if (perf_ctr_state(i) == ENABLED) {
|
|
rc = perf_disable_ctr(i, supported_counters);
|
|
if (rc != 0) {
|
|
send_answer(s, i, rc, 0);
|
|
break;
|
|
}
|
|
}
|
|
send_answer(s, i, perf_ctr_state(i), 0);
|
|
}
|
|
}
|
|
if (rc == 0) {
|
|
rc = perf_read_ctr(HOTPLUG_DETECTED, &value, NULL);
|
|
send_answer(s, HOTPLUG_DETECTED, rc, value);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int do_reset(int s, enum ctr_e ctr, unsigned int *supported_counters)
|
|
{
|
|
int i, rc = 0, state;
|
|
uint64_t value;
|
|
|
|
for (i = 0; i < NUM_COUNTER; i++) {
|
|
if (i == ALL_COUNTER)
|
|
continue;
|
|
if (i == (int) ctr || ctr == ALL_COUNTER) {
|
|
state = perf_ctr_state(i);
|
|
if (state == ENABLED) {
|
|
rc = perf_reset_ctr(i, &value, supported_counters);
|
|
if (rc != 0) {
|
|
send_answer(s, i, rc, 0);
|
|
break;
|
|
}
|
|
}
|
|
send_answer(s, i, state, value);
|
|
if (i == PAI_USER)
|
|
rc = do_send_pai(s, 1, supported_counters);
|
|
if (i == PAI_KERNEL)
|
|
rc = do_send_pai(s, 0, supported_counters);
|
|
}
|
|
}
|
|
if (rc == 0) {
|
|
rc = perf_read_ctr(HOTPLUG_DETECTED, &value, NULL);
|
|
send_answer(s, HOTPLUG_DETECTED, rc, value);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int do_print(int s, enum ctr_e ctr, unsigned int *supported_counters)
|
|
{
|
|
int i, rc = 0, state;
|
|
uint64_t value = 0;
|
|
|
|
for (i = 0; i < NUM_COUNTER; i++) {
|
|
if (i == ALL_COUNTER)
|
|
continue;
|
|
if (i == (int) ctr || ctr == ALL_COUNTER) {
|
|
state = perf_ctr_state(i);
|
|
if (state == ENABLED) {
|
|
rc = perf_read_ctr(i, &value, supported_counters);
|
|
if (rc != 0) {
|
|
send_answer(s, i, rc, 0);
|
|
break;
|
|
}
|
|
}
|
|
send_answer(s, i, state, value);
|
|
if (i == PAI_USER)
|
|
rc = do_send_pai(s, 1, supported_counters);
|
|
if (i == PAI_KERNEL)
|
|
rc = do_send_pai(s, 0, supported_counters);
|
|
}
|
|
}
|
|
if (rc == 0) {
|
|
rc = perf_read_ctr(HOTPLUG_DETECTED, &value, NULL);
|
|
send_answer(s, HOTPLUG_DETECTED, rc, value);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int become_daemon(int *startup_pipe)
|
|
{
|
|
int child_initialized = 0, fd;
|
|
int pipefds[2];
|
|
FILE *f;
|
|
|
|
/* syslog */
|
|
openlog("cpacfstatsd", 0, LOG_DAEMON);
|
|
|
|
if (pipe(pipefds) != 0) {
|
|
eprint("pipe() failed, errno=%d [%s]\n", errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* fork and terminate parent
|
|
* Reasons:
|
|
* - opens new command line prompt
|
|
* - the child process is guaranteed not to be the process group leader
|
|
* necessary for setsid.
|
|
*/
|
|
|
|
switch (fork()) {
|
|
case -1: /* error */
|
|
eprint("Fork() failed, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
return -1;
|
|
case 0: /* child */
|
|
break;
|
|
default: /* parent */
|
|
(void)close(pipefds[1]);
|
|
if (read(pipefds[0], &child_initialized, sizeof(child_initialized)) !=
|
|
sizeof(child_initialized)) {
|
|
eprint("Couldn't read from PIPE, errno=%d [%s]\n", errno, strerror(errno));
|
|
(void)close(pipefds[0]);
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
(void)close(pipefds[0]);
|
|
if (!child_initialized)
|
|
_exit(EXIT_FAILURE);
|
|
_exit(0);
|
|
}
|
|
|
|
/* Executed within the child context only */
|
|
(void)close(pipefds[0]);
|
|
*startup_pipe = pipefds[1];
|
|
|
|
if (chdir("/") != 0) {
|
|
eprint("Chdir('/') failed, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* start new session */
|
|
if (setsid() == -1) {
|
|
eprint("Setsid() failed, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* clear umask so that socket has right default permission */
|
|
umask(0007);
|
|
|
|
/* make stdin, stdout and stderr use /dev/null */
|
|
fd = open("/dev/null", O_RDWR);
|
|
if (fd < 0) {
|
|
eprint("Could not open /dev/null, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
dup2(fd, STDIN_FILENO);
|
|
dup2(fd, STDOUT_FILENO);
|
|
dup2(fd, STDERR_FILENO);
|
|
close(fd);
|
|
|
|
daemonized = 1;
|
|
|
|
/* make pid file, fails if the file exists */
|
|
f = fopen(PID_FILE, "w+x");
|
|
if (!f) {
|
|
eprint("Couldn't create pid file '%s', errno=%d [%s]\n",
|
|
PID_FILE, errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
fprintf(f, "%lu", (unsigned long)getpid());
|
|
fflush(f);
|
|
fclose(f);
|
|
chmod(PID_FILE, 0644);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void remove_sock(void)
|
|
{
|
|
remove(SOCKET_FILE);
|
|
}
|
|
|
|
static int check_pidfile(void)
|
|
{
|
|
unsigned long pid;
|
|
FILE *f;
|
|
|
|
f = fopen(PID_FILE, "r");
|
|
if (!f) {
|
|
if (errno == ENOENT) {
|
|
/* pid file does not exit, pid file check is ok */
|
|
return 0;
|
|
}
|
|
/* unknown errno, pid file check is not ok */
|
|
eprint("Unknown error on pid file check '%s', errno=%d [%s]\n",
|
|
PID_FILE, errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* pid file could be opened, scan pid in there */
|
|
if (fscanf(f, "%lu", &pid) != 1) {
|
|
/*
|
|
* invalid, maybe a leftover from a previous run
|
|
* remove and return pid file check ok
|
|
*/
|
|
fclose(f);
|
|
remove(PID_FILE);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
|
|
/* check if this process is still running */
|
|
if (kill(pid, 0) != 0) {
|
|
/*
|
|
* failure, assume this means there is no such pid running
|
|
* remove pid file and return pid file check ok
|
|
*/
|
|
remove(PID_FILE);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* looks like there is another cpacfstatsd running
|
|
* return with pid file check failure
|
|
*/
|
|
eprint("Looks like there is another cpacfstatsd (pid=%lu) running\n",
|
|
pid);
|
|
eprint("Please check and maybe remove stale pid file '%s'\n",
|
|
PID_FILE);
|
|
|
|
return -1;
|
|
}
|
|
|
|
static void remove_pidfile(void)
|
|
{
|
|
remove(PID_FILE);
|
|
}
|
|
|
|
void signalhandler(int sig)
|
|
{
|
|
perf_stop();
|
|
stopsig = sig;
|
|
}
|
|
|
|
int eprint(const char *format, ...)
|
|
{
|
|
char buf[512];
|
|
va_list vargs;
|
|
int i, n;
|
|
|
|
i = snprintf(buf, sizeof(buf), "%s: ", name);
|
|
va_start(vargs, format);
|
|
n = vsnprintf(buf+i, sizeof(buf)-i, format, vargs);
|
|
va_end(vargs);
|
|
|
|
if (n > 0) {
|
|
if (daemonized)
|
|
syslog(LOG_WARNING, "%s", buf);
|
|
else
|
|
fputs(buf, stderr);
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
/*
|
|
* returns -1 on error
|
|
* returns X where X is the found counters in dir
|
|
*
|
|
* the supplied array supported_counters[] is filled in this function with the
|
|
* available PAI counters found in SYSFS_PAI_COUNTER
|
|
*/
|
|
static void supported_functions(unsigned int supported_counters[])
|
|
{
|
|
const char *dir = SYSFS_PAI_COUNTER;
|
|
struct dirent *dp = NULL;
|
|
char filepath[PATH_MAX];
|
|
unsigned int num;
|
|
DIR *dfd = NULL;
|
|
|
|
dfd = opendir(dir);
|
|
if (dfd == NULL)
|
|
return;
|
|
|
|
while ((dp = readdir(dfd)) != NULL) {
|
|
if ((strcmp(dp->d_name, ".") != 0) &&
|
|
(strcmp(dp->d_name, "..") != 0)) {
|
|
snprintf(filepath, sizeof(filepath), "%s%s", dir, dp->d_name);
|
|
if (util_file_read_va(filepath, "event=0x10%x", &num) != 1)
|
|
continue;
|
|
if (num > 0 && num <= MAX_NUM_PAI)
|
|
supported_counters[num - 1] = 1;
|
|
}
|
|
}
|
|
|
|
closedir(dfd);
|
|
return;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
int rc, sfd, foreground = 0, startup_pipe = -1, initialized = 0;
|
|
unsigned int supported_counters[MAX_NUM_PAI] = { 0 };
|
|
struct sigaction act;
|
|
|
|
if (argc > 1) {
|
|
int opt, idx = 0;
|
|
const struct option long_opts[] = {
|
|
{ "help", 0, NULL, 'h' },
|
|
{ "foreground", 0, NULL, 'f' },
|
|
{ "version", 0, NULL, 'v' },
|
|
{ NULL, 0, NULL, 0 } };
|
|
while (1) {
|
|
opt = getopt_long(argc, argv,
|
|
"hfv", long_opts, &idx);
|
|
if (opt == -1)
|
|
break; /* no more arguments */
|
|
switch (opt) {
|
|
case 'h':
|
|
printf(usage, name);
|
|
return 0;
|
|
case 'f':
|
|
foreground = 1;
|
|
break;
|
|
case 'v':
|
|
printf("%s: Linux on System z CPACF Crypto Activity Counters Daemon\n"
|
|
"Version %s\n%s\n",
|
|
name, RELEASE_STRING, COPYRIGHT);
|
|
return 0;
|
|
default:
|
|
printf("%s: Invalid argument, try -h or --help for more information\n",
|
|
name);
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (check_pidfile() != 0) {
|
|
eprint("Stalled pid file or daemon already running, terminating\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (!foreground) {
|
|
if (become_daemon(&startup_pipe) != 0) {
|
|
eprint("Couldn't daemonize\n");
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
supported_functions(supported_counters);
|
|
|
|
if (perf_init(supported_counters) != 0) {
|
|
eprint("Couldn't initialize perf lib\n");
|
|
goto error;
|
|
}
|
|
atexit(perf_close);
|
|
|
|
sfd = open_socket(SERVER);
|
|
if (sfd < 0) {
|
|
eprint("Couldn't initialize server socket\n");
|
|
goto error;
|
|
}
|
|
atexit(remove_sock);
|
|
|
|
memset(&act, 0, sizeof(act));
|
|
act.sa_handler = signalhandler;
|
|
act.sa_flags = 0;
|
|
if (sigaction(SIGINT, &act, 0) != 0) {
|
|
eprint("Couldn't establish signal handler for SIGINT, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
goto error;
|
|
}
|
|
if (sigaction(SIGTERM, &act, 0) != 0) {
|
|
eprint("Couldn't establish signal handler for SIGTERM, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
goto error;
|
|
}
|
|
/* Ignore SIGPIPE such that we see EPIPE as return from write. */
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
eprint("Running\n");
|
|
initialized = 1;
|
|
/* `startup_pipe` has been initialized, so we know we are
|
|
* running in daemon mode. Let's write to the pipe so that the
|
|
* parent knows that the initialization is complete.
|
|
*/
|
|
if (startup_pipe != -1 &&
|
|
write(startup_pipe, &initialized, sizeof(initialized)) != sizeof(initialized))
|
|
goto error;
|
|
(void)close(startup_pipe);
|
|
startup_pipe = -1;
|
|
|
|
while (!stopsig) {
|
|
enum ctr_e ctr;
|
|
enum cmd_e cmd;
|
|
int s;
|
|
|
|
s = accept(sfd, NULL, NULL);
|
|
if (s < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
eprint("Accept() failure, errno=%d [%s]\n",
|
|
errno, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
rc = recv_query(s, &ctr, &cmd);
|
|
if (rc != 0) {
|
|
eprint("Recv_query() failed, ignoring\n");
|
|
close(s);
|
|
continue;
|
|
}
|
|
|
|
if (cmd == ENABLE)
|
|
rc = do_enable(s, ctr, supported_counters);
|
|
else if (cmd == DISABLE)
|
|
rc = do_disable(s, ctr, supported_counters);
|
|
else if (cmd == RESET)
|
|
rc = do_reset(s, ctr, supported_counters);
|
|
else if (cmd == PRINT)
|
|
rc = do_print(s, ctr, supported_counters);
|
|
else {
|
|
eprint("Received unknown command %d, ignoring\n",
|
|
(int) cmd);
|
|
close(s);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (stopsig == SIGTERM)
|
|
eprint("Caught signal SIGTERM, terminating...\n");
|
|
else if (stopsig == SIGINT)
|
|
eprint("Caught signal SIGINT, terminating...\n");
|
|
else
|
|
eprint("Caught signal %d, terminating...\n", stopsig);
|
|
remove_pidfile();
|
|
|
|
return 0;
|
|
|
|
error:
|
|
if (startup_pipe != -1) {
|
|
/* Notify the parent process that there was an error */
|
|
if (write(startup_pipe, &initialized, sizeof(initialized)) != sizeof(initialized))
|
|
eprint("Couldn't write to PIPE, errno=%d [%s]\n", errno, strerror(errno));
|
|
(void)close(startup_pipe);
|
|
}
|
|
|
|
return EXIT_FAILURE;
|
|
}
|