System Call - Signal

Hyungseop Lee·2023년 11월 27일
post-thumbnail

Signal Concepts

  • Signal : are SW interrupts and provide a way to handle asynchronous events
    (비동기식 프로그램을 수행하기 위한 하나의 수단..)

  • HW는 clock에 의해 동기적으로 수행, SW는 왜 비동기적인가?
    OS에서 HW를 추상화시키기 때문에 우리가 program을 짤 때, clock의 개념이 사라진다.

  • Signal이 발생하면, 그 Signal은 해당 Process에게 전달되고,
    그 process는 signal에 대한 action을 취한다.

    • action
    1. ignore it : 하지만 몇몇 signal은 무시되지 못함
    2. catch it : Signal handler를 호출하여 처리
    3. accept the default
      • Ignored
      • Terminated
      • Core dump :
        A core dump file.swp(process의 regiser, memory값들을 작성) is generated,
        and the process is terminated
      • Stopped
      • Resumed

Linux Signals

  • Each signal is defined as a unique integer. (defined in signal.h)
  • See also signal(2)/signal(7) for deatils of the signals
    • Core : Core dump
      Stop : 일시 정지
      Cont : 재개
      Term : 종료
      Ign : 무시
  • 기본적으로 알아야 하는 SIGNAL
    • SIGALRM : 특정 시간이 지나면 알람
    • SIGCHILD : child process가 끝나면 parent process한테 가는 signal
    • SIGSEGV : 잘못된 memory 접근
    • SIGSTOP : process 일시 정지 (= Ctrl + Z)
    • SIGCONT : process가 중지되어 있을 경우, 재개
    • SIGTERM : process 종료 (= Ctrl + C)
  • handler를 등록할 수 없는 signal : SIGKILL, SIGSTOP

Dispatching Signals : Kill(2), raise(3)

  • kill(2) :
    명시적으로 지정한 process에게 signal을 보낼 수 있다.
    kill(pid, SIGTERM)
    • pid > 0 : signal is sent to process with process ID pid
    • pid = 0 : sender와 같은 gid를 갖는 process에게 모두 signal = broadcasting
  • raise(3) : sends a signal to itself

Example

SIGINT = Ctrl + C

  • SIGINT :
  • Ctrl + C :

SIGSTOP = Ctrl + Z

  • SIGSTOP :
  • Ctrl + Z :

SIGQUIT = Ctrl + \

  • SIGQUIT :

  • Ctrl + \ :

SIGKILL = 9

  • kill -SIGKILL <pid> :

  • kill -9 <pid> :

Signal delivery and handler execution


gdb

  • gdb : The GNU Debugger
    • gcc -g : debug용으로 compile하는 옵션
    • gdb ./(실행파일)
    • break = b : breakpoint 설정
    • r : 실행
    • n : 한줄씩 실행 / 그냥 enter : 이전 명령어(n) 실행
    • step = s : 해당 함수로 들어가라
    • list = l : 현재 어디에 있는지 확인하기
    • backtrace :
      (goo함수에 들어왔으니)

      (foo함수의 마지막 코드인 return; 뒤 '}'에서 backtrace)
    • Ctrl + d : debugger 종료

Setting up a Signal Handler : signal(2)

  • signal(2) :
    signal은 UNIX가 개발되었을 때부터 사용되었던 오래된 system call임.
    그런데 thread 같이 새로운 것들이 Linux에 점점 추가되면서
    signal을 더 정교하게 사용해야할 필요가 있어졌다.
    그래서 이제는 signal(2)보다 sigaction(2)을 더 많이 사용한다.

sigaction(2)

  • sigaction(2) :
    signal(2)과 유사하게 해당 signal에 대한 signal handler를 설정하는데,
    더욱 정밀하게 할 수 있다. (thread, Real time signal 등)
    • int signum \to void(*sa_handler)(int) :
      signal handler에 전달되는 값 (SIGINT, SIGKILL 등등)
    • const struct sigaction
      • void (*sa_handler)(int):
        signal handling을 해줄 function의 pointer
      • sigset_t sa_mask:
        signal handling되고 있는 중에, 또 다른 signal(sigset에 지정한 signal들)이 왔을 때 blocking할 수 있도록 막아줌.
        예를 들어, SIGINT에 대한 signal handler를 수행하고 있는데 SIGTERM signal이 들어왔다.
        만약 SIGINT에 대한 signal handling이 중단되기를 바라지 않으면 SIGTERM에 대해서 blocking할 수 있음.
        그리고 SIGINT가 끝나고 나면, 그 다음 SIGTERM을 수행하도록.

signal set(sigset_t)

  • signal을 하나씩 지정해주지 않고, set으로 지정해주고 싶을 때

example

  • SIGINT(ctrl + C), SIGQUIT(ctrl + \) 에 대한 signal handler 등록하기

  • SIGALRM signal에 대한 handler 등록하기


sigprocmask(2), sigpending(2)

  • sigprocmask(2) : blocking할 signal들을 등록
    • int how: const sigset_t *set을 어떻게 등록할 것인가?
      • SIG_BLOCK :
        기존에 blocking 등록된 signal set들과
        현재 *set로 blocking하려는 signal을 합산(union)하여 block
      • SIG_UNBLOCK :
        기존에 blocking 등록된 signal set들 중에서
        현재 *set로 전달해주는 signal만 unblocking하겠다.
      • SIG_SETMASK :
        기존에 blocking 등록된 signal set들을 무시하고,
        새로운 blocking signal set을 만들겠다.
    • sigset_t* oldset:
      기존의 blocked된 signal set을 받아옴
  • sigpending(2) :
    현재 pending되고 있는 signal이 무엇이 있는지 set에 반환.

example

  • sigaction example 1 :
    ➡️ Ctrl+C를 여러번 눌러도 딱 하나만 pending된다.

  • sigaction example 2 :
    왜 (0, 1) 또는 (1, 0)이라는 값이 들어갈까?
    구조체끼리 대입 연산을 할 때, 실제로 한번에 연산되는 것이 아니라
    machine code를 보면 구조체 안에 있는 변수를 각각 대입한다.
    예를 들어
    0 0인 상태에서 1 1을 대입하려고 할 때,
    1 0, 1 1 이러한 순서로 대입이 된다.
    하지만 1 0에서 alarm이 오면 끊고나서, 1 1이 들어가지 않고 1 0이 들어가는 상황이 생기게 된다.
    마찬가지로 반대의 경우 0 1도...

    이러한 오류를 막기 위해서 sigprocmask()를 통해 구조체끼리 대입 연산을 하는 동안 signal을 받지 않도록 해야 한다.


alarm(2)

  • alarm(2) :
    지정한 시간이 지나면, SIGALRM signal이 들어온다.
    alarm(2)은 process에서 딱 1개만 설정할 수 있어서 제약점이 많다.
    그래서 여러 개의 alarm설정, 정밀한 작업을 위해 POSIX interval timer도 만들어졌다.

example


sleep(3), nanosleep(2)

  • sleep(3) :
    지정된 시간 동안 program 실행을 중지
  • nanosleep(2) :
    만약 1초 100ms를 sleep하고 싶다면,
    tv_sec에 1,
    tv_nsec에 100,000,000 (ms = 10310^{-3}, ns = 10910^{-9})

Summary

간단하게 정리하자면
sigaction(2)은 기존의 Signal에 대한 action을 다르게 지정할 수 있는 system call.
sigprocmask(2)는 특정 signal이 들어오는 것을 blocking할 수 있는 system call.


Examples

-SIGINT, Ctrl + C (interrupt)

-SIGSTOP, Ctrl + Z (stop)

-SIGCONT

  • 아래처럼, SIGCONT로 STOP된 process를 실행시킬 수 있지만, terimnal로 입력하는 signal을 받지 못함.
    (STOP된 프로세스는 user-space 코드가 완전히 중단되기 때문에 signal handler를 실행할 수 없다.
    SIGINT/SIGQUIT 같은 터미널 신호는 handler를 실행해야 동작하는데, STOP 상태에서는 handler 실행 자체가 불가능하므로 신호를 받아도 아무 반응이 없다.
    SIGCONT는 커널이 동작을 재개시키는 특별한 신호라서 STOP 상태에서도 즉시 동작한다.)

  • 그래서 kill -SIGKILL 로 중단

-SIGQUIT, Ctrl + \ (core dump)

gdb (break, r, bt, n, list, print)

  • gcc의 -g 옵션으로 디버깅 정보를 포함한 실행 파일을 만들 수 있음.
    • (r)un: 전체 실행
    • (b)reak: 중단 지점 설정하기
    • (n)ext: 다음 명령 실행
    • (l)ist: 현재 내가 어느 코드에 있는지 확인하기
    • (b)ack(t)race: 호출된 function stack 확인하기

gdb 실행파일

  • gdb 실행파일
    • 함수 호출(또는 종료) 직전, 직후를 중단점으로 잡아서 여러 gdb 옵션을 테스트해보겠다.
    • run: 다음 breakpoint까지 명령어 쭉 실행.
      아래에서는 첫 breakpoint가 main()이었기 때문에 main 함수 들어오고 break됨.
      즉, foo() 함수 호출 직전.
    • (b)ack(t)race:
      함수 호출 상태, 즉 스택을 확인.
      아직 main() 함수만 존재.
    • (n)ext:
      다음 명령어를 한 줄씩 실행
      다음 명령어 한 줄을 실행 했더니, 마침 breakpoint였음
      즉, foo() 함수 호출 직후임.

      backtrace로 확인해본 결과, function call stack에 foo()가 push된 것을 확인할 수 있음.
      n으로 쭉 실행하고, goo() 함수 호출 직후로 이동.n으로 쭉 실행하고, goo() 함수 종료 직후로 이동.
      backtrace 결과, stack에서 goo ()가 pop되었음을 확인할 수 있음.
    • 이쯤에서, list로 현재 내가 디버깅 중인 코드 주변 라인을 출력.
      foo() 함수 언저리에 있다는 것을 알 수 있음
    • 이제 n으로 쭉 실행하여 종료
    • Ctrl+D로 gdb에서 quit하기.

gdb 실행파일 [coredump]

  • -SIGQUIT, -SIGSEGV 같은 signal들은 default action으로 core dump file을 생성한다.
    gdb를 이용하여 core dump file을 확인해보자.

core dump file 생성 허가

  • 우선 Linux는 저장공간을 절약하기 위해 default로 core dump file 생성을 막아놨다.
    core dump file 생성을 허가하고, 크기를 제한하고 싶다면 다음 명령어를 사용한다.
ulimit -c unlimited

core dump file 생성

  • -SIGQUIT (Ctrl + \)을 통해 core dump file을 생성하겠다.

  • 이제 core dump file이 생성되었는데, 어디에 있을까?

    • WSL에서는 core dump file이 /mnt/wslg/dumps 에 만들어진다고 함.
    • 내 system에서는 /var/lib/apport/coredump에 만들어졌다.
  • gdb 실행파일_path coredumpfile_path
    아래 화면과 같이, debugging 결과 SIGQUIT signal로 인해 program이 종료되었다는 메시지를 확인할 수 있다.

sigaction(2) - SIGINT, SIGQUIT, SIGALRM에 대한 signal handling 처리

  • sigaction(2):
    어떤 signal을 어떤 handler로 처리할지 지정
    단, SIGSTOP과 SIGKILL에 대한 handler는 설정할 수 없음.

sigprocmask(2), sigpending(2), sigismember(3)

  • sigprocmask(2):
    process가 어떤 signal을 block(mask)할지 설정
  • sigpending(2):
    signal set 주소를 전달해주면, pending되어 있는 signal들에 대해 값을 처리해줌.
    그리고 나서, sigismember(3)를 사용하여 pending된 signal을 확인할 수 있음.
  • sigismember(3):
    sigset_t 안에 특정 signal이 포함되어 있는지 확인

SIGINT blocking & pending시키기

sigprocmask()로 race condition 해결하기

  • data = zeros;와 data = ones; 코드를 무한 루프로 계속해서 수행하다가
    SIGALRM이 들어오면 handler가 구조체의 값을 읽어서 출력하는 예제이다.
    여기서, 아래와 같이 의도한 (0,0) (1,1)이 나오지 않고 (0, 1) 또는 (1,0)이 나오는 경우가 존재한다.
    이 이유는, 구조체 대입은 atomic한 연산이 아니기 때문에 (0,0) \to (1,1)이 바로 되는게 아니라
    (0,0) \to (0, 1) 또는 (1,0) \to (1,1) 이러한 순서로 저장된다.
    이때, (0,1)인 순간에 SIGALRM event 발생으로 인해 handler가 수행 될 때, 원하지 않는 (0,1)이 출력되는 것이다.
    즉, signal handler 호출 중 global variable에 대해 race condition이 일어나서 구조체 값이 깨지는 것이다.

이를 해결하기 위해, sigprocmask를 이용하여 구조체 대입 연산을 atomic하게 만들어 비동기 signal을 blocking할 수 있다.

profile
Efficient Deep Learning

0개의 댓글