printf
재진입이 아니기 때문에 시그널 핸들러에서 사용하는 것은 안전하지 않습니다. 하지만 printf
이런 식으로 사용하는 예제 코드를 많이 보았습니다 .
그래서 제 질문은 printf
신호 처리기에서 사용을 피해야 할 때이며 권장되는 대체품이 있습니까?
printf
재진입이 아니기 때문에 시그널 핸들러에서 사용하는 것은 안전하지 않습니다. 하지만 printf
이런 식으로 사용하는 예제 코드를 많이 보았습니다 .
그래서 제 질문은 printf
신호 처리기에서 사용을 피해야 할 때이며 권장되는 대체품이 있습니까?
답변:
일부 플래그 변수를 사용하고 시그널 핸들러 내에서 해당 플래그를 설정하고 printf()
정상적인 작동 중에 main () 또는 프로그램의 다른 부분에서 해당 플래그 호출 함수를 기반으로 할 수 있습니다.
printf
신호 처리기 내에서 와 같은 모든 함수를 호출하는 것은 안전하지 않습니다 . 유용한 기술은 신호 처리기를 사용하여 a를 설정 한flag
다음flag
주 프로그램에서 확인하고 필요한 경우 메시지를 인쇄하는 것입니다.
아래 예제에서 신호 처리기 ding () alarm_fired
은 SIGALRM이 포착되고 주 함수 alarm_fired
값 에서 printf를 조건부로 올바르게 호출하기 위해 검사 될 때 플래그 를 1로 설정 합니다.
static int alarm_fired = 0;
void ding(int sig) // can be called asynchronously
{
alarm_fired = 1; // set flag
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* if we get here we are the parent process */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired) // check flag to call printf
printf("Ding!\n");
printf("done\n");
exit(0);
}
참조 : Beginning Linux Programming, 4th Edition ,이 책에서는 코드가 정확히 설명되어 있습니다 (원하는 것), 11 장 : 프로세스 및 신호, 484 페이지
또한 비동기 적으로 호출 할 수 있으므로 핸들러 함수를 작성할 때 특별한주의가 필요합니다. 즉, 처리기는 프로그램의 어느 지점에서나 예기치 않게 호출 될 수 있습니다. 매우 짧은 간격 동안 두 신호가 도착하면 하나의 핸들러가 다른 핸들러 내에서 실행될 수 있습니다. 그리고 선언하는 것이 더 좋은 방법으로 간주됩니다 volatile sigatomic_t
.이 유형은 항상 원자 적으로 액세스되며 변수에 대한 액세스 중단에 대한 불확실성을 방지합니다. ( 자세한 만료를 위해 원자 데이터 액세스 및 신호 처리 읽기 ).
신호 처리기 정의 읽기 : signal()
또는 sigaction()
함수 로 설정할 수있는 신호 처리기 함수를 작성하는 방법을 배웁니다 . 매뉴얼 페이지
의 인증 된 기능 목록 , 신호 처리기 내에서이 함수를 호출하는 것은 안전합니다.
volatile sigatomic_t alarm_fired;
주요 문제는 신호가 중단 malloc()
되거나 유사한 기능 이있는 경우 사용 가능한 목록과 사용 된 목록 또는 기타 유사한 작업간에 메모리 블록을 이동하는 동안 내부 상태가 일시적으로 일치하지 않을 수 있다는 것입니다. 신호 처리기의 코드가 다음을 호출하는 함수를 호출하면malloc()
하면 메모리 관리가 완전히 망가질 수 있습니다.
C 표준은 신호 처리기에서 수행 할 수있는 작업에 대해 매우 보수적 인 관점을 취합니다.
ISO / IEC 9899 : 2011 §7.14.1.1
signal
기능¶5
abort
또는raise
함수 를 호출 한 결과가 아닌 다른 신호가 발생하는 경우, 신호 핸들러가 값을 할당하는 것 외에 잠금이없는 원자 객체가 아닌 정적 또는 스레드 저장 기간을 가진 객체를 참조하면 동작이 정의되지 않습니다. 로 선언 된 객체로 선언volatile sig_atomic_t
되거나, 신호 핸들러가abort
함수,_Exit
함수,quick_exit
함수 또는signal
함수를 호출 한 신호에 해당하는 신호 번호와 동일한 첫 번째 인수 를 사용하여 표준 라이브러리의 모든 함수를 호출합니다. 매니저. 또한 이러한signal
함수 호출로 인해SIG_ERR
반환이 발생하면의 값errno
이 불확실합니다. 252)252) 비동기 신호 처리기에 의해 신호가 생성되면 동작이 정의되지 않습니다.
POSIX는 시그널 핸들러에서 할 수있는 일에 대해 훨씬 더 관대합니다.
POSIX 2008 에디션의 신호 개념 은 다음과 같이 말합니다.
프로세스가 다중 스레드이거나 프로세스가 단일 스레드이고 신호 처리기가 다음의 결과가 아닌 다른 실행되는 경우 :
프로세스 호출은
abort()
,raise()
,kill()
,pthread_kill()
, 또는sigqueue()
차단되지 않은 신호를 생성하도록보류중인 신호가 차단 해제되고 차단 해제 된 호출이 반환되기 전에 전달됩니다.
신호 처리기가 다음
errno
과 같이 선언 된 개체에 값을 할당하는 것 외에 정적 저장 기간이 아닌 다른 개체를 참조하는 경우 동작이 정의되지 않습니다.volatile sig_atomic_t
를 참조하거나 신호 처리기가에 나열된 함수 중 하나가 아닌이 표준에 정의 된 함수를 호출하는 경우 동작이 정의되지 않습니다. 다음 표.다음 표는 비동기 신호 안전 기능 집합을 정의합니다. 따라서 응용 프로그램은 제한없이 신호 포착 함수에서이를 호출 할 수 있습니다.
_Exit() fexecve() posix_trace_event() sigprocmask() _exit() fork() pselect() sigqueue() … fcntl() pipe() sigpause() write() fdatasync() poll() sigpending()
위 표에없는 모든 기능은 신호와 관련하여 안전하지 않은 것으로 간주됩니다. 신호가있는 경우 POSIX.1-2008의이 볼륨에 정의 된 모든 기능은 신호 포착 기능에서 호출되거나 중단 될 때 정의 된대로 동작해야합니다. 단, 신호가 안전하지 않은 기능을 중단하고 신호- catching 함수는 안전하지 않은 함수를 호출하지만 동작은 정의되지 않았습니다.
값을 얻는
errno
작업과 값을 할당하는 작업errno
은 비동기 신호 안전이어야합니다.신호가 스레드에 전달 될 때 해당 신호의 동작이 종료, 중지 또는 계속을 지정하면 전체 프로세스가 각각 종료, 중지 또는 계속됩니다.
그러나, 그 printf()
함수 계열은 해당 목록에 특히없고 신호 처리기에서 안전하게 호출되지 않을 수 있습니다.
POSIX 2,016 업데이트를 포함하는 안전 기능의 목록을 연장, 특히 발 기능의 다수 <string.h>
특히 유용한 추가이다 (또는 특히 초조 감독이었다). 이제 목록은 다음과 같습니다.
_Exit() getppid() sendmsg() tcgetpgrp()
_exit() getsockname() sendto() tcsendbreak()
abort() getsockopt() setgid() tcsetattr()
accept() getuid() setpgid() tcsetpgrp()
access() htonl() setsid() time()
aio_error() htons() setsockopt() timer_getoverrun()
aio_return() kill() setuid() timer_gettime()
aio_suspend() link() shutdown() timer_settime()
alarm() linkat() sigaction() times()
bind() listen() sigaddset() umask()
cfgetispeed() longjmp() sigdelset() uname()
cfgetospeed() lseek() sigemptyset() unlink()
cfsetispeed() lstat() sigfillset() unlinkat()
cfsetospeed() memccpy() sigismember() utime()
chdir() memchr() siglongjmp() utimensat()
chmod() memcmp() signal() utimes()
chown() memcpy() sigpause() wait()
clock_gettime() memmove() sigpending() waitpid()
close() memset() sigprocmask() wcpcpy()
connect() mkdir() sigqueue() wcpncpy()
creat() mkdirat() sigset() wcscat()
dup() mkfifo() sigsuspend() wcschr()
dup2() mkfifoat() sleep() wcscmp()
execl() mknod() sockatmark() wcscpy()
execle() mknodat() socket() wcscspn()
execv() ntohl() socketpair() wcslen()
execve() ntohs() stat() wcsncat()
faccessat() open() stpcpy() wcsncmp()
fchdir() openat() stpncpy() wcsncpy()
fchmod() pause() strcat() wcsnlen()
fchmodat() pipe() strchr() wcspbrk()
fchown() poll() strcmp() wcsrchr()
fchownat() posix_trace_event() strcpy() wcsspn()
fcntl() pselect() strcspn() wcsstr()
fdatasync() pthread_kill() strlen() wcstok()
fexecve() pthread_self() strncat() wmemchr()
ffs() pthread_sigmask() strncmp() wmemcmp()
fork() raise() strncpy() wmemcpy()
fstat() read() strnlen() wmemmove()
fstatat() readlink() strpbrk() wmemset()
fsync() readlinkat() strrchr() write()
ftruncate() recv() strspn()
futimens() recvfrom() strstr()
getegid() recvmsg() strtok_r()
geteuid() rename() symlink()
getgid() renameat() symlinkat()
getgroups() rmdir() tcdrain()
getpeername() select() tcflow()
getpgrp() sem_post() tcflush()
getpid() send() tcgetattr()
결과적 write()
으로 printf()
et al이 제공하는 서식 지원없이 사용 하거나 코드의 적절한 위치에서 (주기적으로) 테스트하는 플래그를 설정하게됩니다. 이 기술은 훌륭하게에서 증명되고 대답 하여 Grijesh 차우 .
chqrlie 는 흥미로운 질문에 대해 부분적인 답변 만 제공합니다.
대부분의 문자열 함수
<string.h>
또는 문자 클래스 함수의 출처<ctype.h>
와 더 많은 C 표준 라이브러리 함수가 위 목록에없는 이유는 무엇입니까? 구현은strlen()
시그널 핸들러에서 호출 하는 것을 안전하지 않게 만들기 위해 고의적으로 악해야 합니다.
의 많은 함수에 <string.h>
대해 비동기 신호 안전으로 선언되지 않은 이유를 알기가 어렵습니다.strlen()
와 함께 대표적인 예이며 strchr()
, strstr()
등, 등 한편, 다른 기능 strtok()
, strcoll()
및 strxfrm()
다소 복잡하고 비동기 신호에 안전하지 않을 수 있습니다. 때문에이 strtok()
호출 사이에 상태를 유지하고, 신호 처리기 쉽게 사용하는 코드의 일부가 여부를 알 수없는 strtok()
엉망이 될 것이다. strcoll()
및 strxfrm()
기능은 로케일에 민감한 데이터로 작업하고, 로케일을로드하는 것은 국가 설정의 모든 종류를 포함한다.
에서 기능 (매크로) <ctype.h>
모든 로케일에 의존하며, 따라서 같은 문제로 실행 수 strcoll()
와 strxfrm()
.
나는 하드에서 수학 함수는 이유를 찾을 <math.h>
그들이 SIGFPE (부동 소수점 예외)에 의해 영향을받을 수 있기 때문에 유일한 시간에 대해 내가 그 요즘 볼 수 있지만 그것이 아닌 경우입니다, 비동기 시그널에 안전하지 않습니다 정수 0으로 나누기. 유사한 불확실성이 <complex.h>
, <fenv.h>
및 에서 발생합니다 <tgmath.h>
.
예를 들어의 일부 기능은 <stdlib.h>
제외 될 수 있습니다 abs()
. 다른 것들은 특히 문제가 있습니다. malloc()
그리고 가족이 대표적인 예입니다.
POSIX 환경에서 사용되는 Standard C (2011)의 다른 헤더에 대해서도 유사한 평가를 할 수 있습니다. (표준 C는 너무 제한적이어서 순수한 표준 C 환경에서 분석하는 데 관심이 없습니다.) '로케일 종속'으로 표시된 것은 로케일을 조작하는 데 메모리 할당 등이 필요할 수 있기 때문에 안전하지 않습니다.
<assert.h>
— 아마도 안전하지 않을 것입니다.<complex.h>
— 아마도 안전함<ctype.h>
- 안전하지 않음<errno.h>
— 안전<fenv.h>
— 아마도 안전하지 않을 것입니다.<float.h>
— 기능 없음<inttypes.h>
— 로케일 구분 함수 (안전하지 않음)<iso646.h>
— 기능 없음<limits.h>
— 기능 없음<locale.h>
— 로케일 구분 함수 (안전하지 않음)<math.h>
— 아마도 안전함<setjmp.h>
- 안전하지 않음<signal.h>
— 허용됨<stdalign.h>
— 기능 없음<stdarg.h>
— 기능 없음<stdatomic.h>
— 안전 할 수 있지만 안전하지 않을 수 있음<stdbool.h>
— 기능 없음<stddef.h>
— 기능 없음<stdint.h>
— 기능 없음<stdio.h>
- 안전하지 않음<stdlib.h>
— 모두 안전하지는 않습니다 (일부는 허용되지만 다른 것은 허용되지 않음).<stdnoreturn.h>
— 기능 없음<string.h>
— 모두 안전하지 않음<tgmath.h>
— 아마도 안전함<threads.h>
— 아마도 안전하지 않을 것입니다.<time.h>
— 로케일에 따라 다름 (그러나 time()
명시 적으로 허용됨)<uchar.h>
— 로케일 종속<wchar.h>
— 로케일 종속<wctype.h>
— 로케일 종속POSIX 헤더를 분석하는 것은… 많은 것이 있고 일부 기능은 안전 할 수 있지만 많은 것은 안전하지 않을 수 있다는 점에서 더 어려울 것입니다. 그러나 POSIX가 어떤 기능이 비동기 신호 안전인지 (많은 것은 아님)를 말하므로 더 간단합니다. 같은 헤더 <pthread.h>
에는 세 가지 안전한 기능과 많은 안전하지 않은 기능이 있습니다.
NB : POSIX 환경에서 C 기능 및 헤더에 대한 거의 모든 평가는 반 교육을받은 추측입니다. 표준 기관의 결정적인 진술은 의미가 없습니다.
<string.h>
또는 문자 클래스 함수의 출처 <ctype.h>
와 더 많은 C 표준 라이브러리 함수가 위 목록에없는 이유는 무엇입니까? 구현은 strlen()
시그널 핸들러에서 호출 하는 것을 안전하지 않게 만들기 위해 고의적으로 악해야 합니다.
<ctype.h>
물건에 관해서 는 로케일 에 따라 다르며 신호가 로케일 설정 기능을 방해하면 문제를 일으킬 수 있지만 로케일이로드되면이를 사용하는 것이 안전해야합니다. 복잡한 상황에서는 로케일 데이터를 점진적으로로드하여 함수가 <ctype.h>
안전하지 않게 만들 수 있다고 생각합니다 . 결론은 여전히 남아 있습니다. 의심 스러우면 기권하십시오.
printf
신호 처리기에서 사용을 피하는 방법은 무엇입니까?
항상 피하십시오 printf()
. 신호 처리기에서 사용하지 마십시오 .
POSIX 준수 시스템에 적어도, 당신이 사용할 수있는 write(STDOUT_FILENO, ...)
대신에 printf()
. 그러나 형식화가 쉽지 않을 수 있습니다. 쓰기 또는 비동기 안전 함수를 사용하여 신호 처리기에서 int 인쇄
Always avoid it.
는 의미합니까? 피 printf()
하시겠습니까?
printf()
시그널 핸들러에서 사용을 피해야 할 때를 묻기 때문 입니다.
2
포인트에 대해 Alk +1 , 신호 처리기에서 사용을 피하는 방법 을 묻는 OP를 확인하십시오 printf()
.
디버깅 목적으로, 실제로는 async-signal-safe
목록의 함수 만 호출하는지 확인 하고 신호 컨텍스트 내에서 호출되는 안전하지 않은 함수마다 경고 메시지를 출력 하는 도구를 작성했습니다 . 신호 컨텍스트에서 비동기 안전하지 않은 함수를 호출하려는 문제를 해결하지는 못하지만 적어도 실수로 수행 한 경우를 찾는 데 도움이됩니다.
소스 코드는 GitHub에 있습니다 . 오버로딩 signal/sigaction
한 다음 일시적 PLT
으로 안전하지 않은 기능 의 항목을 가로 채서 작동합니다. 이로 인해 안전하지 않은 함수에 대한 호출이 래퍼로 리디렉션됩니다.
자체 비동기 신호 안전 구현 snprintf("%d
및 사용write
내가 생각했던 것만 큼 나쁘지 않습니다 . C에서 int를 문자열로 변환하는 방법? 몇 가지 구현이 있습니다.
신호 처리기가 액세스 할 수있는 흥미로운 데이터 유형은 두 가지뿐입니다.
sig_atomic_t
글로벌int
신호 인수이것은 기본적으로 모든 흥미로운 사용 사례를 다룹니다.
사실 그 strcpy
또한 신호 안전 은 상황을 더욱 개선합니다.
아래 POSIX 프로그램은 지금까지 SIGINT를 수신 한 횟수를 표준 출력으로 인쇄합니다. Ctrl + C
하고, 및 신호 ID로 .
다음을 사용하여 프로그램을 종료 할 수 있습니다. 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:
* /programming/8257714/how-to-convert-an-int-to-string-in-c/32871108#32871108
*
* TODO: get tight limits for all bases, possibly by looking into
* glibc's atoi: /programming/190229/where-is-the-itoa-function-in-linux/52127877#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 key_str[] = "count, sigid: ";
/* This is exact:
* - the null after the first int will contain the space
* - the null after the second int will contain the newline
*/
char buf[2 * ITOA_SAFE_STRLEN(sig_atomic_t) + sizeof(key_str)];
enum { base = 10 };
char *end;
end = buf;
strcpy(end, key_str);
end += sizeof(key_str);
end = itoa_safe(global, end, base);
*end++ = ' ';
end = itoa_safe(sig, end, base);
*end++ = '\n';
write(STDOUT_FILENO, buf, end - buf);
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
Ctrl + C를 15 번 누르면 터미널에 다음이 표시됩니다.
^Ccount, sigid: 0 2
^Ccount, sigid: 1 2
^Ccount, sigid: 2 2
^Ccount, sigid: 3 2
^Ccount, sigid: 4 2
^Ccount, sigid: 5 2
^Ccount, sigid: 6 2
^Ccount, sigid: 7 2
^Ccount, sigid: 8 2
^Ccount, sigid: 9 2
^Ccount, sigid: 10 2
^Ccount, sigid: 11 2
^Ccount, sigid: 12 2
^Ccount, sigid: 13 2
^Ccount, sigid: 14 2
2
신호 번호는 어디에 있습니까?SIGINT
?
Ubuntu 18.04에서 테스트되었습니다. GitHub 업스트림 .
선택 루프 가 있는 프로그램에서 특히 유용한 한 가지 기술은 신호 수신시 파이프에 단일 바이트를 기록한 다음 선택 루프에서 신호를 처리하는 것입니다. 다음 과 같은 내용 (간결성을 위해 오류 처리 및 기타 세부 정보 생략) :
static int sigPipe[2];
static void gotSig ( int num ) { write(sigPipe[1], "!", 1); }
int main ( void ) {
pipe(sigPipe);
/* use sigaction to point signal(s) at gotSig() */
FD_SET(sigPipe[0], &readFDs);
for (;;) {
n = select(nFDs, &readFDs, ...);
if (FD_ISSET(sigPipe[0], &readFDs)) {
read(sigPipe[0], ch, 1);
/* do something about the signal here */
}
/* ... the rest of your select loop */
}
}
어떤 신호 인지 신경 쓰면 파이프 아래의 바이트가 신호 번호가 될 수 있습니다.
pthread 라이브러리를 사용하는 경우 신호 처리기에서 printf를 사용할 수 있습니다. unix / posix는 printf가 스레드에 대해 원자임을 지정합니다. cf Dave Butenhof는 여기에 답장합니다. https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw 더 명확한 그림을 얻으려면 printf 출력의 경우 GUI에 의해 생성 된 의사 tty가 아닌 콘솔에서 애플리케이션을 실행해야합니다 (리눅스에서는 ctl + alt + f1을 사용하여 콘솔 1을 시작).
printf
신호 처리자에서 호출이 보이십니까? 그것을 삭제하십시오.