Signal Handler

COZYHAMA·2024년 5월 5일
0

Operating System

목록 보기
5/5
post-thumbnail

정의

시그널 핸들러(signal handler)란 운영 체제로부터 특정 유형의 시그널을 받았을 때 해당 시그널에 대응하는 사용자 정의 함수나 프로세스의 일부를 말한다. 시그널은 프로그램의 정상적인 흐름을 방해하는 비동기적인 이벤트들로, 주로 예외 상황, 시스템 요청, 또는 다른 프로그램과의 상호 작용 과정에서 발생한다. 시그널 핸들러는 이러한 시그널에 대해 특정 동작을 실행하도록 프로그래밍할 수 있어, 프로그램이 자체적인 방식으로 예외를 처리하거나 필요한 청소 작업을 수행할 수 있게 한다.

문제

터미널 2개를 열고, 터미널 1에서 CTRL+C를 누르면 확인을 받고 종료하는 프로그램.
터미널 2에서는 터미널 1에서 구동적인 프로그램으로 시그널 전달.
터미널 1의 프로그램은 터미널 2에서 어떤 시그널이 들어왔는지 출력.
즉, 터미널 1을 시그널 핸들러로, 터미널 2를 시그널 생성 및 전달자로 이용.

해결

터미널 1 시그널 핸들러(.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

// 시그널 핸들러 함수
void signal_handler(int sig) {
    char response[10]; // 사용자 응답을 저장할 문자 배열

    // 받은 시그널에 따라 적절한 작업 수행
    switch(sig) {
        case SIGINT: // SIGINT 시그널을 받은 경우 (Ctrl+C)
            printf("Received SIGINT. Do you want to exit? (yes/no): ");
            scanf("%s", response); // 사용자로부터 응답 받기
            if (strcmp(response, "yes") == 0) { // "yes"라고 답하면 프로그램 종료
                printf("Exiting...\n");
                exit(0);
            } else { // "no"라고 답하면 계속 실행
                printf("Continuing...\n");
            }
            break;
        case SIGTERM: // SIGTERM 시그널을 받은 경우
            printf("Received SIGTERM. Shutting down the program...\n");
            exit(0); // 바로 프로그램 종료
            break;
        case SIGALRM: // SIGALRM 시그널을 받은 경우 (알람 시그널)
            printf("Received SIGALRM. Doing next task...\n");
            // 필요한 경우 여기에 추가 작업 코드를 넣을 수 있습니다.
            break;
        default: // 예상치 못한 다른 시그널을 받은 경우
            printf("Received unknown signal %d\n", sig);
            break;
    }
}

int main() {
    struct sigaction sa; // 시그널을 처리하기 위한 구조체 선언
    sa.sa_handler = signal_handler; // 시그널이 오면 실행할 함수 지정
    sigemptyset(&sa.sa_mask); // 다른 시그널이 핸들러 실행 중 방해되지 않도록 모든 시그널을 시그널 마스크에서 제거
    sa.sa_flags = 0; // 추가적인 옵션 사용하지 않음

    // SIGINT, SIGTERM, SIGALRM에 대해 시그널 핸들러 설정
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGALRM, &sa, NULL);

    // 무한 루프를 돌면서 프로그램이 계속 실행되도록 함
    while (1) {
        printf("Program is running...\n");
        sleep(1); // 1초 동안 대기
    }
    return 0; // 프로그램이 정상적으로 종료되면 0을 반환
}

터미널 2 시그널 생성 및 전달자(.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    int choice;  // 사용자의 선택을 저장할 변수
    int seconds; // 알람 시간을 저장할 변수

    while(1) {
        // 사용자 인터페이스 출력
        printf("--------Signal Controller--------\n");
        printf("1 : send SIGTERM\n2 : set SIGALRM\n3 : EXIT\n");
        printf("Input : ");
        scanf("%d", &choice); // 사용자로부터 선택 입력 받기

        // 선택에 따라 조건 처리
        if (choice == 1) {
            // SIGTERM 시그널 보내기
            system("./signal_pipe_comment.sh SIGTERM");
            printf("Exit the program\n"); // 프로그램 종료 메시지 출력
            break; // 반복문 종료
        } else if (choice == 2) {
            // SIGALRM 시간 설정
            printf("Enter the number of seconds to send SIGALRM : ");
            scanf("%d", &seconds);
            char command[50]; // 추후 시스템 함수에 전달되어 실제 명령어를 실행 할 시스템 명령을 저장할 문자열
            sprintf(command, "./signal_pipe_comment.sh SIGALRM %d", seconds); // 명령 문자열 생성
            system(command); // 시그널 전송 명령 실행
        } else if (choice == 3) {
            // 프로그램 종료 선택
            printf("Exit the program\n"); // 종료 메시지 출력
            break; // 반복문 종료
        } else {
            // 잘못된 입력 처리
            printf("Invalid choice.\n"); // 잘못된 입력 메시지 출력
        }
    }
    return 0; // 프로그램 종료
}

터미널 1과 터미널 2를 연결할 시그널 파이프 코드(.sh)

#!/bin/bash
# handler 프로세스에게 시그널을 보내는 기능을 하는 스크립트

# 첫 번째 인자로 받은 값(SIGTERM 또는 SIGALRM)을 signal_type 변수에 저장
signal_type=$1

# handler(현재 실행 파일 이름은 handler_cmt)라는 정확한 이름의 프로세스 ID를 찾아서 pid 변수에 저장
pid=$(pgrep -x handler_cmt)

# pid 변수가 비어 있지 않은 경우 즉, 프로세스가 존재하는 경우 아래 조건문을 실행
if [ ! -z "$pid" ]; then
    # signal_type 값이 'SIGTERM'인 경우 아래 조건문을 실행
    if [ "$signal_type" == "SIGTERM" ]; then
        # 해당 pid의 프로세스에 'SIGTERM' 시그널 전송
        kill -SIGTERM $pid
        # 프로세스 ID와 함께 'SIGTERM'이 보내졌음을 공지
        echo "SIGTERM sent to process ID $pid"
    # signal_type 값이 'SIGALRM'인 경우 아래 조건문을 실행
    elif [ "$signal_type" == "SIGALRM" ]; then
        # 두 번째 인자로 받은 값(초 단위)을 seconds 변수에 저장
        seconds=$2
        # seconds로 지정된 시간 동안 대기
        sleep $seconds
        # 대기 후 해당 pid의 프로세스에 'SIGALRM' 시그널을 전송
        kill -SIGALRM $pid
        # 프로세스 ID와 함께 'SIGALRM'이 설정된 시간 후에 보내졌음을 공지
        echo "SIGALRM sent to process ID $pid after $seconds seconds"
    fi
else
    # pid가 비어 있다면, 즉 프로세스가 존재하지 않는다면 아래 메시지를 출력
    echo "No process found"
fi

실행 화면

△ 터미널 1 실행 화면                      △ 터미널 2 실행 화면

△ 터미널 1 CTRL+C 입력 결과

프로그램 설명

터미널 1에서 handler_cmt를 실행하고 터미널 2에서 controller_cmt를 실행하면 기본 준비는 끝난다. handler_cmt에는 SIGINT, SIGTERM, SIGALRM과 같은 시그널 핸들러가 정의되어 있다. controller_cmt에는 SIGTERM, SIGLARM, EXIT와 같은 3가지의 선택이 있고 사용자가 선택할 수 있다. 터미널 2 즉, handler_controller에서 1번인 SIGTERM을 선택하게 되면 터미널 1인 handler_cmt가 종료되며, 핸들러 종류와 함께 설정된 메시지가 출력된다. 2번인 SIGALRM을 선택하게 되면 몇 초로 alarm()을 설정할지 추가적인 입력을 받고 입력한 초가 지난 후 터미널 1에서 핸들러 종류와 함께 설정된 메시지가 출력되며, 터미널 2의 프로그램이 종료된다. 3번인 EXIT를 선택하게 되면 터미널 1에는 아무 영향도 주지 않은 채 터미널 2의 프로그램이 종료된다. 추가적으로 터미널 1에서 ctrl+c를 입력하게 되면 즉시 종료되지 않고 설정된 메시지를 통해 한번 더 종료를 확인한 후, 종료가 된다.
handler와 controller 간의 간편한 프로세스 관리, 프로그래밍 효율성 그리고, 쉬운 구현 등의 이유로 연결하는 pipe를 쉘 스크립트로 구현했다.

프로그램 실행 과정

  1. 터미널 1에서 시그널 핸들러 프로그램 실행
    handler_cmt가 시작되며, sigaction을 사용하여 해당 시그널이 발생할 때 signal_handler 함수가 호출되도록 하는 SIGINT, SIGTERM, 그리고 SIGALRM 시그널에 대한 핸들러를 설정한다. 프로그램은 무한 루프를 돌며 "Program is running..." 메시지를 출력하고 1초마다 대기한다.
  2. 터미널 2에서 시그널 생성 및 전달자 프로그램 실행
    controller_cmt는 사용자에게 메뉴를 통해 3가지의 옵션을 제공한다. 사용자는 SIGTERM 시그널 전송, SIGALRM 시그널 설정 및 전송, 그리고 프로그램 종료 중에서 선택할 수 있다. 사용자가 SIGTERM을 선택하면, signal_pipe_comment.sh가 호출되어 handler_cmt 프로세스에 SIGTERM 시그널을 전송한 후 controller_cmt 또한, 종료된다. 사용자가 SIGALRM을 선택하면, 사용자로부터 알람 시간을 초 단위로 입력받고, 쉘 스크립트를 호출하여 설정된 시간이 지난 후 SIGALRM 시그널을 전송한다. 사용자가 EXIT를 선택하면, controller_cmt가 종료된다.
  3. 쉘 스크립트(signal_pipe_comment.sh) 실행
    스크립트는 첫 번째 인자로 SIGTERM 또는 SIGALRM을 받는다. pgrep -x 명령어를 사용하여 handler_cmt의 PID를 찾는다. SIGTERM이면 즉시 해당 PID로 SIGTERM 시그널을 보내며, SIGALRM이면 사용자가 지정한 시간만큼 대기한 후 해당 PID로 SIGALRM 시그널을 보낸다.

느낀점

본 과제를 진행하며, 레시피 없는 요리한다는 느낌을 받았다. 처음 프로그래밍을 시작할 때, 큰 문제라고 생각한 부분이 두 터미널 간에 연결을 하는 것이었다. 두 개의 프로세스가 시그널 핸들러를 주고 받으려면 결국에는 process id를 알아야 한다고 생각했다. 본인의 process id를 추적하는 것을 fork 과제를 통해 알고 있었던 터라, C언어를 통해 연결을 하려 하였으나, 상대의 process id를 추적하는 것에는 어려움이 있었다. 그래서 C언어만을 사용하는 것 대신, pgrep 명령어를 사용하여 조회하는 방식을 이용했다. 또한, 매번 별도로 실행을 해주기 번거로운 부분이 있어 이를 쉘 스크립트로 작성하여, controller에 추가하여 controller만을 통해서도 process id를 찾게 하였다. 여기까지 와보니, controller에서 시그널을 별도로 전달하는 느낌보다는 쉘 스크립트를 handler와 controller를 연결하는 파이프 느낌으로 사용하여 시그널 핸들러를 시스템 호출이나 추가적인 API 사용 없이 사용하면 좋겠다는 생각이 들어 쉘 스크립트를 통해 시그널을 전달하는 방식을 선택했다.
이러한 과정은 마치 레시피 없는 요리라는 생각이 들었다. 레시피 없는 요리를 하다 보면, 많은 시도를 해보고 각각의 장단점을 몸소 겪으며, 요리를 완성하게 된다. 이 과정에서, 요리를 하는 순서 뿐만 아니라, 재료를 손질하고, 맛에 필요한 양념들을 배우게 된다. 그리고 이 배움을 통해 나만의 비법과 레시피가 생기게 된다. 이와 같이, 이번 과제를 진행하며 단순히 시그널 핸들러를 구현하는 것 뿐만 아니라, 어떤 방식으로 프로그래밍을 해나가는데 효과적일지 그리고, 추가적으로 process id를 찾는 방법과 쉘 스크립트를 작성하고 권한을 주는 방법 등 프로그래밍에 도움이 되는 부수적인 많은 요소들을 배웠던 과제인 것 같다.
이번 프로그래밍을 요리하는 과정에서 얻은 배움들을 잊지 않고 앞으로 더욱 발전시켜 나가서 프로그래밍 과정 중 필요할 때 언제든 쓸 수 있는 나만의 비법을 굳히고 이 레시피로 자유자재로 프로그래밍을 요리해 나가도록 노력할 것이다.

profile
코딩의 지식으로 하루를 마무리

0개의 댓글

관련 채용 정보