17. Variations in control flow

프로그램의 제어 흐름은 프로그램의 각 명령문들이 어떤 순서로 실행되는지를 나타낸다. 지금까지 다룬 함수들은 기본 블록들의 계층들로 볼 수 있었다. 기본 블록은 명령문이 조건 없이 연속적으로 계속 실행되는 최소 단위들을 말한다. 기본적으로는 이러한 프로그래밍 모델을 쓰는 것이 좋으나 가끔 제어 흐름을 변경해야 할 때가 있다.

  • 조건문 : if/else, switch/case
  • 반복문 : do-while, while, for
  • 함수 : 함수 호출, return, _Noreturn
  • 짧은 점프 : goto
  • 긴 점프 : setjmp/longjmp
  • 중단 : signal 및 그 핸들러
  • 스레드 : thrd_create, thrd_exit

이런 제어 흐름의 변화는 실행의 추상 상태에 대한 판단을 어렵게 만든다. 위의 4개는 C 언어 특성이지만 아래 3개는 C 라이브러리 인터페이스이다. 예상치 못한 제어 흐름은 다음과 같은 부정적 효과가 있다:

  • 오브젝트가 생애 주기 밖에서 사용될 수 있다.
  • 오브젝트가 초기화되지 않은 채 사용될 수 있다.
  • 오브젝트의 값이 최적화에 의해 잘못 세팅될 수 있다. (volatile)
  • 오브젝트가 부분적으로 수정될 수 있다. (sig_atomic_t, atomic_flag, _Atomic)
  • 오브젝트의 업데이트가 예상치 못하게 순서화될 수 있다. (_Atomic)
  • 한계 섹션 내에서의 실행은 배타적이어야 한다. (mtx_t)

C에서는 이러한 난점에 대해 다루는 법들을 제공한다.

17.1. A complicated example.

다음은 재귀적 파서의 예제이다.

#include <ctype.h>
#include <setjmp.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include "sighandler.h"

#define LEFT '{'
#define RIGHT '}'

static char const head[] = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>| ";

static char const* skipspace(char const* s) {
  while (s && isspace(s[0])) {
    ++s;
  }
  return s;
}

enum state {
  execution = 0,    //*< Normal execution
  plusL,            //*< Too many left braces
  plusR,            //*< Too many right braces
  tooDeep,          //*< Nesting too deep to handle
  eofOut,           //*< End of output
  interrupted,      //*< Interrupted by a signal
};

static char const* end_line(char const* s, jmp_buf jmpTarget) {
  if (putchar('\n') == EOF) longjmp(jmpTarget, eofOut);
  return skipspace(s);
}

typedef enum state state;

static sig_atomic_t volatile interrupt = 0;

static char const* descend(char const* act,
                    unsigned dp[restrict static 1], // Bad
                    size_t len, char buffer[len],
                    jmp_buf jmpTarget) {
  if (dp[0]+3 > sizeof head) longjmp(jmpTarget, tooDeep);
  ++dp[0];/*@\label{lab:incr-rec}*/
 NEW_LINE:                             // Loops on output
  while (!act || !act[0]) {            // Loops for input
    if (interrupt) longjmp(jmpTarget, interrupted);
    act = skipspace(fgets(buffer, len, stdin));
    if (!act) {                        // End of stream
      if (dp[0] != 1) longjmp(jmpTarget, plusL);
      else goto ASCEND;
    }
  }
  fputs(&head[sizeof head - (dp[0] + 2)], stdout); // Header

  for (; act && act[0]; ++act) { // Remainder of the line
    switch (act[0]) {            /*@\label{lab:switch-char}*/
    case LEFT:                   // Descends on left brace
      act = end_line(act+1, jmpTarget);
      act = descend(act, dp, len, buffer, jmpTarget);/*@\label{lab:descend}*/
      act = end_line(act+1, jmpTarget);
      goto NEW_LINE;
    case RIGHT:                  // Returns on right brace
      if (dp[0] == 1) longjmp(jmpTarget, plusR);
      else goto ASCEND;
    default:                     // Prints char and goes on
      putchar(act[0]);
    }
  }
  goto NEW_LINE;
 ASCEND:
  --dp[0];/*@\label{lab:decr-rec}*/
  return act;
}

enum { maxline = 256 };

void basic_blocks(void) {
  char buffer[maxline];
  unsigned depth = 0;
  char const* format =
    "All %0.0d%c %c blocks have been closed correctly\n";
  jmp_buf jmpTarget;
  switch (setjmp(jmpTarget)) {
  case 0:
    descend(0, &depth, maxline, buffer, jmpTarget);
    break;
  case plusL:
    format =
      "Warning: %d %c %c blocks have not been closed properly\n";
    break;
  case plusR:
    format =
      "Error: closing too many (%d) %c %c blocks\n";
    break;
  case tooDeep:
    format =
      "Error: nesting (%d) of %c %c blocks is too deep\n";
    break;
  case eofOut:
    format =
      "Error: EOF for stdout at nesting (%d) of %c %c blocks\n";
    break;
  case interrupted:
    format =
      "Interrupted at level %d of %c %c block nesting\n";
    break;
  default:;
    format =
      "Error: unknown error within (%d) %c %c blocks\n";
  }
  fflush(stdout);
  fprintf(stderr, format, depth, LEFT, RIGHT);
  if (interrupt) {
    SH_PRINT(stderr, interrupt,
             "is somebody trying to kill us?");
    raise(interrupt);
  }
}

static void signal_handler(int sig) {
  sh_count(sig);
  switch (sig) {
  case SIGTERM: quick_exit(EXIT_FAILURE);
  case SIGABRT: _Exit(EXIT_FAILURE);
#ifdef SIGCONT
    // continue normal operation
  case SIGCONT: return;
#endif
  default:
    /* reset the handling to its default */
    signal(sig, SIG_DFL);
    interrupt = sig;
    return;
  }
}

static char** lastOpen = 0;

void doAtExit(void) {
  if (lastOpen && lastOpen[0]) {
    fprintf(stderr, "\n***********\nabnormal exit, last open file was %s\n", lastOpen[0]);
  }
}

int main(int argc, char* argv[argc+1]) {
  // Ensures that stdout is line buffered
  if (setvbuf(stdout, 0, _IOLBF, maxline + sizeof head + 2)) {
    fputs("we could not establish line buffering for stdout, terminating.", stderr);
    return EXIT_FAILURE;
  }

  // Establishes exit handlers
  atexit(doAtExit);
  at_quick_exit(doAtExit);

  // Establishes signal handlers
  for (unsigned i = 1; i < sh_known; ++i)
    sh_enable(i, signal_handler);

  // If there are no command-line arguments, reads from stdin
  lastOpen = argv;
  if (argc < 2) goto RUN;

  // Runs basic_blocks for each command-line argument
  for (++lastOpen; lastOpen[0]; ++lastOpen) {
    if (!freopen(lastOpen[0], "r", stdin)) {
      perror(lastOpen[0]);
      return EXIT_FAILURE;
    }
    printf("++++++++++ %s +++++++++++\n", lastOpen[0]);
  RUN:
    basic_blocks();
  }
  return EXIT_SUCCESS;
}

17.2. Sequencing.

일반적으로 C 표현식은 순서화되지 않는다. 함수 인자 내의 표현식이 부가 효과를 가질 경우 예상치 못한 결과를 낳을 수 있다. 이는 컴파일러 최적화의 효율화를 위해서이다.

#include<stdio.h>

unsigned add(unsigned* x, unsigned const* y) {
  return *x += *y;
}
int main(void) {
  unsigned a = 3;
  unsigned b = 5; 
  printf("a = %u, b = %u\n", add(&a, &b), add(&b, &a));
}

위의 코드는 a = 8, b = 13이 나올 수도 있고 a = 11, b = 8이 나올 수도 있다. 이것은 정의되지 않은 행동이 아니라, 정의된 행동이다. 그러므로 컴파일러가 경고해주지 않는다고 불평하지 말라.

Takeaway 3.17.2.1. 함수 내의 부가 효과는 판정 불능의 결과를 낳을 수 있다.

C에서 순서를 정해주는 순서 포인트들은 다음과 같다.

  • ;나 ,로 끝나는 명령문의 끝.
  • , 이전 표현식의 끝.
  • ;나 ,로 끝나는 선언문의 끝.
  • if, switch, for, while, ? : , ||, && 의 제어식의 끝.
  • 함수 지정자와 함수 인자들 이후 함수 호출 이전
  • return문 끝.

Takeaway 3.17.2.2. 임의 연산자의 연산은 피연산자의 평가 이후에 이루어진다.

Takeaway 3.17.2.3. 오브젝트의 대입, 증가, 감소로 인한 업데이트는 피연산자의 평가 이후에 이루어진다.

Takeaway 3.17.2.4. 함수 호출은 호출자의 평가 이후에 이루어진다.

Takeaway 3.17.2.5. 배열이나 구조체에 대한 초기화 리스트의 평가 순서는 정해져 있지 않다.

C 라이브러리에도 몇 가지 순서 지점이 정의되어 있다.

  • IO 함수의 포맷 특정자 이후
  • C 라이브러리 함수 리턴 이전
  • 탐색/정렬에 쓰이는 비교 함수 호출 이전/이후

17.3. Short jumps.

다음 두 코드는 같지 않다. ip 내의 오브젝트 주기가 달라지기 때문이다.

size_t * ip = 0
while ( something )
ip = &( size_t ){ fun () }; /* Life ends with while */
/* Good : resource is freed */
printf ("i is %d", *ip) /* Bad: object is dead */
size_t * ip = 0
RETRY :
ip = &( size_t ){ fun () }; /* Life continues */
if ( condition ) goto RETRY ;
/* Bad: resource is blocked */
printf ("i is %d", *ip) /* Good : object is alive */

Takeaway 3.17.3.1. 각 반복문은 지역 객체의 인스턴스를 새로 생성한다.

첫 번째 코드가 제어 흐름이 더 명확하고 더 많은 최적화 기회를 연다.

Takeaway 3.17.3.2. goto는 제어 흐름의 예외적 변환에 대해서만 쓰여야 한다.

17.4. Functions.

Takeaway 3.17.4.1. 각 함수 호출은 지역 객체의 인스턴스를 새로 생성한다.

17.5. Long jumps.

longjmp는 회복될 수 없는 오류에 대한 예외 처리로 쓴다.

Takeaway 3.17.5.1. longjmp는 호출자에 대해 리턴되지 않는다.

Takeaway 3.17.5.2. 일반적 제어 흐름을 통해 도달하였을 때, setjmp는 해당 위치를 점프 지점으로 설정하고 0을 리턴한다.

Takeaway 3.17.5.3. setjmp의 호출의 범위를 벗어나는 것은 점프 지점을 무효화시킨다.

Takeaway 3.17.5.4. longjmp 호출은 setjmp에 세팅된 점프 지점으로 제어 흐름을 이동시키며 이 때 조건식을 받은 것처럼 처리된다.

Takeaway 3.17.5.5. longjmp의 condition 인자에 0이 들어가면 1로 변환된다.

이는 일반 제어 흐름을 두 번 타는 일을 막기 위해서이다.

Takeaway 3.17.5.6. setjmp는 조건문 내 제어식 안에서의 간단한 비교에서만 사용되어야 한다.

setjmp의 리턴값은 대입에 사용될 수 없다. 이는 setjmp 값이 이미 알고 있는 값들에만 사용될 수 있다는 것을 보장한다.

Takeaway 3.17.5.7. setjmp 호출은 심하게 최적화된다.

위의 코드에서 basic_blocks는 depth 변수는 switch 문의 case 0:에서만 변경된다고 가정하기 때문에 이외의 블록에서는 depth 변수를 자동적으로 0으로 세팅한다. 즉, 잘못 최적화하는 것이다.

Takeaway 3.17.5.8. longjmp를 통해 수정되는 오브젝트는 반드시 volatile로 선언해야 한다.

Takeaway 3.17.5.9. volatile 오브젝트는 접근될 때마다 메모리에서 다시 로딩된다.

Takeaway 3.17.5.10. volatile 오브젝트는 수정될 때마다 메모리에 다시 저장된다.

jmp_buf는 불명확 타입이다.

Takeaway 3.17.5.11. jmp_buf의 typedef는 배열 타입을 숨긴다.

  • jmp_buf 타입 오브젝트는 대입될 수 없다.
  • jmp_buf 함수 인자는 jmp_buf_base의 포인터로 덧씌워진다.
  • 이러한 함수는 항상 원본 오브젝트를 가리키고, 복제를 하지 않는다.

이는 참조에 의한 전달 메커니즘을 흉내낸 것이다. 일반적으로 이런 식의 인터페이스는 좋지 않으므로 지양하도록 하자.

17.6. Signal handlers.

신호 처리자는 프로그램 외부에서 신호로 전해지는 예외 조건을 다룬다. 신호에는 2가지가 있는데 하나는 하드웨어 신호(트랩/동기 신호) 또는 소프트웨어 신호(비동기 신호)이다.

첫 번째는 프로세스를 진행하는 기기가 대응 불능한 오류가 났을 때 발생한다. 0으로 나눈다든가 존재하지 않는 메모리에 대한 참조, 잘못 정렬된 주소 등. 이러한 신호는 프로그램 실행과 동기적이기 때문에 어떤 명령문에 의해 실행이 중지되는지 항상 알 수 있다.

두 번째는 운영체제나 런타임 시스템이 프로그램을 종료시켜야 한다고 판단했을 때 발생한다. 사용자가 프로그램을 직접 종료했을 때, 특정 타이머가 흘렀을 때 등. 이러한 신호는 프로그램 실행과 비동기적인데, 이는 여러 명령문 사이에 끼어들어갈 수 있기 때문에 실행 환경의 추상 상태를 판별하기 불가능하게 만든다.

대부분의 현대 프로세서는 하드웨어 중단을 다루기 위한 중단 벡터 테이블이 존재한다. 이 테이블은 특정 폴트가 발생할 때 그에 대응해 발생하는 프로시져에 대한 포인터인 중단 핸들러로 이루어져 있다. 이런 메커니즘은 포터블하지 않다. 폴트의 이름이나 위치는 플랫폼마다 다르기 때문이다.

C의 신호 핸들러는 이런 하드웨어나 소프트웨어 중단을 다루는 추상화를 다음과 같은 방식으로 제공한다.

  • 일부 폴트의 이름을 표준화해 제공한다.
  • 모든 폴트는 기본 핸들러가 주어져 있다 (세부 구현은 구현체에 따라 다르지만).
  • 대부분의 핸들러가 특정 가능하다.

하지만 C의 신호 핸들러는 기초적이기 때문에 모든 플랫폼은 그 확장을 제공하며 특수한 규칙을 갖고 있다.

Takeaway 3.17.6.1. C의 신호 핸들링 인터페이스는 최소한도로만 제공되므로 기초적인 상황에서만 사용되어야 한다.

C 표준에서는 6가지 신호를 제공한다. 다음의 3가지는 하드웨어 중단이다:

  • SIGFPE : 부당한 산술 연산. 0에 의한 나누기 또는 오버플로우 등
  • SIGILL : 부당한 명령문
  • SIGSEGV : 저장소에 대한 부당한 접근

다음의 3가지는 소프트웨어 중단이다:

  • SIGABRT : abort 함수에 의한 비정상적 중단
  • SIGINT : 상호 작용되는 주의 신호
  • SIGTERM : 프로그램 종료 요청에 의한 중단

SIG로 시작되는 식별자는 예약어로 지정되어 있으므로 다른 신호도 정의할 수 있다. C 표준에는 정의되어 있지 않으므로 이를 정의해 쓰면 코드가 덜 포터블해진다.

신호를 다루는 정책은 2가지가 있는데 SIG_DFL은 플랫폼의 해당 신호에 대한 기본 핸들러이고 SIG_IGN은 해당 신호를 무시하라는 뜻이다. 신호 핸들러는 다음과 호환 가능해야 한다:

typedef void sh_handler(int);

신호 핸들러는 signal에 의한 호출로 불려진다. signal은 signal.h에 제공되는 다음의 2가지 인터페이스 중 하나이다:

sh_handler* signal(int, sh_handler*);
int raise(int);

signal의 리턴값은 해당 신호를 보낸 핸들러의 포인터이거나, 에러가 발생한 경우 SIG_ERR이다. 신호 핸들러 내에서 signal은 호출된 신호에 대한 정책을 바꾸는 일만 해야 한다. 예로는 다음 코드가 있다.


#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "sighandler.h"

#define SH_PAIR(X, D) [X] = { .name = #X, .desc = "" D "", }

sh_pair const sh_pairs[] = {
  /* Execution errors */
  SH_PAIR(SIGFPE, "erroneous arithmetic operation"),
  SH_PAIR(SIGILL, "invalid instruction"),
  SH_PAIR(SIGSEGV, "invalid access to storage"),
#ifdef SIGBUS
  SH_PAIR(SIGBUS, "bad address"),
#endif
  /* Job control */
  SH_PAIR(SIGABRT, "abnormal termination"),
  SH_PAIR(SIGINT, "interactive attention signal"),
  SH_PAIR(SIGTERM, "termination request"),
#ifdef SIGKILL
  SH_PAIR(SIGKILL, "kill signal"),
#endif
#ifdef SIGQUIT
  SH_PAIR(SIGQUIT, "keyboard quit"),
#endif
#ifdef SIGSTOP
  SH_PAIR(SIGSTOP, "stop process"),
#endif
#ifdef SIGCONT
  SH_PAIR(SIGCONT, "continue if stopped"),
#endif
#ifdef SIGINFO
  SH_PAIR(SIGINFO, "status information request"),
#endif
};

size_t const sh_known = (sizeof sh_pairs/sizeof sh_pairs[0]);

#if ATOMIC_LONG_LOCK_FREE > 1
_Atomic(unsigned long) sh_counts[sizeof sh_pairs/sizeof sh_pairs[0]];
# define SH_COU " (%lu times),"
#else
# define SH_COU "%0.0lu,"
#endif

void sh_count(int);
unsigned long sh_counted(int);

#define SH_HEAD "\r%s:%zu: "
#define SH_DOC "\t%s,\t%s"

void sh_print(FILE* io, int sig,
              char const* filename, size_t line,
              char const* string) {
  char const* doc =
    (sig < sh_known && sh_pairs[sig].name)
    ? sh_pairs[sig].desc
    : "unknown signal number";
  if (errno) {
    char const* err = strerror(errno);
    errno = 0;
    if (!sig)
      fprintf(io, SH_HEAD "\t%s:\t%s\n", filename, line,
              string, err);
    else if (sig < sh_known && sh_pairs[sig].name)
      fprintf(io, SH_HEAD "%s" SH_COU SH_DOC ":\t%s\n", filename, line,
              sh_pairs[sig].name, sh_counted(sig), doc, string, err);
    else
      fprintf(io, SH_HEAD "#%d" SH_COU SH_DOC ":\t%s\n", filename, line,
              sig, sh_counted(sig), doc, string, err);
  } else {
    if (!sig)
      fprintf(io, SH_HEAD "\t%s\n", filename, line,
              string);
    else if (sig < sh_known && sh_pairs[sig].name)
      fprintf(io, SH_HEAD "%s" SH_COU SH_DOC "\n", filename, line,
              sh_pairs[sig].name, sh_counted(sig), doc, string);
    else
      fprintf(io, SH_HEAD "#%d" SH_COU SH_DOC "\n", filename, line,
              sig, sh_counted(sig), doc, string);
  }
}

sh_handler* sh_enable(int sig, sh_handler* hnd) {
  sh_handler* ret = signal(sig, hnd);
  if (ret == SIG_ERR) {
    SH_PRINT(stderr, sig, "failed");
    errno = 0;
  } else if (ret == SIG_IGN) {
    SH_PRINT(stderr, sig, "previously ignored");
  } else if (ret && ret != SIG_DFL) {
    SH_PRINT(stderr, sig, "previously set otherwise");
  } else {
      SH_PRINT(stderr, sig, "ok");
  }
  return ret;
}

raise는 현재 실행에 대해 특정한 신호를 전달할 때 쓰인다. 메커니즘은 setjmp/longjmp와 비슷한데, 다른 것은 setjmp처럼 점프 대상 지점을 설정하지 않는다는 것이다.

Takeaway 3.17.6.2. 신호 핸들러는 실행 중의 어느 부분으로라도 점프해 들어갈 수 있다.

소프트웨어 중단은 SIGABRT, SIGTERM, SIGINT 등이 있다. UNIX 터미널에서 Ctrl+C 등을 누르면 전달되는 그 신호들이다. 첫 두 개의 신호는 _Exit, quick_exit을 호출한다. 첫 번째는 자원 청소를 하지 않고 두 번째는 quick_exit에 등록된 청소 핸들러들을 이용해 자원 청소를 한다. SIGINT는 기본값 신호 핸들러를 선택한 뒤 중단점으로 이를 반환한다.

Takeaway 3.17.6.3. 신호 핸들러에 의한 반환 이후에, 실행은 중단점부터 재개된다.

즉, 신호 핸들러가 발생하기 전후의 차이점은 신호 핸들러에 의해 변수가 변경된 것밖에 없다.

C 표준에 없는 POSIX 신호인 SIGCONT도 있는데 이는 중단된 프로그램의 실행을 계속하는 것이다. setjmp/longjmp와 또 하나 다른 점은 setjmp의 반환값은 실행 경로를 변경하지만 신호 핸들러는 정보를 프로그램에 전달하기 위한 인터페이스를 따로 만들어줘야 한다는 점이다. 또한, longjmp와 마찬가지로, 신호 핸들러에 의해 변경될 수 있는 모든 값은 volatile로 선언되어야 한다. 또 어려운 점은:

Takeaway 3.17.6.4. C 명령문은 여러 프로세서 명령문에 대응될 수 있다.

예를 들어 double x에 대한 대입은 2개의 기기 단어를 변경하라는 명령문에 대응될 수 있다. 문제는 이 2개의 명령문 사이에 신호 핸들러가 끼어들어갔을 때 발생한다. double의 두 단어 값 중 한 단어만 써지고 한 단어는 안 써지는 좀비 표현식이 발생하는 것이다.

Takeaway 3.17.6.5. 신호 핸들러는 중단 가능하지 않은 연산과 타입에만 적용되어야 한다.

C에는 중단 가능하지 않은 연산을 제공하는 타입이 3가지 있다.

  • sig_atomic_t, 최소 8비트의 두께를 갖는 정수
  • atomic_flag
  • 락-프리 특성을 가진 다른 원자적 타입

sig_atomic_t의 존재성은 보장되지만 여기에 대한 연산이 항상 원자적인 것은 아니다. 오로지 메모리 로딩과 변수에 대한 저장만 원자성을 보장받는다.

Takeaway 3.17.6.6. sig_atomic_t를 카운터로 사용하지 말라.

++ 연산은 로딩, 가산, 저장의 3가지 명령문으로 나뉘고 오버플로우가 발생할 수 있으므로 이는 대단히 골치 아픈 버그를 만들어낸다.

나머지 2개는 C11 및 그 이후에만 정의되어 있으며 __STDC_NO_ATOMICS__가 false인 경우에만 정의되어 있다.

비동기 신호에 대한 신호 핸들러는 프로그램 상태를 제어 가능하지 않은 형태로 접근하거나 변경하면 안 되기 때문에 그럴 수 있는 다른 함수를 호출하면 안 된다. 비동기 신호에 대한 신호 핸들러에 의해 사용될 수 있는 함수 들을 비동기 신호 안전하다고 한다. C 표준에서는 극히 제한적인 경우에만 이를 보장한다.

  • _Noreturn이 붙은 abort, _Exit, quick_exit
  • 해당 신호 핸들러를 호출한 신호에 대한 signal
  • 원자적 오브젝트에 대해 대응하는 함수들

Takeaway 3.17.6.7. 따로 명시되어 있지 않다면, C 라이브러리 함수들은 비동기 신호 안전하지 않다.

C 표준에 의하면 신호 핸들러는 exit을 호출하거나 입출력을 할 수 없다. 보았듯이 C 표준에서의 신호 핸들러에 대한 규정은 매우 제한적이므로 포터블하게 프로그래밍하기 힘들다. 다음의 방법들이 있다.

  • 지역적으로 감지되고 대응될 수 있는 예외 조건은 goto와 적은 수의 라벨을 이용해 다룬다.
  • 지역적으로 감지되고 대응될 수 없는 예외 조건은 특별한 값을 리턴해 다룬다. 널 포인터라든가.
  • 프로그램의 전역적 상태를 바꿀 수 있는 예외 조건은, 예외적 리턴이 어려울 경우 setjmp/longjmp를 이용해 다룬다.
  • 신호에 의해 발생할 수 있는 예외 조건은 신호 핸들러를 이용해 다루지만, 일반적인 제어 흐름 내에서 핸들러의 리턴 이후에 다뤄져야 한다.

플랫폼이 원자적 연산을 제공한다면 원자적 연산을 다음과 같이 수행할 수 있다.

#ifndef SIGHANDLER_H
#define SIGHANDLER_H 1

#include <signal.h>
#include <stdio.h>
#ifndef __STDC_NO_ATOMIC__
# include <stdatomic.h>
#endif

typedef struct sh_pair sh_pair;
struct sh_pair {
  char const* name;
  char const* desc;
};

extern sh_pair const sh_pairs[];

extern size_t const sh_known;

#if ATOMIC_LONG_LOCK_FREE > 1
extern _Atomic(unsigned long) sh_counts[];

inline void sh_count(int sig) {
  if (sig < sh_known) ++sh_counts[sig];
}

inline unsigned long sh_counted(int sig){
  return (sig < sh_known) ? sh_counts[sig] : 0;
}

#else
inline void sh_count(int sig) {
  // empty
}

inline unsigned long sh_counted(int sig){
  return 0;
}
#endif

typedef void sh_handler(int);

void sh_print(FILE* io, int sig,
              char const* filename, size_t line,
              char const* string);

#define SH_PRINT(IO, SIG, STRING) sh_print(IO, SIG, __FILE__, __LINE__, STRING)

sh_handler* sh_enable(int sig, sh_handler* hnd);

#endif

_Atomic이 붙은 오브젝트는 해당 기반 타입에 적용될 수 있는 연산에 대한 원자성을 보장받아서 스레드간 레이스 컨디션이 발생하지 않는다. 타입이 락-프리 특성이 있다면 프로세스 명령문에 의해 중단되지 않는다. 후자는 ATOMIC_LONG_LOCK_FREE로 테스트할 수 있다.

요점 정리

  • 비동기 신호나 병렬 스레드가 없는 경우라도 C 코드의 실행 순서는 선형적이라는 보장이 없다. 어떤 결과값들은 컴파일러가 연산 순서를 어떻게 정하냐에 따라 달라진다.
  • setjmp/longjmp는 중첩된 함수 호출에 대한 예외 조건을 다루는 강력한 도구지만, 이들이 변경할 수 있는 변수는 volatile로 보호해야 한다.
  • C의 동기/비동기 신호를 다루는 인터페이스는 극히 제한적이므로 전역 변수에 중단 조건을 기록하는 등의 극히 제한적인 일만 수행해야 한다. 그 이후 중단 조건을 다루는 것은 중단점으로 핸들러가 리턴된 뒤에 수행한다.
  • 신호 핸들러에 의해 전달되거나 전달할 수 있는 정보들은 volatile sig_atomic_t, atomic_flag, 또는 다른 락-프리 원자적 자료형들뿐이다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중