시스템 프로그래밍 11-2(Signals and Nonlocal Jumps)

TonyHan·2021년 6월 9일
0

6. Nested Signal Handlers


signal은 중첩이 될 수 있다.

  • Handlers can be interrupted by other handlers

현재 signal을 받고 이 프로세스가 다시 context switch가 되어서 다시 돌아와서 수행되는 시점에 커널이 이 프로세스에게 전달이 되어 있는 시그널이 있는지 확인한다. 그래서 내가 받아야 하는 시그널들이 와 있는지 확인하고 핸들러를 수행해준다.

User next를 수행하기 전에 Handler를 수행한다. 그런데 이 Handler가 길어서 수행하다가 돌아와서 보니 다른 signal이 와 있는 것을 확인할 수 있다. 그럼 이것도 수행해주고 다시 돌아온다.

그래서 이러한 방식으로 signal이 중첩이 될 수 있다. 이것을 보고 nested signal handler이라고 부른다.

만약에 이 코드 안에 global 전역변수가 있다면 handler 들이 다 사용하고 바꿀 수 있다. 그러면 handler가 의도하지 않게 전역변수를 바꾸어서 전역변수가 바뀐 것을 이전 handler는 인식할 수 없다. 그럼 여기에서 발생하는 문제는 concurrency문제이다.

그래서 개발자가 concurrency 문제를 해결해주지 않으면 큰일 날 수 있다.

7. Blocking and Unblocking Signals

  • Implicit blocking mechanism
    • Kernel blocks any pending signals of type currently being handled.
    • E.g., A SIGINT handler can’t be interrupted by another SIGINT

커널이 pending signal들을 blocking 할 수 있다. 만약 SIGINT 핸들러는 다른 SIGINT signal을 받았을 때 수행하지 않도록 blocking할 수 있다. 이런것을 보고 implicit blocking mechanism이라고 부른다.

  • Explicit blocking and unblocking mechanism
    • sigprocmask function

명시적으로 어떤 signal을 blocking, unblocking 하겠다고 하는 메커니즘이다. 이것을 위해 sigprocmask를 사용한다.

  • Supporting functions
    • sigemptyset
      • Create empty set : signal들을 array로 표현해서 비우겠다.
    • sigfillset
      • Add every signal number to set : 모든 signal을 blocking
    • sigaddset
      • Add signal number to set : 특정한 signal 부분들만 blocking
    • sigdelset
      • Delete signal number from set : unblocking하겠다.

Temporarily Blocking Signals

sigint signal을 받지 않겠다는 코드를 짜겠다고 하자

sigset_t mask, prev_mask;

Sigemptyset(&mask); // blocking하고자 하는 signal을 모두 masking
Sigaddset(&mask, SIGINT); //해당하는 signal을 mask해서 blocking 하겠다.

/* Block SIGINT and save previous blocked set */
Sigprocmask(SIG_BLOCK, &mask, &prev_mask); // Sigprocmask를 호출해서 mask를 blocking하는데 이 함수 이전에 있던 signal mask를 잠시 저장하고(prev_mask) mask로 setting
.
./* Code region that will not be interrupted by SIGINT */
. //이때부터는 SIGINT는 모두 blocking 가능

/* Restore previous blocked set, unblocking SIGINT */
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);

//다시 예전것으로 복구해준다. 그리고 sigint는 unblocking

8. Safe Signal Handling

  • Handlers are tricky because they are concurrent with main program and share the same global data structures.
    • Shared data structures can become corrupted.
  • We’ll explore concurrency issues later in the term.
  • For now here are some guidelines to help you avoid trouble.

safe한 signal handling이란 main programe과 concurrent하게 수행되기 때문에 굉장히 tricky하다. 프로세스가 global 변수를 참조할 수 있기 때문에 전역변수는 바뀔 수 있고 다른 프로세스가 이것을 확인할 수 없다.

Guidelines for Writing Safe Handlers

  • G0: Keep your handlers as simple as possible
    • e.g., Set a global flag and return

handler는 최대한 간단하게 작성하자

  • G1: Call only async-signal-safe functions in your handlers
    • printf, sprintf, malloc, and exit are not safe!

async-signal-safe한 함수만 호출하자

  • G2: Save and restore errno on entry and exit
    • So that other handlers don’t overwrite your value of errno

entry와 exit시 global 변수는 임시변수에 저장하고 나중에 복원을 하는 방법을 권장한다.

  • G3: Protect accesses to shared data structures by temporarily blocking all signals.
    • To prevent possible corruption

만약 공유자료구조를 접근한다면 다른 signal을 blocking해서 corruption을 방지

  • G4: Declare global variables as volatile
    • To prevent compiler from storing them in a register

global 변수를 volatile로 생성 -> 컴파일러가 머신코드 생성시 변수들이 레지스터에 생성되는 것을 막는다. 레지스터가 아닌 메모리에 저장을 하게 된다.

  • G5: Declare global flags as volatile sig_atomic_t
    • flag: variable that is only read or written (e.g. flag = 1, not flag++)
    • Flag declared this way does not need to be protected like other globals

글로벌 flag 선언시 sig_atomic_t 타입으로 선언해라. 그러면 이 타입으로 선언된 변수는 데이터가 읽혀지거나 쓰여질때 atomic하게 읽히거나 쓰여진다. 그래서 변수를 쓰는 중간에 쪼개질 수 없게 막는다.

Async-Signal-Safety

  • Function is async-signal-safe if either reentrant or non-interruptible by signals.

함수가 async-signal-safety하다는 것은 시그널을 받았을 때 함수가 중간에 쪼개질 수 없다는 것을 이야기 한다.

  • Posix guarantees 117 functions to be async-signal-safe
    • Popular functions on the list:
      • _exit, write, wait, waitpid, sleep, kill
    • Popular functions that are not on the list:
      • printf, sprintf, malloc, exit
      • Unfortunate fact: write is the only async-signal-safe output function

printf는 부모, 자식 프로세스안에서 사용시 lock을 가지고 있다. 만약 printf가 실행중에 context-switch가 발생하는 것이다. signal handler를 수행중에 printf를 수행하려고 하지만 이미 부모 프로세스가 lock을 하고 있어서 자식 프로세스가 실행하지 못하고 있어서 기다리게 된다. 이것을 보고 Deadlock이라고 부른다.

그래서 117개의 async-signal-safe함수를 사용해서 예기치 않은 버그가 발생하지 않도록 한다.

예를 들어서 write와 printf이다. write는 async-signal-safe하기 때문에 printf 함수를 async-signal-safe함수로 바꾸어 수행가능하다.

Safely Generating Formatted Output

  • Use the reentrant SIO (Safe I/O library) from csapp.c in
    your handlers.
  ssize_t sio_puts(char s[]) /* Put string * /
  ssize_t sio_putl(long v) /* Put long * /
  void sio_error(char s[]) /* Put msg & exit * /
void sigint_handler(int sig) /* Safe SIGINT handler */
{
  Sio_puts("So you think you can stop the bomb with ctrl-c, do you?\n");
  sleep(2);
  Sio_puts("Well..."); //여기가 원래 printf였으면 async하게 바꾸었다.
  sleep(1);
  Sio_puts("OK. :-)\n"); //여기가 원래 printf였으면 async하게 바꾸었다.
  _exit(0);
} sigintsafe.c

Correct Signal Handling

  • Pending signals are not queued
    • For each signal type, one bit indicates whether or not signal is pending…
    • …thus at most one pending signal of any particular type.
  • You can’t use signals to count events, such as children terminating.

fork14는 5개의 프로세스를 띄우는 것이다.

Signal(SIGCHLD, child_handler)을 동록했다. child process가 종료되면 SIGCHLD를 보내게 되고 이 signal을 child_handler가 처리해준다.

child_handler
wait함수를 통해서 child process를 reaping해준다.
child_handler는 async-saft-handler가 출력해준다. 그리고 errno를 미리 olderrno에 저장해 놓아서 나중에 errno가 바뀌는 것을 방지해준다.


그런데 child processer은 단 두개만 reaping되고 나머지는 hanging하고 있다. 왜냐하면 이미 handling을 하고 있는 프로세스가 있다면 후에 중간에 오는 signal은 받지 못한다.

그래서 signal handling함수가 두번만 호출이 된 것이다.

Correct Signal Handling

  • Must wait for all terminated child processes
    • Put wait in a loop to reap all terminated children
void child_handler2(int sig) {
  int olderrno = errno;
  pid_t pid;
  while ((pid = wait(NULL)) > 0) {
    ccount--;
    Sio_puts("Handler reaped child ");
    Sio_putl((long)pid); Sio_puts(" \n");
  }
  if (errno != ECHILD)
  	Sio_error("wait error");
  errno = olderrno;
}

그래서 위의 문제를 해결하기 위해 handler에 while문을 추가한다. while문으로 좀비프로세스가 있는 만큼 wait해서 다른 좀비들을 여러번 체크해서 ccount를 declined한다.

9. Portable Signal Handling

  • Ugh! Different versions of Unix can have different signal handling semantics
    • Some older systems restore action to default after catching signal
    • Some interrupted system calls can return with errno == EINTR
    • Some systems don’t block signals of the type being handled

old system과 같은 경우 signal을 받아 handler가 처리하고 default로 돌아가는 문제점이 있다.

어떤 시스템에서는 interrupt 처리중에 signal을 받아서 돌아올때 error을 발생하는 경우가 존재한다.

read 시스템 콜을 발생하다가 시스템이 그냥 abort해서 error을 발생하는 경우가 존재한다. 이때는 프로그래머가 시스템 콜을 다시 보내는 식으로 문제를 해결할 수 있다.

  • Solution: sigaction

그래서 이러한 문제점을 해결하기 위해 sigaction을 사용하게 된다.

10. Synchronizing Flows to Avoid Races

  • Simple shell with a subtle synchronization error because it assumes parent runs before child.

race error은 굉장히 이상한 synchronization error가 발생하는 경우가 생길 수 있다. 동기화 문제로 error가 발생하는 것이다.

main과 handler가 공유하는 자원이 job queue이다. fork를 할때 Execve를 통해 날짜를 화면에 찍어준다. fork를 하면서 띄어주면서 job queue에 집어넣어준다.

handler는 child process가 끝나게 되면 child process가 main에 메세지를 보낸다. 그럼 handler는 job queue에서 dequeue 한다.

여기에서 한가지 문제가 있는데 코드를 보면
addjob을 할때 signal을 받지 않기 위해 Sigfillset으로 모든 signal을 masking해서 아무것도 안받겠다고 했다.

이후 Sigprocmask(SIG_SETMASK, &prev_all, NULL)로 다시 signal을 받는다. 이게 handler에서도 동일하게 작성되어 있다.

그런데 이게 문제점이 있는것이 addjob과 deletejob중 어떤것이 먼저 실행될지는 아무도 알 수 없다. 그래서 어쩌다 보니 addjob이 deletejob 수행후 발생하며 프로세스가 죽었음에도 불구하고 job queue에는 addjob이 들어와 있을 수가 있다.


  • SIGCHLD handler for a simple shell

waitpid로 기다리다가 deletejob으로 자식 프로세스를 지운다.

Corrected Shell Program without Race

그래서 위의 문제를 해결하기 위해서 Sigemptyset으로 마스크를 하나더 만들고 SIGCHLD만 처리한다.

fork이전에 Sigprocmask로 마스킹으로 SIGCHLD를 blocking한다.
그 이후에 addjob을 한다.

11. Explicitly Waiting for Signals

  • Handlers for program explicitly waiting for SIGCHLD to arrive.

waitpid를 사용해서 명시적으로 pid라는 것을 volatile로 선언하고 return 값을 가지고 pid의 값을 확인하는 방법이 있다.


Explicitly Waiting for Signals

foreground로 실행되다가 끝난경우 while(!pid)이면 계속 기다리고 어떤 값이 넘어오면 그때가서 확인할 수 있게 하는 방법을 사용한다.

그런데 이렇게 하면 cpu cycle을 너무 많이 쓴다.


  • Program is correct, but very wasteful
  • Other options:

pause() 혹은 sleep(1)을 쓴다.

  • pause는 signal을 받으면 깨어난다. pause를 깨우는 경우에는 child process가 종료되어서 돌아올때 작동하기를 원하는데 실재로는 이와 다르게 작동할 수 있다. 만약에 pause를 하기 이전에 SIGCHLD를 받게 되면 signal이 절때오지 않고 프로세스는 계속 기다리는 문제가 생길 수 있다.
  • sleep은 시간이 지나면 깨어난다. pid를 얼마나 잘 체크할 거냐라는 문제가 있을 수 있다.
  • Solution: sigsuspend
    sigsuspend로 문제를 해결할 수 있다.

Waiting for Signals with sigsuspend

  • int sigsuspend(const sigset_t *mask)
  • Equivalent to atomic (uninterruptable) version of:
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

시그널을 안받고 기다렸다가 마스크를 푸는 형태이다.


prev가 이전의 signal들에 대한 information을 복원해준다.

그래서 while(!pid) 부분 덕분에 이미 SIGCHLD를 받는 경우를 막을 수 있게 된다.

profile
신촌거지출신개발자(시리즈 부분에 목차가 나옵니다.)

0개의 댓글