System Programming 15. Exception Control Flow II

Layfort·2023년 11월 7일
0

System Programming

목록 보기
1/5

Signal: message from system

  • Ch14에서 우리는 시스템의 Exceptional Control Flow에 어떤 종류가 있는지를 배웠다.
    • Trap(System call)
    • Interrupt
    • Abort
    • 이러한 Exception은 대게 hardware 상에서 instruction 단위로 처리됨...
  • Signal은 이러한 Exceptional Control Flow의 software virtualization이다.
    • Kernel to user process
    • System상에서 일어난 일종의 event를 user process로 전달하는 역할

1. Signal Concepts

  • 일반적으로 Signal은 2단계를 거쳐서 처리된다.
    1. Sending a signal : 시그널은 2가지 이유로 process로 전해진다.
      1. System이 event를 감지했다.(주로 Hardware가 감지)
      2. User가 signal sending systemcall을 실행 시켰다.(kill)
    2. Receiving a signal : 시그널이 전달되면, process는 kernel mode로 돌입하여 이를 처리하고자 한다. 이때, Signal Handler라고 불리는 kernel 함수가 이를 처리한다.
  • 보냈으나 아직 process에서 처리하지 않은 signal을 pending signal이라고 한다.

1-1. Signal Deilvery and Reception

1-1-1. Sending Signal

  • /bin/kill, /usr/bin/killall, Ctrl + C, Ctrl + z, kill, alarm등 다양한 방법이 있음.

    • 또는 System(또는 hardware)에서 event가 발생한 경우(e.g. Divide by Zero, Overflow, page fault, segmentation fault)
  • 따라서 기본적으로 signal은 asynchronous하다.

    • 하지만 결국 이 signal들은 한번에 처리되긴 한다.(return from kernel)
  • Signal은 queue되지 않는다!(중첩은 된다.)

    • 예를 들어... 1번 Signal이 1번 보내지든 2번 보내지든 10000번을 보내지든, process는 이를 알 수 없다.(그저 시그널이 발생했다 정도만 알 수 있다.)

1-1-2. Receiving Signal

  • Signal은 kernel에서 user mode로 switch되는 과정에서 처리된다.(context swtich, syscall return, exception handling)
  • 시그널의 존재여부는... if (pending_signal & ~blocked_signal)로 검사한다.
inside singal handler ...

while(pending & ~blocked) {
	int n = get_least_sig_num(pending & ~blocked); // get signal number(least signal)
    do_signal_action(n); // handle signal
    pending = pending & ~(1 >> n); // clear nth-bit signal
}

return to process

1-2. Signal Handling

  • 일단 signal이 감지되면, user process는 다시 kernel로 돌아가야 한다.(call Signal handler)
  • 이때 각 signal에 대해서 process는 3가지 방식으로 반응할 수 있다.
    1. 시그널을 받아들인다.(catching signal)
    2. 시그널을 막는다.(block signal) - blocked_signal의 역할이 이렇게 block된 signal을 걸러내는 것이다.
    3. 시그널을 무시한다.(ignore signal)
    • 이때, signal을 받아들이기로 했다면, 여기서 또다시 어떻게 처리할 지를 고민해야 한다.
  • 어떻게 처리할 것인가?(Handler의 동작 - default action)
    1. terminate/core : terminate process/core dump
    2. ignore : 그냥 무시된다.(do nothing)
    3. stop/continue : SIGCONT signal이 올때까지 대기한다.
    • 다만, 이러한 default action말고 user가 action을 define할 수 있다.(Signal API에서 보다 자세하게 다루겠다.)
      • sighandler_t signal(int signum, sighandler_t handler);
      • 여기에도 예외가 있어, SIGKILL과 SIGSTOP은 default action을 수정할 수 없다.
    • 일이 끝나면 어디로 돌아가는가? - 일반적으로는 I_next로 돌아간다.
  • 위의 그림은 개념적인 수준에서 Signal Handler Call을 설명하고 있다.

1-3. Nested Signal Handlers

  • Signal Handler도 user code다. (default가 아닌 경우)
    • 이 경우, handler 수행중에도 context가 switch될 수도 있다.
    • 따라서 Handler A 수행중에 Switch 되고 왔는데, 또다시 Handling을 해야하는 경우가 충분히 있다!
    • 이런 경우를 우리는 Nested Signal Handling이라고 부른다.
  • 내부적으로 Handler가 signal을 발생시키는 코드
#include <stdio.h>
#include <signal.h>

int beeps = 0;

/* SIGALRM handler */ 
void handler(int sig) 
{
    printf("BEEP\n");
    fflush(stdout);

    if (++beeps < 5) {
        alarm(1);
    }
    else {
        printf("BOOM!\n");
        exit(0);
    }
}

main() 
{
    signal(SIGALRM, handler); 
    /* send SIGALRM in 1 second */
    alarm(1); 
    
    while (1) {
    /* handler returns here */
    } 
}
  • 이 코든 Beep을 5번 호출하고 터진다. 주의할게 바로 beep이 global variable이라는 점이다. 여기서는 그럴 일이 없을 정도로 간단한 코드이지만, 완전히 안전하다고는 못한다.

1-4. Concurrency Issue in 'Nested Signal Handlers'

프로그래머 입장에서, Nested Signal Handlers는 조심히 다루어야한다

  • 만약, 예를 들어 Handler A와 Handler B가 동일한 Global Variable을 접근한다고 해보자.
    • 의도적이지 않은 Rewrite, Overwrite가 일어날 수 있다.
    • 프로그램 Logic 상에서 핸들러 A가 전역 변수를 취급하면서 수행되고 있다가 갑자기 핸들러 B가 동작하여 전역 변수의 값을 오염시킬 수 있는 것이다.
      • 핸들러 A 입장에서는 이 '오염'을 알아챌 방법이 없다.
  • 이를 'Concurrency' 문제라고 한다. 프로그래머가 'Concurrency Handling'을 잘 해야한다.

2. Signal API

2-1. Process Group

  • Shell상에서 process는 다음과 같은 구조를 지닌채로 동작한다.
    • 모든 process는 한개의 group을 지닌채로 동작한다.
    • 자식은 부모와 같은 process group에 속한다.
    • shell은 이런 process group을 job이라는 struct를 통해서 virtualize한다.

2-2. /bin/kil

  • Usage : /bin/kill -[SIGNUM] -[PID]
    • default SIGNUM은 15번(SIGTERM - 안전하게 종료)이다.
    • 강제종료를 원한다면, 9(SIGKILL)을 고려할 것(SIGKILL은 수정불가능 강제종료)
  • 해당 PID가 속하는 process group의 모든 process에 signal을 보낸다...

2-3. Keyboard Signal

  • 우리 모두가 사랑해 마지않는 Ctrl + CCtrl + Z
    • Ctrl + C : SIGINT를 모든 foreground job에게 보낸다.
    • Ctrl + Z :SIGTSTP를 모든 foreground job에게 보낸다.

2-4. kill function

#include <signal.h>

int kill(pid_t pid, int sig);
  • pid

    1. >0 : send signal to pid
    2. =0 : send signal to all process in current process's pgrp
    3. =-1 : send signal to all process
    4. <-1 : send signal to all process in -pid's pgrp
  • sig

    1. >0 : signal to send
    2. =0 : test signal(no signal send, just test)
  • return value < 0,indicate error occur

2-4-1. Example

int main(int argc, char *argv[])
{
    pid_t pid[N];
    int i, child_status;
    
    /* child process를 N개 생성합니다. */
    for (i = 0; i < N; i++) {
    	if ((pid[i] = fork()) == 0) {  // child sleep for certain amount of time
        	sleep(i/2);
            return 100 + i;
        }
        else {
        	printf("Created process %d\n", pid[i]);
        }
    }
   
   for (i = 0; i < N; i++) {			// parent: send SIGINT to half of children
    	printf("Killing proecess %d\n", pid[i]);
        kill(pid[i], SIGINT);
    }
    
    for (i = 0; i < N; i++) {			// parent: wait for children to terminate;
    	pid_t wpid = wait(&child_status);
        if (WIFEXITED(child_status)) {
        	printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));  
        }
        else {
        	printf("Child %d terminated abnormally\n", wpid);
        }
    }
}

2-5. Register Signal Actions

2-5-1. Using sigaction()

#include <signal.h>

struct sigaction {
	void		(*sa_handler)(int);	// handler functionn
    sigset_t 	sa_mask;
    int			sa_flags;
}

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • sigaction() argument

    • int signum : handler를 등록할 signal
    • sigaction act : handler 포인터
    • sigaction oldact : 이전 handler 반환용... (NULL이면 그냥 무시)
  • struct sigaction

    • sa_handler: handler function pointer 또는 SIG_IGN/SIG_DFL로 무시/기본으로 전환 가능
    • sa_mask: block할 signal set 설정
    • sa_flag: 각종 flag들...

2-5-2. Using signal()

#include <signal.h>

typedef void (*sighandler_t)(int);

int signal(int signum, sighandler_t handler);
  • sigaction() argument
    • int signum : handler를 등록할 signal
    • sighandler_t handler : handler 포인터, SIG_IGN/SIG_DFL로 무시/기본으로 전환 가능

2-5-3. Example

void handler(int sig) {
  static int level = 0;

  level++;
  printf("[nesting level %d] Hohoo, got signal %d\n", level, sig);
  sleep(3);
  level--;
}

void alarm_hdl(int sig) {
  printf("Wake up!\n");
  alarm(5);
}

int main(int argc, char *argv[]) {
  struct sigaction action;

  action.sa_handler = handler;
  sigemptyset(&action.sa_mask); 				// allow nested signals
  action.sa_flags = SA_RESTART | SA_NODEFER; 	// restart syscalls if possible

  if (sigaction(SIGINT, &action, NULL) < 0) {
  	perror("Cannot install signal handler"); return EXIT_FAILURE;	// SIGINT 등록
  }

  if (signal(SIGALRM, alarm_hdl) == SIG_ERR) {
  	perror("Cannot install signal handler"); return EXIT_FAILURE;	// SIGALRM 등록
  }

  alarm(5);
  while (pause() == -1);						// wait until receive signal...

  return EXIT_SUCCESS;
} 

2-6. Blocking/Unblocking signal

2-8. Total example code

void int_hdl(int sig) { printf("Received SIGINT\n"); }

void alarm_hdl(int sig) {
  sigset_t set;
  char sigset[32];
  
  if (sigpending(&set) >= 0) {
  	for (int i=1; i<32; i++) sigset[i] = (sigismember(&set, i)) > 0 ? 'P' : '.';	// count which signal is pended now
    sigset[0] = ' '; sigset[31] = '\0';
    printf("Pending signals: %s\n", sigset);
  }
  
  alarm(1);
}

int main(int argc, char *argv[]) {
  sigset_t set;				// blocking signal set variable
  sigemptyset(&set);		// make empty set
  sigaddset(&set, SIGINT);	// add signal to set
  
  if (signal(SIGALRM, alarm_hdl) == SIG_ERR) { perror("Cannot install signal handler"); return EXIT_FAILURE; }
  if (signal(SIGINT, int_hdl) == SIG_ERR) { perror("Cannot install signal handler"); return EXIT_FAILURE; }
  
  alarm(1);
  
  struct timespec delay = { .tv_sec = 5, .tv_nsec = 0 };
  while ((nanosleep(&delay, &delay) < 0) && (errno == EINTR));
  
  if (sigprocmask(SIG_BLOCK, &set, NULL) == 0) printf("SIGINT is now blocked.\n");
  
  delay.tv_sec = 5; delay.tv_nsec = 0;
  while ((nanosleep(&delay, &delay) < 0) && (errno == EINTR));
  
  if (sigprocmask(SIG_UNBLOCK, &set, NULL) == 0) printf("SIGINT is now unblocked.\n");
  
  delay.tv_sec = 5; delay.tv_nsec = 0;
  while ((nanosleep(&delay, &delay) < 0) && (errno == EINTR));
  
  return EXIT_SUCCESS;
}

3. Signal Handling Issue

3-1. Reaping Children

#define N 16
volatile int counter = 0;

void handler(int sig) { // signal handler
  int child_status;
  pid_t wpid = wait(&child_status);
  
  if (WIFEXITED(child_status))
  	printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
  else
  	printf("Child %d terminated abnormally\n", wpid);
    
  counter++;
}

int main(int argc, char *argv[]) {
  pid_t pid[N];
  if (signal(SIGCHLD, handler) == SIG_ERR) return EXIT_FAILURE; // register signal handler to SIGCHLD(send signal to parent process when child process terminated or stopped)
  
  printf("Creating %d children...\n", N);
  for (int i = 0; i < N; i++) {
  	if ((pid[i] = fork()) == 0) { // child: sleep for some amount of time
    	sleep(i/2);
    	return 100+i;
  	}
  }
  
  for (int i = 0; i < N; i+=2) { // parent: send SIGINT to half of children
    printf("Killing process %d\n", pid[i]);
    kill(pid[i], SIGINT);
  }
  
  printf("Waiting until all %d children have terminated...\n", N); // parent: wait for termination
  while (counter < N) {
  	sleep(1);					// sleep is syscall... so, return from sleep, process will handle(not all, but most case signal handled in here)
  	printf("--> %d / %d terminated.\n", counter, N);
  }
  
  return EXIT_SUCCESS;
}
  • 그래서 문제가 뭐임? : signal은 queue되지 않는다는 것...
    • parent process가 실행중이지 않은 상황에서 child 여러명이 동시에 terminate 되면...
    • signal이 겹쳐서 들어와도 parent process는 signal 처리를 한번만 한다.
  • 그래서 어떻게 해결할건데?
    • (wpid = waitpid(-1, &child_status, WNOHANG)
    • signal을 돌릴때마다 모든 child에 대해서 terminate된 child를 탐색한다...
void handler(int sig) { // signal handler
  int child_status;
  pid_t wpid;
  
  while ((wpid = waitpid(-1, &child_status, WNOHANG)) > 0) { // solution
    if (WIFEXITED(child_status))
      printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
    else
      printf("Child %d terminated abnormally\n", wpid);

    counter++;
  }
}

3-2. Interrupted System Calls

  • Syscall 수행중에 signal이 발생되는 경우
    • signal은 next instruction으로 넘어간다는 것을 명심하라!
struct sigaction action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;

if (sigaction(SIGINT, &action, NULL) < 0) { perror("Cannot install signal handler"); return EXIT_FAILURE; }

//
// Read shell command
//

char prompt[] = "> ";
char cmdline[1024];
int res;

write(STDOUT_FILENO, prompt, strlen(prompt));

res = read(STDIN_FILENO, cmdline, sizeof(cmdline)); //만일 read 실행중 interrupt가 발생하면?

if (res >= 0) {
  cmdline[res] = '\0';
  printf("Executing command: %s", cmdline);
} else {
	perror("Error reading command");
}

3-2-1. Manually Restart

while ((res = read(STDIN_FILENO, cmdline, sizeof(cmdline))) < 0) {
	if (errno != EINTR) break;
}
  • EINTR는 interupt에 의해 방해받았다는 의미의 errno이다.

3-2-2. Automatic Restart

action.sa_flags = SA_RESTART;
  • restart flag이다. interupt된 syscall을 다시 실행시키라는 flag

3-3. Concurrency Races

  • 이건 굳이 예시를 안봐도 알수 있는 것이...
    • data처리를 하다가 갑자기 transition되서 다른 handler에서 또 처리하던 data를 내가 건드리면 이건 문제가 꽤 심각해진다...

3-4. Guidelines for Writing Safe Handlers

  1. Handler는 간단하게!

  2. Handler 실행중에도 scheduling 될 수 있음을 명심하자(Async-signal-safe)

    • Async-signal-safe: functions that can be safely called from signal handlers
    • List of async-signal-safe functions: man signal-safety
  3. errno를 저장하고 복구하자

    • 다른 handler에서도 errno가 건드려질 수 있다...
  4. 전역 변수는 volatile로 선언 하자

    • volatile은 변수를 register에 저장하지 않기 때문에 언제나 최신으로 불러올 수 있다.
  5. Shared resource에 접근하고자 한다면 모든 signal을 막아서 data corruption을 막아야 한다.

  6. volatile sig_atomic_t을 사용하자.

    • atomic read/write 연산을 보장한다. e.g. flag = 1, if (flag == 0)
    • 주의 사항: flag += 1 or flag++ 은 atomic하지 않다!

cf) linux signal table

profile
물리와 컴퓨터

0개의 댓글