[C 언어] localtime() 함수를 signal handler에서 사용시 문제점
작성일: 2025년 1월 21일
C언어로 작성된 Signal Handler 내에서 "localtime()" 함수를 사용할 때, 주의할 점이 있다.
아래 예제 코드처럼 program 내에서 이미 localtime() 함수가 사용되고 있다면, Signal Handler 내부의 localtime() 함수로 인해서 호출되는 futex_wait() 함수 부분에서 Deadlock(데드락) 상태로 빠질 수 있다.
아래 예제 소스 코드를 컴파일하고, 테스트해보면 1~5초가 지날 때쯤 deadlock 상태가 된다.
일반적으로 multi thread 환경에서 mutex lock 때문에 deadlock이 되는 경우는 많지만, 아래 예제 코드처럼 multi thread가 아닌데 deadlock 상태가 되는 경우는 경험하기 쉽지 않다.
만약, 아래 예제 코드에서 signal handler를 제거한다면 deadlock 상황은 발생하지 않는다.
/****************************************************
* How to compile
* $ gcc -g main.c -o myapp
* How to test
* $ ./myapp
***************************************************/
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
static void sigchd_handler(int signo)
{
time_t tv;
tv = time(NULL);
struct tm mytime = *localtime(&tv); // NOTE: 이 부분에서 Deadlock 발생함.
char my_date[64];
sprintf(my_date, "%d-%02d-%02d %02d:%02d:%02d",
mytime.tm_year + 1900, mytime.tm_mon + 1, mytime.tm_mday,
mytime.tm_hour, mytime.tm_min, mytime.tm_sec);
printf("%s(%d) %s() time: %s\n", __FILE__, __LINE__, __func__, my_date);
fflush(NULL);
return ;
}
int register_signal_handler(void)
{
struct sigaction sa;
sa.sa_handler = sigchd_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
return __LINE__;
}
#define LOOP_TRUE 1
int main(void)
{
int ret;
int idx;
ret = register_signal_handler();
time_t tv;
while (LOOP_TRUE)
{
printf("My PID: %d\n", (int) getpid());
FILE *fp = popen("ls -al", "r");
tv = time(NULL);
struct tm mytime;
for (idx = 0; idx < 10000; idx++)
{
printf("localtime() Function Call Counter: %d \r", idx);
fflush(NULL);
mytime = *localtime(&tv); // NOTE: 이 부분에서 Deadlock 발생함.
}
printf("\n");
char my_date[64];
sprintf(my_date, "%d-%02d-%02d %02d:%02d:%02d",
mytime.tm_year + 1900, mytime.tm_mon + 1, mytime.tm_mday,
mytime.tm_hour, mytime.tm_min, mytime.tm_sec);
printf("%s(%d) %s() time: %s\n", __FILE__, __LINE__, __func__, my_date);
fflush(NULL);
pclose(fp);
sleep(1);
}
return 0;
}
위 예제 코드에서 signal handler 때문에 deadlock이 발생하는 이유는 무엇일까?
localtime() 함수의 구현 부분을 보면, futex_wait() 함수를 호출하는 부분이 있다. 이 futex_wait() 함수가 이미 호출된 상태인데 또 한번 futex_wait() 함수가 호출되면 deadlock 상태가 된다. 즉, localtime() 함수가 호출 중인 상태에서 또 한번 localtime() 함수가 호출되면 동일 lock 변수인 tzset_lock 변수를 통해서 lock 시도를 했는데, 또 tzset_lock 변수를 이용해서 lock 시도는 하는 셈이 되는 것이다.
결국 tzset_lock -> locking 그리고 tzset_lock -> locking 이런 식으로 연속으로 두번 locking 시도를 하면서 2번째 locking 시도는 영원히 locking하려고 대기하게 된다. 또한 첫번째 locking 시도는 두 번째의 locking 시도를 한 signal_handler가 종료가 되지 않으니까 영원히 signal_handler의 종료만 기다리며 tzset_lock이 unlocking 되지 않는 것이다.
Deadlock 상태에 빠지는 이유를 이해했다면, 실습해보자!
아래와 같이 Source code를 compile하고, 바이너리 코드를 실행해보자.
실행하고 대략 1~5초 후에 프로세스가 아무것도 출력하지 않고 멈춘 것처럼 보일 것이다.
이때가 동일한 tzset_lock 변수에 대해서 첫번째 locking -> 두번째 locking을 연달아 시도하면서 deadlock 상태에 빠진 것이다.
$ gcc -g main.c -o myapp
$ ./myapp
My PID: 255676
main.c(25) sigchd_handler() time: 2025-01-21 07:54:58
localtime() Function Call Counter: 9999
main.c(72) main() time: 2025-01-21 07:54:58
My PID: 255676
localtime() Function Call Counter: 382 <-- 여기서 Deadlock 상태에 빠짐
머리로 이해한 것을 Process 내부의 function call stack을 통해서 확인해보자!
Deadlock 상태에 빠졌을 때, 아래와 같이 gdb로 function call stack을 확인해보면
- main()함수는 localtime()를 호출하면서 tzset_lock 변수를 통해 locking 시도를 하고 있었고,
- 이런 와중에 popen("ls -al", "r") 함수를 통해 실행되었던 child process가 종료되면서,
- Parent Process인 "myapp"이 SIGCHLD를 수신하게 된다.
- 이때 signal handler인 "sigchd_handler"가 실행되면서 localtime() 함수를 한번 더 실행하게 된다.
- 짠~! main() 함수가 아직 localtime()함수를 통해 tzset_lock을 unlock하지 않았는데, signal handler의 localtime() 함수가 tzset_lock을 locking 시도를 했기 때문에 이때부터 dealock 상태가 되는 것이다.
$ gdb ./myapp 255676
(gdb) where
#0 futex_wait (private=0, expected=2, futex_word=0x7fa99d11c760 <tzset_lock>) at ../sysdeps/nptl/futex-internal.h:146
#1 __GI___lll_lock_wait_private (futex=futex@entry=0x7fa99d11c760 <tzset_lock>) at ./nptl/lowlevellock.c:34
#2 0x00007fa99cfd6744 in __tz_convert (timer=1737446099, use_localtime=1, tp=0x7fa99d11c6a0 <_tmbuf>) at ./time/tzset.c:572
#3 0x0000564ee1a0a30e in sigchd_handler (signo=17) at main.c:20
#4 <signal handler called>
#5 __GI___fstatat64 (fd=-100, file=0x7fa99d0d4b02 "/etc/localtime", buf=0x7ffdd9ca4780, flag=0)
at ../sysdeps/unix/sysv/linux/fstatat64.c:166
#6 0x00007fa99cfd683c in __tzfile_read (file=file@entry=0x7fa99d0d4b02 "/etc/localtime", extra=extra@entry=0,
extrap=extrap@entry=0x0) at ./time/tzfile.c:155
#7 0x00007fa99cfd5c24 in tzset_internal (always=<optimized out>) at ./time/tzset.c:405
#8 0x00007fa99cfd65a7 in __tz_convert (timer=1737446099, use_localtime=1, tp=0x7fa99d11c6a0 <_tmbuf>) at ./time/tzset.c:577
#9 0x0000564ee1a0a528 in main () at main.c:65
(gdb)
그러면, signal handler 내부의 localtime() 함수 때문에 deadlock이 생기는 상황을 피하려면 어떻게 해야 할까?
signal handler에서는 Lock 관련 함수/변수 사용 금지, I/O 관련 함수 호출을 하지 말라는 권고가 옛날부터 전해져 내려오고 있다.
signal handler는 최소의 작업만, 그리고 빠른 시간에 처리하고 종료되도록 구현하라는 권고도 있어왔다.
그러니까 localtime() 함수로 예쁘게 date 값을 뽑아내려고 하지 말고, 그냥 time(NULL)을 이용해서 epoch time 값을 뽑아서 활용하면 문제를 해결할 수 있다.
signal handler 내부에서 Lock 관련 함수를 쓰지 않으면 만사 OK !!