使用write或异步安全函数从信号处理程序打印int

23
我希望在信号处理函数中使用write(或任何异步安全函数)将数字打印到日志或终端,而不是使用缓冲I/O。是否有简单且推荐的方法实现?例如,在下面的代码中,我想要使用write(或任何异步安全函数),而不是printf
void signal_handler(int sig)
{
  pid_t pid;
  int stat;
  int old_errno = errno;

  while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    printf("child %d terminated\n", pid);

  errno = old_errno;
  return;
}

输出字符串很容易。我可以使用以下代码(不打印pid)代替上面的printf

write(STDOUT_FILENO, "child terminated", 16);

2
如果你的程序稍微有点复杂,那么设置一个 signalfd 并将其插入到事件循环中可能会更简单。然后你可以对信号做出任何你想要的响应。 - Kerrek SB
2
"signalfd" 不具备可移植性,它只能在 Linux 上使用。然而,在 Unix 的整个历史中,都有一种可移植的同类解决方案:自管道技巧 - R.. GitHub STOP HELPING ICE
4个回答

11
如果您坚持要在信号处理程序中执行打印操作,基本上有两种选择:
  1. 除了专门为处理信号创建的线程外,阻止该信号。这个特殊的线程可以简单地执行for (;;) pause();,由于pause是异步信号安全的,因此信号处理程序允许使用任何函数;它不仅限于异步信号安全函数。另一方面,它必须以线程安全的方式访问共享资源,因为现在你正在处理线程。

  2. 编写自己的代码将整数转换为十进制字符串。这只是一个简单的循环,使用%10/10来剥离最后一位数字并将它们存储到短数组中。

然而,我强烈建议将此操作从信号处理程序中移出,使用self-pipe trick或类似方法。

是的,谢谢。我已经开始挖掘自管道技巧。那似乎更好。 - user648129
2
只有第二个选项适用于像SIGSEGV、SIGILL等信号。在这些情况下,自管道技巧也不起作用。 - Paul Coccoli

4

实现自己的异步信号安全的snprintf("%d并使用write

并不像我想象的那么糟糕,如何在C中将int转换为字符串?有几种实现方法。

下面的POSIX程序将到stdout计数它收到SIGINT的次数,您可以使用Ctrl + C触发它。

您可以使用Ctrl + \(SIGQUIT)退出程序。

main.c:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/* Calculate the minimal buffer size for a given type.
 *
 * Here we overestimate and reserve 8 chars per byte.
 *
 * With this size we could even print a binary string.
 *
 * - +1 for NULL terminator
 * - +1 for '-' sign
 *
 * A tight limit for base 10 can be found at:
 * https://dev59.com/R2sy5IYBdhLWcg3w3h6I/32871108#32871108
 *
 * TODO: get tight limits for all bases, possibly by looking into
 * glibc's atoi: https://dev59.com/RHVC5IYBdhLWcg3wxEJ1#52127877
 */
#define ITOA_SAFE_STRLEN(type) sizeof(type) * CHAR_BIT + 2

/* async-signal-safe implementation of integer to string conversion.
 *
 * Null terminates the output string.
 *
 * The input buffer size must be large enough to contain the output,
 * the caller must calculate it properly.
 *
 * @param[out] value  Input integer value to convert.
 * @param[out] result Buffer to output to.
 * @param[in]  base   Base to convert to.
 * @return     Pointer to the end of the written string.
 */
char *itoa_safe(intmax_t value, char *result, int base) {
    intmax_t tmp_value;
    char *ptr, *ptr2, tmp_char;
    if (base < 2 || base > 36) {
        return NULL;
    }

    ptr = result;
    do {
        tmp_value = value;
        value /= base;
        *ptr++ = "ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[35 + (tmp_value - value * base)];
    } while (value);
    if (tmp_value < 0)
        *ptr++ = '-';
    ptr2 = result;
    result = ptr;
    *ptr-- = '\0';
    while (ptr2 < ptr) {
        tmp_char = *ptr;
        *ptr--= *ptr2;
        *ptr2++ = tmp_char;
    }
    return result;
}

volatile sig_atomic_t global = 0;

void signal_handler(int sig) {
    char buf[ITOA_SAFE_STRLEN(sig_atomic_t)];
    enum { base = 10 };
    char *end;
    end = itoa_safe(global, buf, base);
    *end = '\n';
    write(STDOUT_FILENO, buf, end - buf + 1);
    global += 1;
    signal(sig, signal_handler);
}

int main(int argc, char **argv) {
    /* Unit test itoa_safe. */
    {
        typedef struct {
            intmax_t n;
            int base;
            char out[1024];
        } InOut;
        char result[1024];
        size_t i;
        InOut io;
        InOut ios[] = {
            /* Base 10. */
            {0, 10, "0"},
            {1, 10, "1"},
            {9, 10, "9"},
            {10, 10, "10"},
            {100, 10, "100"},
            {-1, 10, "-1"},
            {-9, 10, "-9"},
            {-10, 10, "-10"},
            {-100, 10, "-100"},

            /* Base 2. */
            {0, 2, "0"},
            {1, 2, "1"},
            {10, 2, "1010"},
            {100, 2, "1100100"},
            {-1, 2, "-1"},
            {-100, 2, "-1100100"},

            /* Base 35. */
            {0, 35, "0"},
            {1, 35, "1"},
            {34, 35, "Y"},
            {35, 35, "10"},
            {100, 35, "2U"},
            {-1, 35, "-1"},
            {-34, 35, "-Y"},
            {-35, 35, "-10"},
            {-100, 35, "-2U"},
        };
        for (i = 0; i < sizeof(ios)/sizeof(ios[0]); ++i) {
            io = ios[i];
            itoa_safe(io.n, result, io.base);
            if (strcmp(result, io.out)) {
                printf("%ju %d %s\n", io.n, io.base, io.out);
                assert(0);
            }
        }
    }

    /* Handle the signals. */
    if (argc > 1 && !strcmp(argv[1], "1")) {
        signal(SIGINT, signal_handler);
        while(1);
    }

    return EXIT_SUCCESS;
}

编译并运行:

gcc -std=c99 -Wall -Wextra -o main main.c
./main 1

按下15次Ctrl + C后,终端会显示:

^C0
^C1
^C2
^C3
^C4
^C5
^C6
^C7
^C8
^C9
^C10
^C11
^C12
^C13
^C14

这里有一个相关的程序,可以创建更复杂的格式字符串:如何避免在信号处理程序中使用printf? 在Ubuntu 18.04上测试过。GitHub源

该程序不像宣传的那样工作。程序返回时无法按下control-c键。 - Deanie
@Deanie感谢反馈。我在Ubuntu 18.04上重新进行了测试,它再次成功运行。请提供你的发行版 / GCC版本。 - Ciro Santilli OurBigBook.com
gcc version 8.2.1 20181127 (GCC)Linux unknown 4.19.8-arch1-1-ARCH #1 SMP PREEMPT Sat Dec 8 13:49:11 UTC 2018 x86_64 GNU/Linux。当我运行它时,命令提示符会迅速返回... - Deanie
@Deanie 谢谢。你用的是哪个发行版?我这里是 Ubuntu 18.04。 - Ciro Santilli OurBigBook.com
4.19.8-arch1-1-ARCH 是 Arch Linux 4.19.8。 - Deanie

0
你可以使用字符串处理函数(例如 strcat)构建字符串,然后一次性将其write到所需的文件描述符(例如 STDERR_FILENO 代表标准错误)。
我使用以下函数将整数(最多64位宽,有符号或无符号)转换为字符串(C99),这些函数支持最小的格式化标志和常见的数字进制(8、10和16)。
#include <stdbool.h>
#include <inttypes.h>

#define STRIMAX_LEN 21 // = ceil(log10(INTMAX_MAX)) + 2
#define STRUMAX_LEN 25 // = ceil(log8(UINTMAX_MAX)) + 3

static int strimax(intmax_t x,
                   char buf[static STRIMAX_LEN],
                   const char mode[restrict static 1]) {
    /* safe absolute value */
    uintmax_t ux = (x == INTMAX_MIN) ? (uintmax_t)INTMAX_MAX + 1
                                     : (uintmax_t)imaxabs(x);

    /* parse mode */
    bool zero_pad = false;
    bool space_sign = false;
    bool force_sign = false;
    for(const char *c = mode; '\0' != *c; ++c)
        switch(*c) {
            case '0': zero_pad = true; break;
            case '+': force_sign = true; break;
            case ' ': space_sign = true; break;
            case 'd': break; // decimal (always)
        }

    int n = 0;
    char sign = (x < 0) ? '-' : (force_sign ? '+' : ' ');
    buf[STRIMAX_LEN - ++n] = '\0'; // NUL-terminate
    do { buf[STRIMAX_LEN - ++n] = '0' + ux % 10; } while(ux /= 10);
    if(zero_pad) while(n < STRIMAX_LEN - 1) buf[STRIMAX_LEN - ++n] = '0';
    if(x < 0 || force_sign || space_sign) buf[STRIMAX_LEN - ++n] = sign;

    return STRIMAX_LEN - n;
}

static int strumax(uintmax_t ux,
                   char buf[static STRUMAX_LEN],
                   const char mode[restrict static 1]) {
    static const char lbytes[] = "0123456789abcdefx";
    static const char ubytes[] = "0123456789ABCDEFX";

    /* parse mode */
    int base = 10; // default is decimal
    int izero = 4;
    bool zero_pad = false;
    bool alternate = false;
    const char *bytes = lbytes;
    for(const char *c = mode; '\0' != *c; ++c)
        switch(*c) {
            case '#': alternate = true; if(base == 8) izero = 1; break;
            case '0': zero_pad = true; break;
            case 'd': base = 10; izero = 4; break;
            case 'o': base = 8; izero = (alternate ? 1 : 2); break;
            case 'x': base = 16; izero = 8; break;
            case 'X': base = 16; izero = 8; bytes = ubytes; break;
        }

    int n = 0;
    buf[STRUMAX_LEN - ++n] = '\0'; // NUL-terminate
    do { buf[STRUMAX_LEN - ++n] = bytes[ux % base]; } while(ux /= base);
    if(zero_pad) while(n < STRUMAX_LEN - izero) buf[STRUMAX_LEN - ++n] = '0';
    if(alternate && base == 16) {
        buf[STRUMAX_LEN - ++n] = bytes[base];
        buf[STRUMAX_LEN - ++n] = '0';
    } else if(alternate && base == 8 && '0' != buf[STRUMAX_LEN - n])
        buf[STRUMAX_LEN - ++n] = '0';

    return STRUMAX_LEN - n;
}

它们可以这样使用:

#include <unistd.h>

int main (void) {
    char buf[STRIMAX_LEN]; int buf_off;
    buf_off = strimax(12345,buf,"+");
    write(STDERR_FILENO,buf + buf_off,STRIMAX_LEN - buf_off);
}

输出结果为:

+12345

-1

如果你坚持在信号处理程序中使用xprintf(),你总是可以自己编写一个不依赖于缓冲I/O的版本:

#include <stdarg.h> /* vsnprintf() */

void myprintf(FILE *fp, char *fmt, ...)
{
char buff[512];
int rc,fd;
va_list argh;
va_start (argh, fmt);

rc = vsnprintf(buff, sizeof buff, fmt, argh);
if (rc < 0  || rc >= sizeof buff) {
        rc = sprintf(buff, "Argh!: %d:\n", rc);
        }

if (!fp) fp = stderr;
fd = fileno(fp);
if (fd < 0) return;
if (rc > 0)  write(fd, buff, rc);
return;
}

4
很不幸,vsnprintf也不需要支持异步信号安全。你可能认为这只是一个理论问题,在实际的实现中并不重要,但是glibc的实现明显是异步信号安全的,因为它使用了malloc - R.. GitHub STOP HELPING ICE
2
另外,fileno不是异步信号安全的。它需要在文件上获取锁,并且如果被中断的代码正在锁定/解锁文件,则可能会出现严重问题。幸运的是,您对fileno的使用是错误和不必要的。stderr是文件描述符号码2。如果您不想使用“魔法数字”,则可以使用STDERR_FILENO - R.. GitHub STOP HELPING ICE
1
啊,那我就改口了。也许你可以自己创建一个(异步安全的...)snprintf()函数。(上次我查看Apache的实现是50KLOC...)顺便说一句:我知道STDERR_FILENO;只是我照着样子写,我想)基本上,只剩下self-pipe技巧了。 - wildplasser
顺便问一下:是否有办法将整个 va_arg 的 内容 推送到管道中(最多 PIPE_BUFF)?这需要进行深拷贝,并且在处理时,“参数”可能已经失效,我意识到这一点。 - wildplasser
根据此链接:http://www-it.desy.de/cgi-bin/man-cgi?vsnprintf+3,vsnprintf()等函数是异步安全的。(尽管代码示例很糟糕)如果感到困惑,我将检查源代码... - wildplasser
显示剩余5条评论

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