반응형
작성일: 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을 확인해보면

  1. main()함수는 localtime()를 호출하면서 tzset_lock 변수를 통해 locking 시도를 하고 있었고,
  2. 이런 와중에 popen("ls -al", "r") 함수를 통해 실행되었던 child process가 종료되면서,
  3. Parent Process인 "myapp"이 SIGCHLD를 수신하게 된다.
  4. 이때 signal handler인 "sigchd_handler"가 실행되면서 localtime() 함수를 한번 더 실행하게 된다.
  5. 짠~!  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 !!

반응형

 

Kubernetes를 사용하다보면, Pod가 Terminating 상태에서 종료(즉, Pod의 삭제)되지 않고 계속 머물러있는 경우가 종종 발생한다.

이렇게 Pod의 Terminating 교착 상태가 된 원인은 정확히 알 수는 없고, 

단지 이런 경우에 Pod를 종료시킬 수 없어서 당혹스럽다.

 

Ian Miell 이라는 사람이 상황별로 교착 상태에 빠진 Pod를 종료하는 방법을 정리한 Web Docs가  있어서 나한테 맞게 다시 메모를 해봤다.

 

$  kubectl  delete  -n istio-system  deployment  grafana

##
## 위 delete 명령을 수행 후, 1분이 넘도록 Pod가 Terminating 상태라면
## 이 Pod는 계속 Terminating 상태로 남고, 아래 예시처럼 Delete되지 않을 것이다.
##

$  kubectl  get -A pod

NAMESPACE      NAME                                       READY   STATUS        RESTARTS       AGE
istio-system   grafana-68cc7d6d78-7kjw8                   1/1     Terminating   0              37d

... 중간 생략 ...

$

 

 

위 현상을 세분화해서 해결 방법을 설명해보겠다.

 

 

Pod의 상세 정보를 확인

 

##
## (A) 강제로 Pod를 삭제하는 방법
##

$  kubectl  delete  pods <pod>  --grace-period=0  --force

## 웬만하면, 위 명령으로 Pod가 삭제되지만
## 만약 계속 Pod의 찌끄러기가 남아 있다면, 아래 (B) 절차를 추가로 수행해야 한다.



##
## (B) 위 명령을 수행하고도 Pod이 Stuck 상태 또는 Unknown 상태로 남아 있다면
##     아래의 방법으로 Pod를 끝장낼 수 있다.
##

$  kubectl  patch  pod <pod>  -p '{"metadata":{"finalizers":null}}'

 

 

 

Reference

 

Kubernetes.io에 Pod의 강제 종료에 대한 상세한 설명을 있으니, 시간이 있다면 꼼꼼히 읽어보면 도움이 된다.

 

https://kubernetes.io/docs/tasks/run-application/force-delete-stateful-set-pod/

 

Force Delete StatefulSet Pods

This page shows how to delete Pods which are part of a stateful set, and explains the considerations to keep in mind when doing so. Before you begin This is a fairly advanced task and has the potential to violate some of the properties inherent to Stateful

kubernetes.io

 

 

그리고 위에서 finalizers를 강제로 null로 patch했는데, finalizers에 관한 상세한 설명이 궁금하면 아래 kubernetes web docs를 읽어보는 것이 좋다.

 

https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/

 

Finalizers

Finalizers are namespaced keys that tell Kubernetes to wait until specific conditions are met before it fully deletes resources marked for deletion. Finalizers alert controllers to clean up resources the deleted object owned. When you tell Kubernetes to de

kubernetes.io

 

 

게시물 작성자: sejong.jeonjo@gmail.com

 

 


 

 

 

 

 

##
## 채용 관련 글
##
제가 일하고 있는 기업 부설연구소에서 저와 같이 연구/개발할 동료를 찾고 있습니다.
(이곳은 개인 블로그라서 기업 이름은 기재하지 않겠습니다. E-mail로 문의주시면 자세한 정보를 공유하겠습니다.)

근무지 위치:
  서울시 서초구 서초동, 3호선 남부터미널역 근처 (전철역 출구에서 회사 입구까지 도보로 328m)
필요한 지식 (아래 내용 중에서 70% 정도를 미리 알고 있다면 빠르게 협업할 수 있음):
  - 운영체제 (학부 3~4학년 때, 컴퓨터공학 운영체제 과목에서 배운 지식 수준):
    예를 들어, Processor, Process 생성(Fork)/종료, Memory, 동시성, 병렬처리, OS kernel driver  
  - Linux OS에서 IPC 구현이 가능
    예를 들어, MSGQ, SHM, Named PIPE 등 활용하여 Process간 Comm.하는 기능 구현이 가능하면 됨. 
  - Algorithm(C언어, C++ 언어로 구현 가능해야 함)
    예를 들어, Hashtable, B-Tree, Qsort 정도를 C 또는 C++로 구현할 수 있을 정도 
  - Network 패킷 처리 지식(Layer 2 ~ 4, Layer 7)
    예를 들어, DHCP Server/Client의 주요 Feature를 구현할 정도의 능력이 있으면 됨.
  - Netfilter, eBPF 등 (IP packet hooking, ethernet packet 처리, UDP/TCP packet 처리)
  - IETF RFC 문서를 잘 읽고 이해하는 능력 ^^
  # 위에 열거한 내용 외에도 제가 여기 블로그에 적은 내용들이 대부분 업무하면서 관련이 있는 주제를 기록한 것이라서
  # 이 블로그에 있는 내용들을 잘 알고 있다면, 저희 연구소에 와서 연구/개발 업무를 수행함에 있어서 어려움이 없을 겁니다.
회사에서 사용하는 프로그래밍 언어:
  - 프로그래밍 언어: C, C++, Go
    (참고: 아직 연구소 동료들이 Rust를 사용하진 않습니다만, 새 언어로써 Rust를 사용하는 것을 고려하는 중)
근무 시간:
  - 출근: 8~10시 사이에서 자유롭게 선택
  - 퇴근: 8시간 근무 후 퇴근 (오후 5시 ~ 7시 사이)
  - 야근 여부: 거의 없음 (내 경우, 올해 상반기 6개월간 7시 이후에 퇴근한 경우가 2회 있었음)
  - 회식 여부: 자유 (1년에 2회 정도 회식하는데, 본인이 집에 가고 싶으면 회식에 안 감. 왜 참석 안 하는지 묻지도 않음)
외근 여부:
  - 신규 프로젝트 멤버 -> 외근 전혀 하지 않음 (나는 신규 프로젝트만 참여해서 지난 1년 동안 한번도 외근 없었음)
  - 상용 프로젝트 멤버 -> 1년에 5회 미만 정도로 외근
팀 워크샵 여부:
  - 팀 워크샵 자체를 진행하지 않음. (워크샵 참석하는 거 싫어하는 개발자 환영 ^^)
연락처:
  - "sejong.jeonjo@gmail.com"  # 궁금한 점은 이 연락처로 문의주세요.
  - 블로그 비밀 댓글 (제가 하루에 한번씩 댓글 확인하고 있음)
원하는 인재상:
  - 우리 부설연구소는 "긴 호흡으로 프로젝트를 진행"하기 때문에 최소 2년간 한 가지 주제를 꾸준하게 연구/개발할 수 있는 개발자를 원함.
  - 우리 부설연구소는 자주적으로 연구 주제를 찾아서 업무를 하기 때문에 능동적으로 생각하고 행동하는 동료를 원함.
  - 차분하게 연구 주제에 몰입하고, 해법을 찾는 것을 즐기는 사람.
내가 느끼는 우리 연구소의 장점:
  - 갑/을 관계가 없음. (제가 근무하고 있는 연구소는 SI업종이 아니라서 갑/을 회사 개념이 없음)
  - 연구소 자체적으로 연구 주제를 발굴하고 시스템을 개발하기 때문에 개발 일정에 대한 스트레스가 적음
  - 빌딩 전체를 우리 회사가 사용하므로 분위기가 산만하지 않음.
  - 근처에 예술의전당, 우면산 둘레길이 있어서 점심 시간에 산책하기 좋음 ^^
  - 연구소 동료들 매너가 Good (2년간 일하면서 한번도 감정에 스크레치 생기거나 얼굴 붉히며 싸운 적 없음 ^^)

 

+ Recent posts