기본 신호 개념
신호(Signal)의 정의: 소프트웨어가 특정 이벤트를 프로세스에 알리는 방법
신호의 수명: 생성 시점(이벤트 발생시점)과 전달 시점 사이의 기간 (시그널은 해당 이벤트가 발생하면 생성되고 타켓 프로세스에게 전달되고 프로세스가 시그널을 받으면 Delivery된 것! / 수신한 시그널을 전송 완료하면 종료됨), 항상 시그널이 타겟 프로세스에게 전달되는 것은 아님!
대기 중인 신호: 신호가 생성되었지만 전달되지 않은 신호를 (pending signal) -> pending signal은 바로 삭제되는 것이 아니라 pending signal list에 대기하여 이후 전달 가능 상태가 되면 다시 리스트에서 빠져나와 전달됨!
신호 처리: 신호가 전달될 때 프로세스가 신호 처리기를 실행하면 신호를 잡게 됩니다. 따로 정의된 시그널이 존재하지 않으면, default signal handler(신호가 도착했을 때 수행할 동작을 정의한 모듈)가 존재
1. sigaction 함수 : (프로세스가 신호를 받았을 때, 사용자가 따로 지정한 행동을 통해 신호를 처리하는 동작을 등록하는 함수) 예) s라는 신호가 들어왔을 때, A라는 함수를 실행해주도록 등록
- 사용자가 정의한 함수 이름을 통해 sigaction함수를 이용하여 신호 처리기를 설치(등록)합니다.
- SIG_DFL (기본 작업(default action) 수행), SIG_IGN (신호 무시, target signal이 도착했을 때, 아무 행동을 취하지 않고 신호를 버리는 것)와 같은 옵션을 사용하여 호출할 수 있습니다.
2. 프로세스 신호 마스크 ( sigprocmask 함수를 통해서 제어 가능 ) :
- 현재 차단된 신호 목록을 포함합니다. (block시킬 함수를 자유롭게 삽입 및 수정을 하는 제어를 하는 함수 -> pending signal)
- 차단된 신호(pending signal)는 무시된 신호처럼 버려지지 않습니다.
- sigprocmask를 사용하여 프로세스의 신호 마스크(signal mask)를 변경하여 신호를 차단가능(pending signal)
- SIG_IGN을 사용하여 sigaction을 통해 신호를 무시(ignore signal, 생명주기 끝)할 수 있습니다.
Generating signals (신호 생성) in 쉘 터미널
- 모든 신호는 SIG로 시작하는 상징적 이름과 고유한 시그널 번호를 가짐 <signal.h>에 정의됨
- 명령줄에서 신호 생성
- kill 명령어 (특정 프로세스에게 시그널을 전송하는 명령어)
- signal_name 매개변수는 상징적 신호 이름 or 시그널 번호로 지정 가능 이때, 선행하는 SIG를 생략한 형태로 구성 => 예: kill -s USR1(시그널 이름, 접두사 제외하고) 3423(target 프로세스 id) (kill 명령 다음에 -s 옵션을 주고 보낼려는 시그널의 이름을 SIG(접두사)를 빼고 지정하여 3423번 프로세스에게 전송)
- kill의 마지막 형식은 다음과 같은 신호 번호 값만 지원함: 0은 신호 0, 1은 신호 SIGHUP, 2는 신호 SIGINT, 3은 신호 SIGQUIT, 6은 신호 SIGABRT, 9는 신호 SIGKILL, 14는 신호 SIGALRM, 15는 신호 SIGTERM
- 예: kill -9 3423 (9번 시그널(보내려는 시그널 지정)을 3423 프로세스로 전송하겠다는 것)
- -l 옵션은 사용 가능한 상징적 신호 이름의 목록을 제공함
kill 함수를 통하여 -s옵션을 통하여 KILL하여 6986 프로세스를 죽임 (오른쪽) -> 왼쪽과 오른쪽 결과는 동일
신호 생성
- kill 함수
- pid 매개변수 :
- 대상 프로세스 ID( 0이거나 < 0 을 경우 프로세스 그룹으로 신호를 보냄 )
- 0이면 호출자의 프로세스 그룹 구성원들에게 신호를 보냄
- -1이면 권한이 있는 모든 프로세스에게 신호를 보냄
- -1이 아닌 임의의 다른 음수 값(예, -10(프로세스 그룹 아이디)일 경우, 절대값이 pid와 같은 프로세스 그룹 ID(절댓값 10)를 가진 모든 프로세스 그룹에 신호를 보냄
Return value (반환 값) : 성공하면 0, 실패하면 -1을 반환하고 실패 시 errno가 설정
int raise(int sig) (#include <signal.h>)
- 자기 자신에게 신호를 보냄
- 반환 값 : 성공 시 0, 실패 시 -1 반환하고 errno 설정
- unsigned alarm(unsigned seconds) (#include <unistd.h>)
- 지정된 초 후에 호출한 프로세스에게 알람이 완료된 SIGALRM 신호를 보냄
- seconds 매개변수가 0이면 이전에 요청한 알람 요청을 취소함
- 기본 동작은 프로세스를 종료하는 것
- 반환 값 :
- 알람이 설정되기 전에 남아 있던 초의 수를 반환(정상적으로 실행될 때), 이전 알람이 설정되지 않았으면 0을 반환
- 오류를 보고하지 않음
alarm이 설정되면 밑의 for문의 무한루프를 동작하고 10초 동안 알람의 끝날 때까지 대기하고 10초가 종료되면 default action으로 해당 프로세스를 종료함
Signal sets (신호 집합) - signal set이라는 데이터 구조 안에 여러 개의 함수를 등록하여 함수를 처리할 수 있음! (int 타입)
신호 집합은 다섯 가지 함수로 조작됨 (sigset_t(1번째 파라미터)에서 signo의 신호 번호(2번째 파라미터)를 조절 가능!)
집합(set) 추가 및 삭제 (sigaddset, sigdelset) : 사용자가 자유롭게 설정할 수 있음
- 성공 시 0 반환, 실패 시 -1과 함께 errno 설정
초기화 (sigemptyset, sigfillset) : sigemptyset(signal set 타입 내부를 전부 초기화), sigfillset(등록할 수 있는 모든 시그널을 등록) > sigset_t의 변수를 선언하고 sigemptyset_t를 통해 empty합니다. or sigfillset으로 전부 등록 가능!
- sigfillset은 sigset_t를 모든 신호를 포함하도록 초기화
- 성공 시 0 반환, 실패 시 -1과 함께 errno 설정
멤버십 검사 (sigismember) : signo(2번째 파라미터 번호)가 집합에 포함되어 있으면 1 반환, 그렇지 않으면 0 반환
Signal mask (신호 마스크)
Signal mask (신호 마스크) ( (1)시그널 마스크를 어떤 식으로 수정할지 작동방식을 지정, (2) signal set타입으로 2번째 파라미터 지정 - 수정하려는 시그널 번호들 1개 이상이 포함됨 (3) 시그널 set의 output parameter로 2번째 파라미터로 변경되고 나서 변경되기 이전 시그널 정보를 반환하는 역할 )
- 현재 차단된 신호들의 집합을 제공합니다.
- sigset_t 타입입니다.
- sigprocmask() 함수로 신호 마스크를 수정합니다.
sigprocmask()
- how 매개변수는 신호 마스크를 수정하는 방식을 지정합니다. (1번째 파라미터) / 아래는 2번째 파라미터(signal set 타입을 넘겨줌) / 3번째 파라미터(output 파라미터, 앞에 2개는 input 파라미터, 1,2 번째 파라미터로 변경되기 이전 시그널 정보를 마지막 파라미터 정보로 반환) => (아래 3가지는 첫 번째 파라미터 종류! - int how)
- SIG_BLOCK: 현재 차단된 신호에 set 신호들을 추가합니다. =>기존의 시그널이 등록될 경우, 기존의 시그널은 그대로 두고 새로이 추가하는 것
- SIG_UNBLOCK: 현재 차단된 신호에서 set 신호들을 제거합니다. =>기존의 시그널이 등록될 경우, 기존의 시그널은 그대로 두고 새로이 추가하는 것
- SIG_SETMASK: 차단된 신호를 지정된 set 신호로 새로이 설정합니다. (기존의 다른 시그널이 등록이 되어있을 경우, 혹은 변경된 부분을 전부 무시(zero-based)로 하고 2번째 파라미터에 새로이 설정하겠다는 것)
- oset(oldset, 원래의 시그널, SIG_SETMASK로 변경된 부분을 초기화하여 원래 상태로 되돌릴 수 있음)이 NULL이 아니면, 수정 전의 신호 집합을 *oset에 반환합니다. (3번째 파라미터, 이전 시그널 마스크에 저장할 내용이 없을 경우 NULL로 지정 / 변경된 정보가 잘못될 경우 원래의 시그널 형태로 돌아가는 경우가 생김! - sigprocmask( SIG_SETMAK, oset, Null)
- 성공 시 0을 반환하고, 실패 시 -1을 반환하며 errno가 설정됩니다.
- 단일 스레드 프로세스에서만 사용해야함
- 다중 스레드에서 SIGNALMASK를 조절하려면 pthread_sigmask()를 사용해야 합니다.
- 일부 신호(SIGSTOP, SIGKILL)는 차단할 수 없습니다. => signalmask로 막을 수 없음
Signal masks and sets example
{ SIGINT를 블록된 신호 집합에 추가 - interrupt 시그널을 signalmask에 추가하고자 하는 경우}
sigset_t newsigset;
if((sigemptyset(&newsigset) == -1 ||
(sigaddset(&newsigset, SIGINT) == -1))
perror("Failed to initialize the signal set");
else if(sigprocmask(SIG_BLOCK, &newsigset, NULL) == -1)
perror("Failed to block SIGINT");
먼저, sigset_t 변수를 선언한 후에 sigemptyset 함수를 호출하여 시그널set을 비우고, 첫 번째 함수로 if 조건문 내부를 실행하고 -1이면 에러 메세지를 내고 만약, 정상적인 상황이면 || 다음인 sigaddset함수를 호출하여 SIGINT(인터럽트 시그널)을 추가함 => 에러가 나지 않으면, sigprocmask로 현재 시그널 마스크에 2번째 파라미터 signal set의 내부에 있는 함수를 SIG_BLOCK을 통해 추가하겠다는 것 ex) ctrl+C or 인터럽트 시그널을 전달하여도 전달이 되지 않음! -> 인터럽트 시그널은 pending이 됨!
{ 이전 신호 집합 수정 및 복원 - 변경하고 원래의 시그널 마스크로 돌아오는 방법 }
sigset_t blockmask;
sigset_t oldmask;
... // blockmask에 신호 추가
if(sigprocmask(SIG_SETMASK, &blockmask, &oldmask) == -1)
return -1;
...
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1)
return -1;
...
첫 번째 sigprocmask함수를 호출하여서 blockmask를 SIG_SETMASK로 초기화를 하고 변경사항을 진행 -> 이후, 두 번째 sigprocmask함수를 원래 상태로 되돌리기 위해서 SIG_SETMASK를 호출하여 oldmask (변경되기 이전 시그널 정보)를 넘겨주어 이전의 데이터를 복원하는 것
sigaction - 신호 잡기(Catching)와 무시(Ignoring)
#include <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
struct sigaction {
void (*sa_handler)(int); /* SIG_DFL, SIG_IGN 또는 함수 포인터 */ -> 1번째 파라미터 int값 전달
sigset_t sa_mask; /* 핸들러 실행 중 블록될 추가 신호 */
int sa_flags; /* 특수 플래그 및 옵션 */ 추가적으로 설정하고 싶은 옵션을 지정
void (*sa_sigaction)(int, siginfo_t *, void *); /* 실시간 핸들러 - 부가적인 파라미터를 넘
겨 줄 수 있음*/
};
- sigaction 함수는 호출자(프로세스)가 특정 신호와 관련된 동작을 검사하거나 지정할 수 있게 해줍니다.
(첫 번째 파라미터인 sig값이 전달되면 2번째 파라미터에서 정의한 구조체 내부의 액션을 등록, 3번째 파라미터에는 이전 액션 정보를 반환하는 output 파라미터)
{ 매개변수 }
- Sig: 동작을 위한 신호 번호
- Act: 수행될 동작을 지정
- Oact: 신호와 관련된 이전 동작을 받음
{ 반환 값 }
- 성공 시 0을 반환하고, 실패 시 -1을 반환하며 errno를 설정
struct sigaction 구조체 - 4번째 파라미터를 사용하고 싶은면 sa_flags값으로 설정해야 됨!
- 응용 프로그램은 sa_handler와 sa_sigaction 중 하나만 사용해야 합니다.
- SA_SIGINFO 플래그가 설정되지 않으면 sa_handler가 동작을 지정
- SA_SIGINFO가 설정되면 sa_sigaction이 신호 잡기 함수를 지정
신호 핸들러 (signal handler)
- POSIX 기본 표준에서 신호 핸들러는 void를 반환하고 하나의 정수 매개변수를 갖는 일반 함수입니다.
- 이 방식은 핸들러에 값을 전달할 수 없다는 한계로 인해 유용성이 제한됩니다.
POSIX확장을 사용하면 sa_sigaction을 통해 핸들러에 값을 전달할 수 있습니다.
- sa_handler의 특수 값들
- SIG_DFL: 신호에 대한 기본 동작을 복원합니다.
- SIG_IGN: 신호를 무시하여 처리하지 않습니다, (버림)
신호 잡기/무시 예제
SIGINT 신호에 대해 mysighand를 신호 핸들러로 설정
struct sigaction newact;
newact.sa_handler = mysighand;
newact.sa_flags = 0;
if ((sigemptyset(&newact.sa_mask) == -1) ||
(sigaction(SIGINT, &newact, NULL) == -1))
perror("Failed to install SIGINT signal handler");
newact라는 sigaction 구조체를 먼저 선언하고 sa_handler 함수에 mysighand로 핸들러 함수를 등록하고, sa_flag를 0으로 설정하여 옵션 및 플래그를 사용하지 않고, sa_mask값은 sigemptyset함수를 호출하여 초기화하고 sigaction 함수의 값으로 타겟 시그널인 SIGINT 신호에 대해 newact로 설정된 동작을 적용합니다. 만약 실패하면 -1을 반환
이 신호에 대해 기본 동작이 설정되어 있으면(먼저 확인) SIGINT를 무시
struct sigaction act;
if (sigaction(SIGINT, NULL, &act) == -1)
perror("Failed to get old handler for SIGINT");
else if (act.sa_handler == SIG_DFL) {
act.sa_handler = SIG_IGN;
if (sigaction(SIGINT, &act, NULL) == -1)
perror("Failed to ignore SIGINT");
}
첫 번째 sigaction함수를 interrupt signal 액션을 평가하여 현재 등록된 default action값이 등록되어 있는지 확인하고, (2번째 파라미터 null -새로운 액션을 등록하지 않기 때문) 3번째 파라미터 act로 이전 액션이 반환됨 -> 그 후, sigaction이 에러가 나지 않으면, 첫 번째 필드값이 SIG_DFL이면 action을 SIG_IGN로 변경, 2번째 signalaction 값으로 SIGINT값을 SIG_IGN 함수로 등록함
프로그램 8.5
sin(x)의 평균값을 구간 0에서 1 사이에서 (계속 계산하는 프로그램)추정합니다.
신호 핸들러를 사용하여 프로그램을 우아하게 종료하는 방법(while문의 조건문을 true에서 거짓이 되도록 바꿈)을 보여줍니다. -> 반복문이 돌다가 언제라도 인터럽트 시그널이 도착하면, while문 내부 실행이 멈추고signal handler 루틴이 실행됨!)
- 사용자가 Ctrl-C를 입력(interrupt signal)하면, 신호 핸들러가 종료를 알리기 위해 doneflag를 설정합니다. 반복적으로 계속 계산하다가 doneflag 변수값을 1로 변경하여 반복문을 빠져나오고 지금까지 계산했던 내용의 평균값을 제공함
doneflag에 접근하는 코드는 중요한 부분(시그널 핸들러와 메인 프로그램이 동시에 액세스 가능하기에 충돌문제가 나지 않도록 크리티컬 섹션)입니다. 신호 핸들러가 메인 프로그램이 확인하는 값을 수정할 수 있기 때문입니다. (signal handler가 도착하면, 점프하여 signal handling 부분이 실행되고 다시 돌아오는 것을 말함!)
- doneflag를 sig_atomic_t로 선언하여(이 과정이 critical section으로 만든 것!), 작고 원자적으로 접근 가능한 정수 타입이 되도록 합니다. => sig_atomic_t로 읽기 시작하면 완료될때까지 중단되지 않음을 보장!
- volatile 한정자는 컴파일러에게 해당 변수가 프로그램 실행 중 비동기적으로 변경될 수 있음을 알립니다.
프로그램 8.6
- 프로그램 8.5와 유사합니다. (메인 프로그램과 시그널 핸들러가 동시에 액세스 할 수 있음!)
- 메인 프로그램 루프가 10,000번째 반복마다 결과(중간계산결과)를 포함하는 문자열을 버퍼에 저장함
- SIGUSR1가 도착하면, 신호 핸들러가 해당 문자열을 출력합니다.
- 크리티컬 섹션 -> 버퍼를 액세스할 수 있는 부분을 critical section으로 만들어야 함!
- 메인에서 문자열을 수정하고, 신호 핸들러에서 문자열을 출력하는 부분입니다.
- 메인 프로그램은 문자열을 수정하는 동안 결과 함수가 신호를 차단하여 크리티컬 섹션을 보호합니다. ( sigprocmask 함수로 시그널 핸들러의 신호를 pending signal로 막고 작업이 완료되면 다시 sigprocmask로 버퍼의 문을 얄아 USR1 시그널을 불러 안전하게 시그널을 전달할 수 있게끔 하는 것!)
result함수는 10,000번째마다 버퍼에 중간 꼐산 결과를 저장함, snprintf함수를 이용하여 buf에 BUFSIZE만큼 출력한다는 의미(count 값, sum값 등등 이러한 string값을 buf에 쓰고 제일 끝에 null charcter함수를 넣어서 string이 되도록 해줌!)
sigprocmask 함수를 호출하여 SIG_BLOCK으로 호출하여 sigset 변수에 sigaddset함수로 SIGUSR1 시그널을 추가한 것! / 이때, oset함수로 변경된 시그널을 수정하는 것!
신호 대기
신호는 이벤트를 기다리기 위해 바쁜 대기를 하지 않고 대기하는 방법을 제공합니다.
pause(), sigsuspend(), sigwait() 함수가 사용됩니다.
Pause 함수
#include <unistd.h>
int pause(void);
- 호출된 스레드를 일시 중지하여 신호가 전달될 때까지 기다립니다.
- 이 신호는 사용자 정의 핸들러를 실행하거나 프로세스를 종료하도록 설정된 동작을 가집니다.
- 프로세스가 신호를 잡으면(원하는 시그널이 도착하면), pause 함수는 신호 핸들러가 반환된 후에 반환됩니다.
- 항상 -1을 반환합니다. (시그널이 프로세스에서 캐치되면, signal handler가 전부 실행되고 나면 pause함수가 반환됨)
연습 문제 8.21
- pause를 사용하여 특정 신호를 대기하는 프로세스를 작성하는 예제입니다. 신호 핸들러가 sigreceived 변수를 1로 설정하여 신호를 받았음을 나타냅니다.
static volatile sig_atomic_t sigreceived = 0;
-> 이 시점에서 sigprocmask로 block하고 pause함수를 부르기 전에 막았던 시그널을 unblock하고 문을
열어줌
while( sigreceived == 0 )
pause();
sigreceived 변수를 통해 특정 신호가 수신될 때까지 pause() 함수를 이용하여 프로세스를 일시 중지시킵니다.
신호가 수신되면 sigreceived가 1로 설정되어 while 루프에서 빠져나오게 됩니다.
signal handler가 실행되서 sigreceived가 원하는 시그널이 도착하면 1로 종료시킨다는 것, 만약 원하는 시그널이 아니면 다시 pause를 불러서 다시 잠든다는 것!
연습 문제 8.21에서 sigreceived를 검사하고 pause를 호출하는 사이에 신호가 전달되면 어떻게 될까요?
- 이전에 전달된 신호는 pause에 영향을 주지 않습니다. (시그널이 도착하고 난 다음에 pause함수가 불렸기 때문)
- pause는 다른 신호 또는 동일한 신호가 전달될 때까지 반환되지 않습니다.
이러한 문제를 해결하는 방법 : 프로그램은 신호 차단 해제(unblocking)를 하고 pause를 동시에 시작해야 합니다. 이를 위해 sigsuspend 함수가 이러한 기능을 제공합니다.
Sigsuspend 함수
#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
- sigmask가 가리키는 신호 마스크로 설정한 뒤, 프로세스를 중단하여 신호가 수신될 때까지 대기합니다.
- sigmask 매개변수를 사용하여 프로그램이 찾고 있는 신호의 차단을 해제(unblock)할 수 있습니다.
- 함수가 반환될 때, 신호 마스크는 sigsuspend가 호출되기 이전 값으로 재설정됩니다.
- 항상 -1을 반환합니다.
=> 타겟 시그널을 sigprocmask로 막고 다시 풀 때, 타겟 시그널을 뺀 sigsuspend함수로 문을 열어 주는 형식
1. Sigsuspend의 올바른 사용 예시 - 단일 신호를 기다리는 올바른 방법
- 테스트와 중단 사이에 신호가 전달될 수 없습니다. 신호가 테스트될 때 차단되기 때문입니다. (타겟 시그널이 도착할 때만, sigsuspend함수가 깨어날 수 있음!)
- 프로세스가 중단될 때, signum만 차단되지 않습니다.
maskall - 모든 시그널을 포함하여 막음
maskmost - 모든 시그널 중에서 타겟 시그널만 빼놓은 시그널을 모두 막고, 타겟 시그널만 통과가 가능하도록 함
타겟 시그널이 전달이 되면 sigreceived함수가 호출되서 1로 변경하고 시그널 핸들러가 반환하면 sigsuspend함수를 반환하고 sigprocmask를 원래 상태로 복원함
=> sigfillset으로 전부 채우고 sigdelset으로 타겟 시그널만 빼도록 설정
maskold - 원본 시그널 상태를 저장해 둠, 문제가 생길 시 원래 상태로 복원하려는 용도
2. Sigsuspend - 프로세스가 signum을 기다리는 동안 다른 신호도 처리할 수 있도록 하는 수정
- maskblocked는 코드 세그먼트 시작 시 차단된 신호들을 포함하며, signum이 이 집합에 포함되도록 보장됩니다.
- signum 이외의 신호들은 코드 세그먼트에 진입하기 전에 차단 해제될 수 있으며, 이 신호들이 전달되면 sigsuspend가 반환됩니다. 이 코드는 매번 sigreceived를 검사하고, 올바른 신호가 전달될 때까지 프로세스를 다시 중단시킵니다.
maskblocked - 오리지널 + 타겟 시그널
maskunblocked - 오리지널 - 타겟 시그널
애매하게 타겟 시그널이 도착하면 문제가 발생하기에, 작업을 완료하고 타겟 시그널이 도착하게끔 설정
Sigsuspend
- 신호를 기다리기 위해 sigsuspend를 사용하는 잘못된 예시
sigfillset(&sigmost);
sigdelset(&sigmost, signum);
sigsuspend(&sigmost);
이 코드 세그먼트가 시작되기 전에 신호가 전달되면, 프로세스는 여전히 자체 중단 상태로 남아 있고, 다른 signum 신호가 생성되지 않으면 교착 상태에 빠집니다.
Sigwait
#include <signal.h>
int sigwait(const sigset_t *restrict sigmask, int *restrict signo);
- sigmask에 지정된 신호 중 하나가 대기 중일 때까지 차단하고, 해당 신호를 대기 중인 신호 집합에서 제거한 후 차단 해제합니다.
- 제거된 신호의 번호는 signo가 가리키는 위치에 저장됩니다.
- 성공 시 0을 반환하고, 실패 시 -1을 반환하며 errno를 설정합니다.
suspend()와의 차이점 - 타겟 시그널을 집어 넣음(sigwait)
- sigmask 매개변수는 대기할 신호 집합을 포함하므로, 해당 집합 내의 신호만 sigwait가 반환되게 할 수 있습니다.
- 프로세스의 신호 마스크를 변경하지 않습니다.
- sigmask에 있는 신호는 sigwait 호출 전에 차단되어야 합니다.
프로그램 8.11
- sigwait를 사용하여 SIGUSR1이 전달된 횟수를 셉니다.
- 신호가 항상 차단되어 있으므로 별도의 신호 핸들러는 필요하지 않습니다.
signo에는 USR1 시그널로 기록되고 반환됨 -> 타겟 시그널이 와서 pending되면 반환됨!
signalcount 함수는 USR1 시그널이 오면 깨어나서 count값을 늘리고 다시 sigwait함수를 부르고 잠드는 과정을 반복!
에러와 비동기 신호 안전성
신호가 함수 호출과 상호작용할 때 발생하는 3가지 어려움
1. 신호에 의해 중단된 POSIX 함수가 재시작되어야 하는지 여부
- 중단된 함수는 -1을 반환하고, errno가 EINTR로 설정됩니다.
- 프로그램은 이 에러를 명시적으로 처리하고, 필요시 시스템 호출을 재시작해야 합니다.
- 어떤 함수가 중단될 수 있는지 논리적으로 결정하는 것은 항상 가능하지 않습니다 (매뉴얼 페이지 참조)
2. 신호 핸들러가 비재진입 함수(nonreentrant functions)를 호출할 때
- 함수가 신호 핸들러 내에서 안전하게 호출될 수 있다면, 그 함수는 비동기 신호 안전성(async-signal safe)을 가집니다.
- 많은 POSIX 라이브러리 함수는 비동기 신호 안전하지 않습니다. 왜냐하면 이 함수들은 정적 데이터 구조를 사용하거나, malloc 또는 free를 호출하거나, 비재진입 방식으로 전역 데이터 구조를 사용하기 때문입니다 (안전한 함수 목록은 표 8.2 참조)
3. errno를 사용하는 에러 처리
- 신호 핸들러가 프로그램의 다른 부분에서 에러 처리를 방해하지 않도록 주의해야 합니다.
- 신호 핸들러가 errno를 변경하는 함수를 호출할 때, 잘못된 에러가 보고될 수 있습니다.
- 신호 핸들러는 errno를 변경할 가능성이 있는 함수를 호출할 경우, errno를 저장하고 복원해야 합니다.
savederrno 함수에 errno함수를 저장하고 write 함수가 실행되고 나서 errno함수를 복구함!
신호 처리에 유용한 규칙
- 의심스러울 경우, 프로그램 내에서 라이브러리 호출을 명시적으로 재시작하거나 재시작 라이브러리를 사용하세요.
- 신호 핸들러에서 사용하는 각 라이브러리 함수가 비동기 신호 안전 함수 목록에 있는지 확인하세요.
- 외부 변수를 변경하는 신호 핸들러와 이 변수를 접근하는 다른 프로그램 코드 사이의 상호작용을 신중하게 분석하세요.
- 필요할 경우 errno를 저장하고 복원하세요.
프로그램 제어
- 프로그램은 때때로 신호를 사용하여 오류를 처리합니다.
- 오랜 시간 동안 차단된 긴 계산을 중단하면서 프로그램을 종료하지 않기 위해 사용됩니다.
- Ctrl-C 입력 시, 프로그램이 처음부터 다시 시작해야 할 수도 있습니다.
- 간접 신호 처리
- Ctrl-C에 응답하여 플래그를 설정합니다.
- 여러 함수 계층을 통해 복귀해야 할 수도 있어서 복잡할 수 있습니다.
- 직접 접근 방식 : 신호 핸들러가 원하는 종료 지점으로 직접 점프합니다.
- sigsetjmp, siglongjmp 함수 사용
Sigsetjmp와 Siglongjmp
#include <setjmp.h>
void siglongjmp(sigjmp_buf env, int val);
int sigsetjmp(sigjmp_buf env, int savemask);
- sigsetjmp()
- 문(label)과 유사한 마커를 제공합니다.
- 매개변수
- env: 해당 마커로 다시 점프하기 위해 필요한 정보를 수집하는 데 초기화됩니다.
- savemask: 0이 아닌 경우, 현재 신호 마스크 상태가 env 버퍼에 저장됩니다. / 0이면 jmp 위치를 설정한 것
- 반환 값
- 프로그램이 직접 호출될 때 0을 반환합니다.
- siglongjmp() 호출에 의해 실행될 때 val 값을 반환합니다.
- siglongjmp()
- 동일한 sigjmp_buf 변수로 설정된 sigsetjmp 지점으로 다시 점프합니다.
- 매개변수
- val: sigsetjmp()에 의해 설정된 지점에서 반환될 값을 지정합니다.
siglongjmp/sigsetjmp 예제
- 프로그램 8.12
- Ctrl-C가 입력될 때 프로그램이 메인 루프로 돌아가도록 하는 SIGINT 핸들러 설정 방법을 보여줍니다.
- siglongjmp를 호출하기 전에 sigsetjmp를 실행해야 합니다.
- sigaction 호출은 sigsetjmp 이전에 나타나야 하며, 이는 sigaction이 한 번만 호출되도록 하기 위함입니다.
- "jumpok" 플래그
- 프로그램이 sigsetjmp를 실행하기 전에 신호 핸들러가 siglongjmp를 호출하는 것을 방지합니다.
jumpok 함수를 sig_atomic_t로 critical section으로 지정하고, jumpok함수로 준비가 되지 않으면 jump 하지 않고 그냥 리턴하고 main 함수 내부에서는 chandler함수를 handler로 등록하고 sigaction을 호출하여 타겟 시그널은 SIGINT(인터럽트 시그널) 이고 Ctrl+C를 누르게 되면 인터럽트가 발생하고, jumpok함수로 준비가 되면 jump를 함
sigsetjump함수로 jumpbuf 함수에 jump할 때 필요한 정보와 현재 signalmask 정보인 1을 같이 저장함!
만약, 시그널핸들러에서 값이 1이면 siglongjmp를 실행하여 넘겨준 1값을 받아 if 조건문에 1은 참이므로 "Returned to main loop due to ^c\n"을 실행하고 다시 무한루프를 실행 -> 다시 Ctrl+ C(인터럽트 시그널)을 발생하여 과정이 반복
비동기 I/O 프로그래밍
- 비동기 I/O / 동기 I/O는 함수의 태스크가 완료될 때까지 기다렸다가 완료하면 다음 태스크로 넘어가는 I/O 작업
- 일부 유형의 애플리케이션은 요청을 시작하고 계속 실행하면서, I/O 작업이 프로그램 실행과 비동기적으로 처리되도록 허용합니다.
- POSIX확장은 비동기 I/O의 정의를 4가지 주요 함수에 기반하여 제공합니다.
- aio_read(), aio_write(), aio_return(), aio_error() -> aio(Asynchronous I/O)
#include <aio.h>
int aio_read(struct aiocb* aiocbp);
int aio_write(struct aiocb* aiocbp);
- aio_read() / 구조체 내부에 read (fd, buf, nbytes)가 들어 있음
- 프로세스가 읽기 요청을 대기열에 추가합니다.
- aiocbp -> aio_fildes에 연결된 파일에서 aiocbp -> aio_nbytes만큼의 데이터를 aiocbp -> aio_buf로 지정된 버퍼에 읽어들입니다.
- 성공 시 0을 반환하며, 실패 시 -1을 반환하고 errno를 설정합니다.
- aio_write() / 구조체 내부에 write (fd, buf, nbytes)가 들어 있음
- 쓰기 요청을 대기열에 추가합니다.
- 성공 시 0을 반환하며, 실패 시 -1을 반환하고 errno를 설정합니다.
- Struct aiocb 구조체
- int aio_fildes;
- volatile void *aio_buf;
- size_t aio_nbytes;
- off_t aio_offset;
- I/O의 시작 위치를 지정합니다.
- int aio_reqprio;
- 요청의 우선순위를 낮춥니다.
- struct sigevent aio_sigevent;
- 호출 프로세스가 완료를 알리는 방법을 지정합니다. (시그널과 관련된 필드)
- aio_sigevent.sigev_notify가 SIGEV_NONE이면 OS는 신호를 생성하지 않습니다.
- SIGEV_SIGNAL이면 OS는 aio_sigevent.sigev_signo에 지정된 신호를 생성합니다.
- int aio_lio_opcode;
- 여러 비동기 I/O 요청을 한꺼번에 제출하기 위해 lio_listio 함수에서 사용됩니다.
#include <aio.h>
ssize_t aio_return(struct aiocb* aiocbp);
int aio_error(const struct aiocb* aiocbp);
- aio_return()
- 완료된 I/O 작업의 상태를 반환합니다.
- 반환 값 : 작업이 완료되었을 때 읽거나 쓴 바이트 수를 반환합니다.
- aio_error()
- 비동기 I/O 작업의 진행 상황을 모니터링합니다.
- 반환 값
- 작업이 성공적으로 완료되면 0을 반환합니다.
- 작업이 아직 실행 중이면 EINPROGRESS를 반환합니다.
- 실패한 경우 오류 코드를 반환합니다.
- aio_suspend
#include <aio.h>
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec* timeout);
- 설명
- timeout이 NULL이 아닌 경우, aio_suspend는 지정된 시간 이후에 반환될 수 있습니다.
- timeout이 NULL인 경우, 최소한 하나의 I/O 작업이 완료될 때 반환됩니다.
- 이때 aio_error()는 더 이상 EINPROGRESS를 반환하지 않습니다.
- 매개변수 : struct aiocb에 대한 포인터 배열, 배열의 구조체 수(nent), 그리고 타임아웃 값(timeout)
- 반환 값 : 성공 시 0을 반환하고, 실패 시 -1을 반환하며 errno를 설정합니다.
- aio_cancel
#include <aio.h>
int aio_cancel(int fildes, struct aiocb* aiocbp);
- 설명 : fildes에서 하나 이상의 비동기 I/O 요청을 취소하려고 시도합니다.
- 매개변수
- aiocbp 매개변수는 취소할 요청에 대한 제어 블록을 가리킵니다.
- aiocbp가 NULL이면, 해당 파일 디스크립터(fildes)에서 모든 대기 중인 요청을 취소하려고 시도합니다.
- 반환 값
- AIO_CANCELED: 요청이 성공적으로 취소된 경우
- AIO_NOTCANCELED: 요청 중 하나 이상이 취소되지 않은 경우
- AIO_ALLDONE: 모든 작업이 이미 완료된 경우
- 실패 시 -1을 반환하고 errno를 설정합니다.
'Computer Science > 시스템 프로그래밍' 카테고리의 다른 글
Chapter 12. POSIX Threads (0) | 2024.11.26 |
---|---|
시스템 프로그래밍 chapter09. (타임과 타이머) (1) | 2024.11.19 |
시스템 프로그래밍 chapter 06. (UNIX 스페셜 파일) (0) | 2024.11.05 |
시스템 프로그래밍 chapter 05. (파일과 디렉토리) (1) | 2024.10.29 |
시스템 프로그래밍 chapter 04. (4) | 2024.10.09 |