시그널 핸들러(signal handler)란 운영 체제로부터 특정 유형의 시그널을 받았을 때 해당 시그널에 대응하는 사용자 정의 함수나 프로세스의 일부를 말한다. 시그널은 프로그램의 정상적인 흐름을 방해하는 비동기적인 이벤트들로, 주로 예외 상황, 시스템 요청, 또는 다른 프로그램과의 상호 작용 과정에서 발생한다. 시그널 핸들러는 이러한 시그널에 대해 특정 동작을 실행하도록 프로그래밍할 수 있어, 프로그램이 자체적인 방식으로 예외를 처리하거나 필요한 청소 작업을 수행할 수 있게 한다.
터미널 2개를 열고, 터미널 1에서 CTRL+C를 누르면 확인을 받고 종료하는 프로그램.
터미널 2에서는 터미널 1에서 구동적인 프로그램으로 시그널 전달.
터미널 1의 프로그램은 터미널 2에서 어떤 시그널이 들어왔는지 출력.
즉, 터미널 1을 시그널 핸들러로, 터미널 2를 시그널 생성 및 전달자로 이용.
#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을 반환
}
#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; // 프로그램 종료
}
#!/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를 쉘 스크립트로 구현했다.
본 과제를 진행하며, 레시피 없는 요리한다는 느낌을 받았다. 처음 프로그래밍을 시작할 때, 큰 문제라고 생각한 부분이 두 터미널 간에 연결을 하는 것이었다. 두 개의 프로세스가 시그널 핸들러를 주고 받으려면 결국에는 process id를 알아야 한다고 생각했다. 본인의 process id를 추적하는 것을 fork 과제를 통해 알고 있었던 터라, C언어를 통해 연결을 하려 하였으나, 상대의 process id를 추적하는 것에는 어려움이 있었다. 그래서 C언어만을 사용하는 것 대신, pgrep 명령어를 사용하여 조회하는 방식을 이용했다. 또한, 매번 별도로 실행을 해주기 번거로운 부분이 있어 이를 쉘 스크립트로 작성하여, controller에 추가하여 controller만을 통해서도 process id를 찾게 하였다. 여기까지 와보니, controller에서 시그널을 별도로 전달하는 느낌보다는 쉘 스크립트를 handler와 controller를 연결하는 파이프 느낌으로 사용하여 시그널 핸들러를 시스템 호출이나 추가적인 API 사용 없이 사용하면 좋겠다는 생각이 들어 쉘 스크립트를 통해 시그널을 전달하는 방식을 선택했다.
이러한 과정은 마치 레시피 없는 요리라는 생각이 들었다. 레시피 없는 요리를 하다 보면, 많은 시도를 해보고 각각의 장단점을 몸소 겪으며, 요리를 완성하게 된다. 이 과정에서, 요리를 하는 순서 뿐만 아니라, 재료를 손질하고, 맛에 필요한 양념들을 배우게 된다. 그리고 이 배움을 통해 나만의 비법과 레시피가 생기게 된다. 이와 같이, 이번 과제를 진행하며 단순히 시그널 핸들러를 구현하는 것 뿐만 아니라, 어떤 방식으로 프로그래밍을 해나가는데 효과적일지 그리고, 추가적으로 process id를 찾는 방법과 쉘 스크립트를 작성하고 권한을 주는 방법 등 프로그래밍에 도움이 되는 부수적인 많은 요소들을 배웠던 과제인 것 같다.
이번 프로그래밍을 요리하는 과정에서 얻은 배움들을 잊지 않고 앞으로 더욱 발전시켜 나가서 프로그래밍 과정 중 필요할 때 언제든 쓸 수 있는 나만의 비법을 굳히고 이 레시피로 자유자재로 프로그래밍을 요리해 나가도록 노력할 것이다.