CS(Computer Science)/UNIX

UNIX - [SIGNAL]

seongmik 2022. 12. 29. 23:46
728x90
반응형

signal이란?

→ 소프트웨어 인터럽트 (하드웨어가 아니라 소프트웨어적으로 interrupt를 발생시키는 방법)

인터럽트(interrupt)를 발생시키는 기능을 하는 시그널은 시스템에서 아주 중요하게 사용된다.

예를 들면 터미널에서는 흔히 Control + C 을 사용해서 실행 중인 프로세스를 종료시킨다.

이는 SIGINT(interrupt signal)라는 미리 정의된 시그널을 프로세스에게 보내서 강제 종료 시키는 방법이다.

우리는 이렇게 알게 모르게 시그널을 사용 중이다.

인터럽트(Interrupt)란?
인터럽트란 CPU가 하던 일을 멈추게하고(혹은 아무 것도 안하고 있는 상태를 멈추고) 끼어드는 것으로 CPU에 할당된 작업을 변경하는데 사용할 수 있다.
일반적으로 CPU는 Fetch stage - Execute stage로(여기선 Decode stage는 생략) 나눠서 한 단위의 일을 하는데 매번 하나의 일을 수행할 때 마다(Execute stage가 끝난 직후) 인터럽트 라인(여기에 인터럽트 여부가 표시되어 있다.)을 확인하고 입터럽트가 발생 했다면 하던 일을 멈추고 다른 작업을 수행한다.
인터럽트는 Hardware/Software Interrupt로 나뉘는데 하드웨어 인터럽트는 하드웨어가 발생시킨 인터럽트를 의미하고, 예시로는 I/O 장비가 입출력을 끝냈을 때 작업 중이던 CPU에게 알려주는 경우 등이 있다. 소프트웨어 인터럽트는 소프트웨어가 발생시킨 인터럽트를 의미하고 인터럽트 라인을 세팅해서 인터럽트를 발생시킨다. 예시로는 System call이 호출 됐을 때가 있다.
signal은 시스템 콜이므로 소프트웨어 인터럽트에 해당한다.

 

signal을 핸들링 하는 방법

→ sigaction함수를 이용해서 특정 프로세스에게 특정 시그널에 대한 지침을 정할 수 있다.

시그널에 대한 지침이란 어떤 시그널을 받았을 때, 어떻게 행동해라. 라는 걸 프로세스에게 알려주는 것이다.

모든 시그널을 핸들링 할 수 있는 것은 아니며, OS에서 강제 종료할 수 있는 시그널은 무시할 수 없다.

 

signal을 보내는 방법

→ “int kill(pid_t pid, int sig)” 함수를 사용해서 특정 프로세스 id를 가진 프로세스에게 원하는 signal을 보낼 수 있다.

이 함수는 첫번째 인자의 값을 어떻게 넣냐에 따라 4가지 케이스로 동작한다.

  • case1 : pid > 0

→ 해당 id의 process에게 시그널 전달.

  • case2 : pid = 0

→ 시그널을 보낸 process(sender)와 같은 process group에 속하는 모든 process에게 시그널 전달. (자신에게도 전달)

  • case3 : pid = -1

→ uid가 sender의 euid와 같은 모든 process에게 시그널 전달. (자신에게도 전달)

  • case4 : pid < 0 & pid ≠ -1

→ process의 group id가 pid의 절댓값과 같은 모든 process에게 시그널 전달.

다른 사용자의 process에게 시그널을 보내면 -1을 return 한다. (실패를 의미한다.)

 

signal 핸들링 코드 예시

// 특정 시그널에 대한 동작을 설정하는 코드
static struct sigaction act;
act.sa_handler = 함수포인터;
sigaction(SIGNUM, &act, NULL);

 

sigaction과 pause()를 이용한 프로세스 순서 동기화

// 차일드 프로세스의 번호 역순으로 실행하는 동기화 코드
// 주의 : pause()는 어떤 signal을 받던지 무조건 대기가 풀린다.
void do_child(int i, int *cid) {
	int j;
	pid_t pid;
	static struct sigaction act;

	act.sa_handler = sigbreak;
	sigaction(SIGUSR1, &act, NULL);

	if(i < 4) {
		pause();
	}
	
	pid = getpid();
	for(j=0;j<5;j++) {
		printf("child %d .... \\n", pid);
		sleep(1);
	}

	if(i > 0) {
		kill(cid[i-1], SIGUSR1);
	}

	exit(0);
}

void sigbreak() {
	return;
}

int main(void) {
	int i, status;
	pid_t pid[5];

	for (i=0;i<5;i++) {
		pid[i] = fork();
		if (pid[i] == 0) {
			do_child(i, pid);
		}
	}

	for (i=0;i<5;i++) {
		wait(&status);
	}

	exit(0);
}

→ 생각해볼점 : child가 만들어진 역순이 아니라, child가 만들어진 순서대로 출력하라고 한다면?

힌트 : 처음 만들어진 child는 자기 다음 프로세스가 누군지 알 수 없다.

fork()시점에 모든 정보를 복사해서 가져가기 때문에 fork() 이후에 생성된 프로세스들의 pid는 알 수 없다.

 

SIGNAL에 대한 이전의 설정 복원하기 :

// SIGTERM이라는 signal을 받았을 때 하라고 설정됐던 정보를 oact구조체에 저장
sigaction(SIGTERM, NULL, &oact); /*과거 설정 저장*/

act.sa_handler = SIG_IGN;
sigaction(SIGTERM, &act, NULL);
// do anything;
sigaction(SIGTERM, &oact, NULL); // 설정 복원

 

alarm signal 설정

alarm signal은 몇 초 후에 시그널을 보내달라고 예약을 하는 함수이다.

 

timer 사용 :

#incluce <signal.h>
unsigned int alarm(unsigned int secs);

/*
> secs : 초 단위의 시간, 시간 종료 후 SIGALRM을 보낸다.
> alarm은 exec후에도 계속 작동, but fork후에는 자식 processd에 대한 alarm은 작동하지 않는다.
> alarm(0) -> alarm 끄기
> alarm은 누적되지 않는다. 2번 사용되면, 두 번째 alarm이 대체
  alarm을 또 실행시킨다면 그 다음 알람 기준으로 시작된다.
> 두 번째 alarm의 return 값이 첫 alarm의 잔여 시간
*/

예제 코드 [7장 #2-2]

void catchalarm(int);

int main(void) {
    int i, n;
    static struct sigaction act;
		
		act.sa_handler = catchalarm;
		sigaction(SIGALRM, &act, NULL);

		n = alarm(10);
		printf("알람설정: %d\\n", n);

		for(i = 0; i < 10; i++) {
				printf("... child ...\\n");
				if(i == 1) {
						n = alarm(3);
						printf("알람재설정: %d\\n", n);
				}
				sleep(1);
		}
		exit(0);
}

void catchalarm(int signo) {
		printf("\\n CATCHALARM:signo=%d\\n", signo);
		alarm(3);
}

→ alarm 시그널을 처리하는 함수 안에다가 작업을 처리해야 주기적으로 반복해서 어떤 작업을 시킬 수 있다.

→ 첫번째 알람은 메인함수 안에서 설정해야 한다.

→ 하지만 주기적인 실행을 요하는 알람은 시그널을 처리하는 함수 안에서 설정해줘야 한다.

 

예제 코드 [7장 #2-3]

void catchint(int);

int main(void) {
		int i, j, num[10], sum = 0;
		sigset_t mask;
		static struct sigaction act;

		act.sa_handler = catchint;
		sigaction(SIGINT, &act, NULL);

		sigemptyset(&mask);
		sigaddset(&mask, SIGINT);

		for(i = 0; i < 5; i++) {
				sigprocmask(SIG_SETMASK, &mask, NULL);
				scanf("%d", &num[i]);
				sigprocmask(SIG_UNBLOCK, &mask, NULL);
				sum += num[i];
				for(j = 0; j <= i; j++) {
						printf("... %d\\n", num[j]);
						sleep(1);
				}
		}

		exit(0);
}

void catchint(int signo) {
		printf("DO NOT INTERRUPT ... \\n");
}

위의 코드는 입력한 숫자를 1부터 오름차순으로 출력한다.

→ 출력 중간에 SIGINT를 보낸다면 DO NOT INTERRUPT가 출력된다.

→ 코드에 있는 에러 : 입력을 해야 할 때 SIGINT를 보낸다면 DO NOT INTERRUPT가 출력된다.

→ 참고 : 이 코드는 이미 문제가 해결된 코드이다.

→ 이런 에러가 일어난 이유 : process가 block이 되어있다가 아무 signal을 받으면 진행되는데 scanf나 getchar 등의 입력을 요구하는 함수는 프로세스를 blocked queue에 집어넣어 두고 대기한다. 근데 signal을 받으면 pause가 풀리고 ready queue에 넣는다. 그래서 입력도 못 받았는데 바로 출력문으로 넘어가는 것이다.

→ 운영체제에서 유닉스 상태도를 보면 어떤 작업이 INTERRUPTERBLE과 UNINTERRUPTERBLE로 나뉘는데 이는 signal을 받으면 pause가 깨어나나 안 깨어나는지 알려주는 용도다.

→ 해결법 : scanf를 하는 동안 signal을 blocking (시그널 블락킹은 시그널 무시와 다르다.)

  • 시그널 블락킹 : 시그널을 잠시 막아둔다. (언블락시 시그널을 처리한다.)
  • 시그널 무시 : 시그널이 오면 아예 무시한다. (아무 일도 없다.)
시그널 블락킹을 프로세스는 어떻게 처리하는가?
시그널을 블락해놓고 언블락시 처리하려면 프로세스는 블락된 동안 자신에게 온 시그널을 기억해둬야 한다. 
프로세스는 이런 작업을 하기 위해 sigset_t라는 자료구조를 사용하는데 이 자료구조는 비트마스킹을 이용한다.
시그널 하나가 sigset_t변수의 한 비트를 의미하고, 이는 한 시그널에 대해 1또는 0밖에 체크를 할 수 밖에 없음을 의미한다.
그렇다면 똑같은 시그널이 언블락 없이 2번 블락이 되면 어떻게 될까?
SIGINT 시그널이 2번 블락됐다고 가정해보자.
먼저 SIGINT 시그널이 첫 번째 블락될 때, 프로세스의 시그널 셋(sigset_t)에 SIGINT 시그널이 블락됐음을 1로 체크한다. 이는 시그널이 언블락 된 후 시그널을 처리해야 함을 프로세스가 기록한 것이다.
그 후 언블락이 없이 SIGINT 시그널이 두 번째 블락될 때, 프로세스의 시그널 셋에 SIGINT 시그널이 블락됐음을 1로 체크한다.
이 때, 두 번째 시그널을 체크하는 일은 1로 이미 체크 된 값을 다시 1로 바꾸는 일이다. 이는 아무 변화가 없음을 의미한다.
따라서 같은 시그널을 몇 번 블락 해뒀는지 프로세스는 알 수 없다.

 

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

/*
> how := SIG_SETMASK : set에 있는 signal들을 지금부터 봉쇄하겠다.
> oset은 봉쇄된 signal들의 현재 mask, 관심 없으면 NULL로 지정해서 사용.
> how := SIG_UNBLOCK : 봉쇄 제거
*/

 

pause 시스템 호출

#include <unistd.h>
int pause(void);

/*
> signal 도착까지 실행을 일시 중단 (CPU 사용 없이)
> signal이 포착되면 -> 처리 routine수행 && -1을 return
*/

 

예제 코드 [7장 #2-4]

void catchint(int);

int main(void) {
		int i, n;
		static struct sigaction act;

		act.sa_handler = catchint;
		sigaction(SIGINT, &act, NULL);

		n = pause();

		for(i = 0; i < 5; i++) {
				printf("%d ... %d\\n", i, n);
		}

		exit(0);
}

void catchint(int signo) {
		printf("signal catch ... \\n");
}

signal 블락 시 온 signal들은 unblock시에 실행한다.

하지만 들어온 순서대로 block 된 시그널을 실행하지 않는다.

그냥 signal번호순으로 실행한다.

→ signal이 오면 큐에 넣는 게 아니라 그냥 배열에 값을 하나씩 체크하기 때문에 시그널의 순서와, 횟수를 알 수 없다.

→ signal을 blocking 한다는 말과 무시한다는 말은 아예 다르다.

 

SIGINT signal을 원하는 구간동안 blocking 하는 예제코드

void catchint(int);

int main(void) {
        int i, j, num[10], sum = 0;
        sigset_t mask;
        static struct sigaction act;

        act.sa_handler = catchint;
        sigaction(SIGINT, &act, NULL);

        sigemptyset(&mask);
        sigaddset(&mask, SIGINT);

        for (i=0;i<5;i++) {
                sigprocmask(SIG_SETMASK, &mask, NULL); // 블락킹
                scanf("%d", &num[i]);
                sigprocmask(SIG_UNBLOCK, &mask, NULL); // 언블락킹
                sum += num[i];
                for (j=0;j<=i;j++) {
                        printf("... %d\\n", num[j]);
                        sleep(1);
                }
                printf("SUM=%d\\n", sum);
        }
        exit(0);
}

void catchint(int signo) {
        printf("DO NOT INTERRUPT .... \\n");
}

Parent가 2개의 Child를 만들고 그 두 Child process가 자기의 Child를 하나씩 만들어서 짝을 짓는다.

그 짝끼리 그룹으로 묶여서 Signal을 처리하게 하는 코드이다.

728x90
반응형

'CS(Computer Science) > UNIX' 카테고리의 다른 글

UNIX - [PIPE - 2]  (0) 2022.12.31
UNIX - [PIPE]  (3) 2022.12.30
UNIX - [MEMORY MAPPING]  (0) 2022.12.30
UNIX - [소켓 프로그래밍 예제 - 2]  (1) 2022.12.27
UNIX - [소켓 프로그래밍 기초 - 1]  (0) 2022.12.26