ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Exceptional Control Flow : Signals and Nonlocal Jumps
    시스템 프로그래밍 2022. 3. 29. 01:33
    728x90

    Shell

    리눅스 process의 hierarchy입니다. init에서 시작해 tree구조로 되어있습니다.

     

    Shell이란 user대신에 프로그램을 실행시키는 응용 프로그램입니다.

    즉, 사용자와 OS kernel의 interface로 shell은 OS 에서 사용자의 명령을 받아 읽고 해석해

    Hardware에게 명령을 내리는 것입니다.

    예를 들어 unix shell이나 C shell, bash 등등 이 있습니다.

    shell은 기본적으로 2가지로 실행됩니다. 

    바로 command를 읽는 read 부분과 입력받은 command를 실행하는 evaluate부분 입니다.

     

    Simple Shell

    입력받은 command를 실행(evaluate)하는 부분입니다.

    command에는 총 2가지 유형이 있습니다.

    1) Foreground (포 그라운드)

    입력된 command가 작업을 완료할때 까지 기다리는 방식입니다.

    일반적으로 command가 포그라운드로 입력되면 command를 완료하기 전까지

    다른 command를 입력할 수 없습니다.

    입력 command 예시 : ls -al

     

    2) Background (백 그라운드)

    command를 통해 새로운 process가 실행되는 동안 다른 process도 실행 가능합니다.

    즉, 하나의 shell에서 여러개의 process를 실행하는 방법입니다.

    백 그라운드로 명령어를 실행하면 곧바로 다음 명령어도 실행할 수 있습니다.

    필요한 여러작업을 동시에 작업하면서 포 그라운드로 작업을 진행할 수 있습니다.

    process 작업이 오래걸릴걸 대비해 여러 작업을 동시에 수행할 수 있습니다.

    입력 command 예시 : ls -al& (뒤에다 &을 붙입니다)

     

    command에 ls -al이 입력으로 들어오면

    변수 bg에는 command가 백그라운드인지 포그라운드인지 들어갑니다.

    (pid = Fork()) == 0 //ls를 입력받고 -al이라는 child process 생성

    if(!bg) 백그라운드가 아닐경우, 즉, 포그라운드일 경우 wait 함수를 통해 child process가 exit 하는 동안 

    reaping 해달라는 signal을 기다립니다.

     

    포그라운드는 wait함수를 통해 signal을 기다리면 되지만 백그라운드는 child process가 언제 끝나는지 모릅니다.

    나중에 child process가 끝나면 parent process한테 끝났다고 reaping 해달라고 signal을 보내야 합니다.

     

    Inter-Process Communication

    우리가 shell에서 >ls -al | tail -5 를 입력하면 shell이 두 개의 process를 fork해서 띄우고 

    ls -al의 출력을 tail의 입력으로 넣어서 명령을 수행합니다.

    각 process는 adress space를 가지고 있는 데 서로 공유할 space가 없습니다.

    그래서 process들 끼리 서로 통신하기 위해 Inter-process communication을 만들었습니다.

    이러한 IPC는 OS가 제공해주는데 대표적으로 Pipe가 있습니다.

    즉, process끼리 통신하기위한 장치입니다.

    Process A가 Pipe를 통해 데이터를 Process B에게 전달할 수 있습니다.

    Process A와 B는 서로 다른 address space를 가지고 있어 공유할 space가 없기 때문

    이를 pipe가 해결해줍니다.

     

    Pipe

    한 프로세스를 다른 프로세스에 연결하는 Unidirectional(단일방향) byte stream입니다.

    데이터가 process 한쪽 끝에서 쓰여지면 다른쪽 process 끝에서 읽는 방식입니다.

    구조화된 communcation이 아닙니다 : pipe에 포함된 데이터의 크기, sender와 receiver를 알 수 없습니다.

    pipe에 대한 access는 file descriptor의 reading /writing을 통해 이루어집니다.

     

    예를들어 shell 명령어로 >ps -aux | grep root | tail 이렇게 들어오면

    다음과 같은 pipe를 통해 연결됩니다.

    ps -aux의 output이 grep root의 input으로 들어가고 grep root의 output이 tail의 input으로 들어갑니다.

     

    EX)

    Parent process가 fork를 통해 child는 parent의 동일한 복제본입니다. 

    따라서 fd[0], fd[1]이 같이 복제됩니다. fd는 file descriptor입니다.

    fd[0]은 input을 받는곳이고 fd[1]은 output을 pipe로 보내는 곳입니다.

    먼저 parent 부분을 보면 close(fd[0])을 통해 input으로 받을 수 있는 fd[0]을 닫았습니다.

    어차피 안쓰기 때문입니다.

    그다음 write를 통해 fd[1]에다가 'Hello World\n" 라는 데이터를 12만큼 파이프로 보내고 

    wait함수를 통해 기다립니다. (child process가 exit 할때 까지)

    이제 child 부분을 보면 먼저 fd[1]을 닫습니다. 어차피 안쓰기 때문입니다.

    그 다음 read를 하고 write를 통해 Hello World를 출력해주고 exit 합니다. (parent에게  reaping signal)

     

    Problem with Simple Shell

    위에서 봤던 simple shell은 fore ground 작업은 정상적으로 wait하고 child process reaping이 완료됩니다.

    하지만 back ground 작업은 child process가 정상적으로 reaping 되지 않은 상태에서 parent process가

    계속해서 process를 생성하고 작업할 수 있습니다.

    이러한 경우 child process는 좀비가 될 수 있고, 영원히 reaping 안될 수 있습니다.

    또한 child process는 kernel에 계속해서 상주해 있기 때문에 메모리 누수가 발생 할 수 있습니다.

     

    ECF to the Rescue

    따라서 이러한 문제를 해결할 수 있는 것이 Exceptional Control Flow입니다.

    kernel이 back ground가 완료되면 우리에게 알리기 위해 interrupt를 겁니다.

    Unix에서 이러한 알림 메카니즘을 Signal이라고 합니다.

     

    Signals

    시그널은 process에게 시스템내에서 어떤 event가 발생했다고 알려주는 작은 메시지 입니다.

    시그널은 exception과 interrupt와 매우 유사합니다. 

    시그널은 kernel(때로는 process)로 부터 process로 전송됩니다.

    시그널은 종류마다 정수형으로 ID가 지정되어있습니다. 

    ID가 2번인 SIGINT는 우리가 Ctrl + c를 눌렀을때 발생하는 signal입니다. 기본으로 process를 종료시킵니다.

    ID가 17번인 SIGCHLD는 Child process가 멈추거나 종료됐을 때 발생하는 signal입니다. 기본으로 ignore합니다.

    등등

     

    Kernel은 destination process(signal을 받는 process)의 context 상태를 업데이트 해줌으로써

    destination process에게 signal을 보냅니다.

    밑에는 sinal을 보내는 이유입니다.

    ex)

    0으로 나누라는 event가 발생했을 때 (SIGFPE)

    child process를 종료할때 (SIGCHLD)

    kill command를 사용할 경우

     

    Signal Concepts : Receiving a Signal

    시그널이 sent(deliverd)된 상태와 received 된 상태는 다릅니다.

    sent(delivered) 된 상태는 Kernel이 process에게 signal을 보낸상태지만 process는 받기만 한 상태입니다.

    received된 상태는 process가 signal을 받아 수행(react)하는 것입니다.

     

    process가 signal을 received하면 다음과 같은 행동을 할 수 있습니다.

    • Ignore the signal (signal을 무시하는 것입니다. 즉, 아무것도 하지 않습니다.)
    • Terminate the process (process를 종료)
    • signal handler라는 user-level function을 실행함으로써 signal을 catch합니다.

    signal handler는 앞서 배웠던 exception handler의 일종인 asynchronous interrupt와 유사합니다.

    Signal Concepts : Pending and Blocked Signals

    시그널은 처리하는 방식은 두 가지로 나눌 수 있습니다.

    1) Pending

    시그널이 pending 상태인것은 sent되었지만 received되지 않은 상태입니다.

    Pending은 "not queued" 즉, 시그널 중첩이 되지않는다는 의미입니다.

    시그널 ID마다 최대 1개씩 Pending 상태일 수 있고 동일 시그널이 오면 버립니다.

     

    2) Block

    signal이 delivered 되었지만 process가 의지로 received하지 않은것을 의미합니다. 

    signal이 unblocked 될때 까지 received하지 않을 수 있습니다.

    이로 인해 어떤 시그널에 대해서 process는 선택적으로 received할 수 있고 또는 blocking이 가능합니다.

     

    Signal Concepts : Pending / Blocked Bits

    pending과 block은 Kernel에 의해 수행됩니다.

    kernel이 수행할 때는 pending bit 벡터와 block bit 벡터를 사용합니다.

    모든 bit 벡터는 0으로 초기화되있습니다.

     

    1) Pending

    시그널 k가 delivered 되면 kernel이 pending bit 벡터에 k번째 bit를 1로 set(설정) 해놓습니다.

    시그널 k가 received 되면 kernel이 pending bit 벡터에 k번째 bit를 clear(지우기)해놓습니다.

     

    2) Blocked

    sigpromask function을 사용하여 set기능과 clear기능을 작업을 할 수 있습니다.

    미리 block bit 벡터의 k번째 bit를 1로 설정하여 signal을 received안할 수 있습니다. 

     

    Sendign Signals : Process Groups

    하나의 process 그룹에는 여러개의 process들이 있을 수 있습니다.

    모든 process들은 1개의 process 그룹을 가집니다.

    pid는 process의 ID이고 pgid는 process가 속한 process 그룹의 ID 입니디ㅏ.

    getpgrp() : 현재 process의 process 그룹 ID를 return합니다.

    setpgid() : process의 그룹을 변경합니다.

    1) forks 16이라는 파일을 실행시커 child process 2개를 생성하였습니다.

    pid를 살펴보면 서로 다르지만 pgrp를 보면 둘이 같습니다.

    2) ps명령어를 통해 현재 PID 24818, 24819가 돌고 있는것을 확인했습니다.

    만약 여기서 /bin/kill -9 24818 라는 command를 입력하면 

    24818이라는 process에게 SIGKILL이라는 signal을 보내라는 의미입니다.

    내부적으로는 24818이라는 process context에 위치한 pending bit 벡터에 kernel이 SIGKILL의 bit에

    set 해줍니다. 이제 24818 process를 실행할때 context switching이 일어나면 kernel이 pending bit 벡터를 확인해

    kill 해줍니다.

    3) 만약 process 그룹을 kill해 그룹에 들어있는 모든 process를 kill하고 싶으면

    /bin/kill -9 -24817을 해주면 SIGKILL 시그널을 24817에 해당하는 그룹안에 있는 모든 process에게 보냅니다.

     

    Sending Signals from the Keyboard

    키보드를 이용해 signal을 부를 수 있습니다.

    예를 들어 Ctrl + c 혹은 Ctrl + z가 있습니다.

    Ctrl + c를 누르게 되면 kernel이 SIGINT라는 시그널을 Foreground로 돌아가는 모든 process에게 전달합니다.

    SIGINT의 기본 action은 process 종료(terminate) 입니다.

    Ctrl + z를 누르게 되면 kernel이 SIGTSTP라는 시그널을 Foreground로 돌아가는 모든 process에게 전달합니다.

    SIGTSTP의 기본 action은 process 중단(suspend or stop)입니다.

    fork를 통해 child, parent를 만듭니다.

    그리고 Ctrl + z를 입력하고 커멘드 라인에 ps w를 입력하면 현재 process의 상태를 보여줍니다.

    밑줄친 곳을 보면 현재 T의 상태입니다. T는 stopped상태입니다.

    커멘드라인에 fg를 입력해 다시 멈춰있던 process를 실행시켜줍니다.

    이제는 Ctrl + c를 입력하고 커멘드 라인에 ps w를 입력하면 

    child, parent둘다 없어진것을 볼 수 있습니다. 왜냐하면 Ctrl + c의 기본 action은 terminate이기 때문입니다.

     

    Receiving Signals

    시그널 처리 과정입니다.

    Kernel은 pnb를 계산합니다. pnb = pending &(AND) ~blocked 입니다.

    if(pnb == 0) 

    pnb가 0이라는 의미는 pendig과 non blocked 간의 어떤 bit도 둘다 1인것이 없다는 의미입니다.

    따라서 그냥 아무런 action없이 다음 code를 실행합니다.

    else

    반면에 pnb가 어느 한 bit가 1이라면 pnb의 0이 아닌 bit에 해당하는 signal을 receive하고 

    action을 합니다.

    다 처리하였으면 다음 code를 실행합니다.

     

    Signals Handlers as Concurrent Flows

    signal handler가 작동하는 그림을 보면 마치 process라고 착각할 수 있습니다.

    process A에서 signal handler가 실행되는것은 process A안에 있는 어떤 signal handler가

    작동하는 것입니다.

    즉, signal handler는 process가 아닙니다.

     

    Nested Signal Handlers

    signal handler는 중첩될 수 있습니다. 

    (1)번 시점에서 어떤 interrupt가 생겨 context switch가 생겨 다시 돌아오는 시점에서 signal이 왔는지 확인합니다.

    그 다음 pending bit나 bolck bit를 확인하고 이에 맞는 handler를 수행하고 돌아가려 할때 (3)번에서 

    또 다른 signal이 와 있는것을 확인할 수 있습니다. 그럼 또 다시 signal handler를 수행합니다.

    이런식으로 signal은 중첩될 수 있습니다. 

    하지만 중첩이 되면 문제점이 발생할 수 있습니다.

    예를들어 전역변수 a를 Handler S가 1로 설정했다고 하면 (3)에서 다른 signal이 들어와 Handler T가 a를 2로

    바꿀 경우 다시 (5)번 처럼 돌아왔을 때 a = 2인 상태이므로 code상에서 제대로 작동하지 않을 수 있습니다.

     

    Blocking and Unblocking Signals

    Implicit blocking mechanism

    kernel이 handler에 의해 처리되고 있는 모든 대기 signal들을 blocking할 수 있습니다.

    예를 들어 SIGINT handler는 다른 SIGINT signal를 받았을때 수행하지 않도록 blocking할 수 있습니다.

     

    Explicit blocking and unblocking mechanism

    sigpromask funtion을 사용하여 명시적으로 어떤 signal을 blocking 또는 unblocking하는 것입니다.

    #include <signal.h>
    
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);

    sigemptyset : set 비움

    sigfillset : 모든 signal을 set에 추가 (모든 signal을 blocking)

    sigaddset : signum을 set에 추가 (signum에 해당하는 signal을 blocking)

    sigdelset : signum을 set에서 지움 (signum에 해당하는 signal을 unblocking)

     

    EX) SIGINT signal을 받지 않겠다는 code

    마지막 줄에는 Sigpromask함수를 이용해 unblocking해줍니다.

     

    Safe Signal Handling

    위에서 봤던것과 같이 signal handler가 중첩되면 여러문제가 생길 수 있습니다.

    따라서 우리는 안전하게 signal handler를 작성할 필요가 있습니다.

     

    Guidelines for Writing Safe Handlers

    G0 : handler는 최대한 간단하게 작성

    G1 : handler에서 async-signal-safe한 함수만 호출

    예를 들어 printf, sprintf, malloc and exit 함수들은 safe하지 않습니다.

    G2 : "errno"를 저장하고 복원

    많은 async-signal-safe function은 에러를 가지고 리턴할 때 전역변수 errno를 설정합니다.

    때문에 errno에 접근하는 프로그램의 다른 부분에서 문제가 생길 수 있습니다.

    핸들러는 진입할 때 errno를 지역변수에 저장하고, 핸들러가 리턴하기 전에 errno을 복원해야 합니다.

    G3 : 임시적으로 모든 signal을 block시켜 전역적으로 사용하는 자료구조들을 보호 

    G4 : 전역변수를 volatile 형식으로 선언

    volatile int a;

    컴파일러가 machine code를 생성시에 변수들이 register에 생성되는 것을 막습니다.

    register가 아닌 memory에 저장하게 됩니다.

    G5 : volatile형식으로 sig_atomic_t를 전역변수로 설정

    volatile sig_atomic_t flag;

    이러한 flag변수는 데이터가 읽혀지거나 쓰여질때 atomic하게 읽히거나 쓰여집니다. 

    따라서 변수를 쓰는 중간에 쪼개질 수 없게 막습니다.

     

    Async-Signal-Safety

    함수가 async-signal-safe하다는 의미는 signal에의해 재 진입하거나 interrupt가 불가능한 경우입니다.

    즉, signal handler에 의해 안전하게 호출할 수 있습니다. 

    _exit, write, wait, waitpid, sleep, kill

    위의 함수는 async-signal-safe function들입니다. 이외에도 총 117개의 function이 존재합니다.

    printf, sprintf, malloc, exit

    위의 함수는 대표적인 async-signal-safe function이 아닙니다.

    예를들어 printf는 main함수에서 사용시 lock을 가지고 있습니다. 만약 printf사용 도중 context switch가 일어나면

    signal handler가 printf를 수행하려하지만 이미 main함수에서 lock을 하고 있어 다른 process에서

    printf를 실행하지 못하고 기다리게 됩니다. 이러한 현상을 Deadlock(교착상태)라고 합니다.

    이러한 문제점을 보완하고자 asnc-signal-safety함수를 사용하여야 합니다.

     

    예를들어 csapp.c에 있는 SIO(Safe I/O library)를 살펴봅시다.

    ssize_t sio_puts(char s[]) //문자열 출력
    ssize_t sio_putl(long v) //long 출력
    void sio_error(char s[]) //에러 메시지 출력 & exit

    왼쪽의 code를 오른쪽의 code로 안전하게 바꿔주었습니다.

     

    Correct Signal Handling

    위의 code에서 fork 14가 main함수라 하면 N = 5라고 가정하자

    5개의 child process를 만들고 child process가 exit하면 Kernel에게 SIGCHLD라는 singnal을 보내 signal handling

    이 적용되어야 합니다.

    따라서 ccount는 signal handling (위의 코드에서는 child_handler 함수)이 발생할 때마다 ccount가 --되어서

    결국 0이 되면 while문이 끝나면서 완전히 종료되게 되있습니다.

    하지만 실행한 결과 handler는 2번만 실행하고 무한 while 루프를 돌고 있는 모습을 보여줍니다.

    이는 pending에 의한것입니다. pending은 signal 중첩을 허용하지 않습니다. 따라서 만약 child process 2가 

    handling하고 있는 와중에 또 child process 3이 종료되어 똑같은 signal이 SIGCHLD가 보내지면

    무시하는 것입니다. 

    이러한 code는 수정할 필요가 있습니다.

    child_handler를 child_handler2로 수정하였습니다. 

    자세히 보면 while문을 통해 좀비 process를 여러번 체크해 signal 중첩이 안일어 나게 합니다.

    실행결과 정상적으로 reaping 되었습니다.

     

    Portable Signal Handling

    Unix의 다른 version에서는 서로 다른 signal handling을 사용할 수 있습니다.

    old system의 경우 signal handling을 처리하고 다시 원래 상태로 돌아가 signal이 발생할 때마다 

    signal handling을 작성해줘야하는 경우가 있습니다.

    어떤 시스템에서는 interrupt 처리중에 또 signal을 받으면 error를 발생하는 경우가 있습니다.

    위와 같은 호환성 문제 때문에 sigaction함수를 정의합니다.

    위와 같은 래퍼함수를 이용합니다.

     

    Synchronizing Flow to Avoid Races

    main함수와 handler는 공유하는 자원은 job queue입니다. 자료구조 queue형태로 되있으면서 

    처리된 일은 큐에서 delete하고 새로운 일은 큐에 insert해줍니다.

    아래에 code에 의하면 fork함수를 실행할 때마다 child process가 생성된 date를

    job queue에 insert해줍니다.

    main함수에서 Sigfllset을 통해 들어오는 signal을 block하기 위해 setting을 해줍니다.

    그 다음 while문을 보면 fork함수를 통해 child precess를 생성해 줍니다. 

    addjob 하기 전에 Sigpromask를 통해 사전에 미리 signal이 들어오는것을 대비해 모두 block처리합니다.

    그 다음 addjob을 하고 Sigpromask를 통해 이제 signal을 받을 수 있게 설정해줍니다.

    handler 함수부분입니다. 마찬가지로 위의 code와 비슷하지만 여기서는 deletejob을 해줍니다.

     

    여기서 문제점이 발생합니다. parent와 child가 누가 먼저 생성되고 없어지는지는 control할 수 없습니다.

    예를들어 child 가 먼저 생성되고 종료되면 signal을 보내 handler가 작동합니다. 근데 job queue에는

    addjob 함수가 아직 안들어와 있는 상태이기 때문에(parent 영역이기 때문) 아무것도 없는 queue에 delete를

    하게 됩니다. 그 다음에 parent가 addjob을 하게 되면 이미 delete를 했으므로 그냥 넘어갈 수 있습니다.

    이런식으로 에러가 반복될 수 있습니다.

    이를 방지하기 위해 Sigemptyset을 통해 하나의 signal set을 비우고 

    Sigaddset을 통해 SIGCHLD signal을 받지 않겠다는 set을 해줍니다.

    그리고 while문 들어가서 바로 Sigpromask를 통해 set된 signal을 block처리해줍니다.

    그렇게 되면 child가 종료되어도 signal handler가 작동안하기 때문에

    무조건 parent영역의 addjob이 먼저 실행됩니다. 

    따라서 add -> delete 가 보장됩니다.

     

    Explicitly Waiting for Signals

    sigchild_handler입니다. waitpid를 통해 PID의 번호를 확인할 수 있습니다.

    foreground로 실행된다고 가정하면 child가 종료될때 까지(SIGCHLD를 수신할 때까지)

    whild(!pid) 문이 계속해서 무한 루프를 돌고 있습니다.

    이렇게 되면 cpu cycle을 너무 많이 소비하게 됩니다.

    이를 방지하고자 pause() 또는 sleep(1)을 사용합니다. 

    pause는 signal을 받으면 깨어납니다. 

    하지만 이러한 경우 pause 상태이어야만 유효합니다. 만약 pause상태가 이기전에 signal을 받아버리면 

    영원히 while문을 돕니다.

    sleep은 특정한 시간이 지나면 스스로 깨어납니다. 이러한 code는 너무 불확실합니다.

     

    이를 보완해 sigsuspend함수가 나왔습니다.

    int sigsuspend (const sigset_t *mask)

    위 3줄의 코드를 압축한 형태입니다.

    미리 signal들어올 가능성을 sigpromask를 통해 block해두고

    pause상태를 겁니다.

    pause상태가 풀리면 다시 sigpromask를 통해 block된 signal을 풉니다.

    다시 수정한 code입니다.

    '시스템 프로그래밍' 카테고리의 다른 글

    Concurrent Programming  (0) 2022.04.16
    Network Programming : Part 2  (0) 2022.04.14
    Network Programming : Part 1  (0) 2022.04.12
    System-Level I/O  (0) 2022.04.05
    Exceptional Control Flow : Exceptions and Processes  (0) 2022.03.10
Designed by Tistory.