Basic signal concept
signal은 process의 event의 소프트웨어적인 notification이다. 그래서 signal의 타겟은 process이다. event가 발생했다라고 하는 사실을 target process에게 알려주기 위한 수단으로 signal이 사용된다.
signal이 생성되는 시점은 해당 event가 발생했을때 발생한 target process에게 전달되서 process가 받아들이면 'delivery되었다' 라고 하고 받으면 수신한 signal에대한 처리를 하도록 되어있다. 처리를 하면 해당 signal이 삭제된다.
즉, signal의 lifetime은 생성되고 전달되는 과정까지이다. signal이 target process가 있다고 해서 무조건 전달되는게 아니고 막히는 경우가 있는데 이런 경우 pending signal이라고 한다. 즉, signal이 생성되었는데 deliver되지 못한 경우이다.
예를 들면, 해당 target process가 받지 않겠다고 제어하는 경우이다. signal이 pending 되었다고 해서 pending 된 signal이 삭제되는게 아니고 list에 들어가 대기하게 된다. 나중에라도 process가 받아들일 상태가 되면 다시 list에서 빠져나와서 process에 전달되는 경우도 있다.
Signal을 받았을때 따로 처리 코드가 없다면 default로 정의된 signal handler를 사용한다. siganl이 도착했을 때에 수행해야 할 task를 정의해놓은 것을 signal handler라고 한다.
따로 정의 안했는데 signal 받으면 default signal handler가 실행하게 된다. catch 할 수도 있고 pending할 수도 있다. signal을 받았을때 program에서 나는 어떤 signal이 오면 signal이 왔을때 다른 task를 수행하고 싶다 할때 signal handler 함수를 따로 정의해서 이 signal handler가 호출되게끔 등록할 수 있다.
signal에서 중요한 2개의 함수는 sigaction function과 sigprocmask이다.
sigaction function : 특정 신호에 대해서 어떻게 처리할지 결정하는 함수
#include <signal.h>
int sigaction(int signo, const struct sigaction* act, struct sigaction* oact);
process가 signal을 받았을때 어떤 사용자가 정의한 다른 특정한 액션을 수행하게끔 할때 signal handler 함수를 등록해주는 함수이다. signal handler를 등록할 수 있고 signal handler는 사용자가 user-written function(정의 할 수 있다) Sigaction은 handler 대신에 SIG_DFL, SIG_IGN를 사용할 수 있다.
SIG_DFL : signal이 도착했을때 default action을 취해라
SIG_IGN : signal을 ignore해라(signal을 버려달라)
sigprocmask :
pending signal에서 '벽'같은 signal mask를 가진다. signal mask에 signal 번호를 등록할 수 있는데 등록된 signal이 오면 막히는 거다. 말그대로 masking을 하는 것이다. signal mask를 control 해야 될 필요가 있을때 signal mask제어를 sigpromask로 한다. signal을 받았을 때 어떤 action을 취할 것이냐를 나타내고 등록은 sigaction function으로 한다.
signal 수신할때 제어를 2개의 함수로 할 수 있는 것이다.
process signal mask :
현재 block된 signal들의 list를 포함하고 있다. (contain a list of currently blocked signals) 여기서 block시키고 싶은 signal 목록을 등록시키고 뺄 수 있다. block된 signal은 ignore처럼 버려지는 것은 아니다. block되면 pending signal이라고 얘기하고 pending 된것은 list에 대기. 나중에라도 process로 전달 되기도 한다.
signal mask에서 등록된 것을 삭제할 수도 있다. sigprocmaks를 통해서 signal mask에 signal을 넣거나 뺄 수 있다. 해당 operation을 sigprocmask에 parameter로 등록하게 되어있다.
Generating signals
모든 signal은 symbolic name과 unique한 ide값을 가지고 있다. signal의 이름은 SIG라는 prefix(접두어)를 가지고 있다. SIG로 시작하는 것은 signal.h를 보면 정의가 되어있다.
signal을 생성하는 것은 프로그램상에서 sytstem call이나 shell에서 linux 명령어로 할 수 있다. 보통은 명령어 이름과 시스템 콜 함수 이름이 똑같다. 예시로는 kill 명령어가 있다.
kill -9 명령은 돌고있는 process를 강제 종료할 때 사용한다. kill 이름때매 오해할 수 있는데 kill명령어는 기본적으로 signal을 전송하는 명령어다. kill함수를 통해서 특정 프로세스에게 signal을 보낼수가 있다. kill명령어를 보면 뒤에 2개의 parameter가 온다. 몇번 signal을 보낼건지, 누구에게 보낼건지. 타겟은 process id로 지정하면 된다. signal name으로 signal이름을 지정할 때는 앞에 SIG를 뺀 나머지 부분을 지정하면 된다.
ex> kill -s USR1 3423
ex> kill -9 3423 // (9번 signal을 보내겠다)
(9번은 SIGKILL, 2번은 SIGINT(interrrupt signal)_
-s : symbolic name의 s USR1 signal name 3423은 target process
-l option을 보면 available한 signal 보여준다.
많이 사용하는 signal에 대해서만 집중적으로 살펴볼 예정이다.
강제 종료시키는 명령
kill 함수를 통해서도 signal을 보낼 수 있다. 첫번째 파라미터는 target process id, 두번째는 전송하려는 signal id이다.
#include <signal.h>
int kill(pid_t pid, int sig);
pid 파라미터에는 target process ID인데,
0이 오게 되면 caller's 의 process group의 memeber를 send하고
-1이면 permission이 있는 모든 process에게 send한다(모든 프로세스에게 이 signal을 다 전달해주는 효과를 낼 수 있다)
-10이라고 주면 절댓값 취한게 proces group의 id이다. 10이라는 group에 속한 거에 id를 주는것이다.
같은 방식으로 target process를 지정할 수 있다.
성공하면 0값이 return되고, 실패하면 -1이 return된다.
signal을 만들 때, 특정 목적을 가진 함수가 2가지 함수가 있는데
#include <signal.h>
int raise(int sig);
int raise는 signal을 파라미터로 지정한 signal을 보낸 함수인데 target process는 나 자신이다.
즉, 이 함수를 호출한 process에게 signal을 올리는 함수이다.
성공하면 0을 반환하고, 실패하면 error value를 반환한다.
#include <unistd.h>
unsigned alarm(unsigned seconds)
alarm 함수는 timer의 역할을 수행하는 함수이다. 파라미터로 초단위의 숫자값을 입력하면 내부적으로 타이머가 돌고 타이머가 expire되면 alram이 다되었다고 alarm signal을 보낸다. 이것도 나 자신에게 signal을 보내는 함수인 것이다. parameter로는 초단위의 timer값을 몇 초 뒤에 alarm이 울릴것인지 정한다. 0으로 파라미터를 주면 alarm을 취소시킨다. 프로세스는 알람 signal을 받게됬을때 process는 default로 종료한다. process를 생성해서 process에게 interrupt signal이 도착하고 default 액션은 process 종료하는 것이다.
alarm함수의 리턴값은 unsigned integer -> 남아있는 초값이 return이 된다. 남아있는 시간값이 리턴이 된다. 정상적으로 완료되어 알람이 울리는 거였다면 남아있는 시간이 없으므로 0을 리턴된다. alarm함수는 error를 따로 리턴하지 않는다. unistd.h를 include하고 사용을 하면 된다.
main함수 안에서 alarm하고 10 -> 알람함수를 호출하면 sleep함수처럼 멈추는게 아니고 alarm이 설정되면 바로 return되는 것이다. alarm이 리셋되기전에 return이 되는것이고 밑으로 내려오면 타이머는 10초 돌고 있고 for로 무한 돌려서 alarm 작동하는 것을 test한다.
simplealarm.c
#include <unistd.h>
int main(void) {
alarm(10);
for ( ; ; ) ;
}
Signal sets
#include <signal.h>
int sigaddset(sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigismember(const sigset_t* set, int signo);
signal을 수신했을때 처리되는 함수들을 살펴보기 전에 sigprocmask 함수에서는 signal mask에 등록하거나 뺄 signal을 하나만 등록하는게 아니라 여러개를 사용할 수 있다. Signal sets이라는 data structure을 사용해서 할 수 있다. siganal set data structure에 여러 개를 등록할 수 있다. sigaction 함수에서도 signal set을 파라미터로 넘겨주게 된다. signal set을 어떻게 다루는지 알아볼 필요가 있다. 5개의 함수 제공하고 그 의미를 알면 된다.
1. sigaddset 함수 : sigset type의 변수에다가 signal set에 signal 추가하고 싶을때 사용한다. 두번째 파라미터는 넣고자 하는 signal번호. 그 변수에다가 숫자로 정의가 되어있고 특정 signal추가해 준다.
2. sigdelset 함수 : sigaddset 처럼 이번에는 빼고 싶은 signal이 있을때 사용.
3. sigemptyset 함수 : setset_t라는 집합에서 모든 signal을 제거하고 싶을때 사용한다.
4. sigfillset 함수 : setset_t라는 집합에서 모든 signal을 추가한다.
5. sigismember 함수 : 해당 signal이 있는지 확인할 때 사용한다. 성공하면 0을 리턴하고 실패하면 -1을 리턴한다.
signal set을 가지고 sigprocmask함수를 호출한다.
Signal masks : sigprocmask()
#include <signal.h>
int sigprocmask(int how, const sigset_t* restrict set, sigset_t* restrict oset);
signal mask의 의미 : block시킬려고 하는 signal의 목록을 가지는 것이 signal mask이고 sigset_t 타입으로 관리가 된다. signal mask를 변경하기 위해서 sigprocmask() 를 호출하면 된다.
sigprocmask()에는 3개의 파라미터가 있고 첫번째는 how, signal mask를 어떻게 수정할지 operation지정한다. signal mask에서 추가할 수도 있고 뺄 수도 있다. how에는 이미 constanat값으로 이미 정의된 것이 있다.
(SIG_BLOCK, SIG_UNBLOCK,SIG_SETMAKS) 여기서 'set'은 2번째 파라미터를 의미한다.
SIG_BLOCK : 'set' signal을 add한다
SIG_UNBLOCK : 'set' signal을 delete한다
SIG_SETMASK : 'set' signal을 set한다.
add와 set은 다른 부분이 있다. 다른 signa들이 이미 등록이 된경우 add는 기존의 것을 나두고 추가하는 거고, set은 기존 무시하고 zerobase에서 다시시작하는 것을 의미한다.
여기서 3번째 파라미터도 sigset_t 타입이고 oset은 old set을 의미한다. 이 함수의 output parameter이다. 만약 oset이 NULL이 아니라면, *oset으로 바뀌기전으로 돌린다. sigset으로 수정한 다음에 변경되기전에 원래의 signal mask에 등록된 signal이 있었을건데 이전 Signal의 것을 반환해주는것이다.
sig action에서도 똑같이 사용한다. 변경되기 이전 반환하는 파라미터를 이용하는 경우는 보통 프로세스가 signal mask를 변경해야하는 경우는 중요한 작업을 처리하는 동안만 막아놓고 싶은 경우. 계속 바꾸고 있는것이 아니라. 잠깐 signal mask변경 했다가 원래 signal mask로 돌아가야 하는 경우가 생기니까 원래 signal mask값을 알고있어야한다.
원래의 값으로 다시 돌아갈려면, 다시 sigprocmask를 호출하면서 원래의 signal들로 다시 설정하면 된다. sigprocmaks(SIG_SETMASK,oset,NULL) 이런식으로 호출하면 된다.
sigprocmask()의 성공의 의미는 0, 에러가 나면 -1을 리턴한다. single thread process에만 사용해야 한다. single thread process레벨에서는 sigprocmask함수를 사용하고 만약 다중 thread 프로세스를 실행한다고 하면 pthread_sigmaks()가 사용되어야 한다.
SIGSTOP, SIGKILL과 같은 signal은 signalmask로 막을 수 없다.
Signal masks and sets example
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("Falied to block SIGINT");
sigemptyset으로 signal set을 비우고 SIGINT을 추가해주고 싶다. sigset_t 변수를 먼저 준비해야한다. 초기화부터 하기위해서 sigemptyset으로 비우고 sigaddset으로 sigset변수에 SIGINT변수를 추가하겠다.
else if문 안에서 sigprocmask를 호출한다. 현재 signalmask에 두번째 파라미터로 newsigset을 넘기면 interrupt signal이 추가가 된것이다. interrupt signal(SIGINT)은 pending이 된다.
이번에는 signal set을 잠깐 변경해서 수행하고 다시 되돌려보자.
sigset_t blockmask;
sigset_t oldmask;
if(sigprocmask(SIG_SETMASK, &blockmask, &oldmask) == -1) // add signals to blockmask
return -1;
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) == - 1) // 원래 상태(oldmask)로 되돌리기
return -1;
Catching and ignoring signals
#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 or pointer to function
sigset_t sa_mask; //additional signals to be blocked during execution of handler
int sa_flags; //special flags and options
void (*sa_sigaction) (int, siginfo_t *, void *); // realtime handler
}
sigaction function은 process가 signal받았을때 어떻게 할것이냐. 내가 수행할 task를 signal handler함수로 정의하고 signal을 받았을 때 수행할 action을 등록하는 함수다. sigaction은 파라미터 3개이다.
1) target signal : action할 signal number
2) act : sigaction이라는 구조체 타입으로 어떤 action을 취할지 명시
3) oact : old action 등록된 action 이전의 정보를 반환해주는 output parameter
각 필드값을 설정하고 sigaction을 호출해야 한다. 각 필드를 알고있어야 한다. sigaction에서는 첫번째만 사용할 것이다!
void(*sa_handler)(int); void 리턴타입에 integer 타입에 signal 번호가 전달이 된다.
signal handler는 void를 return하고 하나의 integer parameter를 가진다. sa_handler에는 SIG_DFL(signal의 default action 회복), SIG_IGN(signal을 ignore)를 줄 수 있다.
- signal handler가 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("Falied to install SIGINT signal handler");
Example )
반복문을 돌면서 0과 1사이의 interval에 있는 값인 x에 sin값의 평균을 계속 계산하는 프로그램이다.
뭘 계산하는지는 중요하지 않음. 반복해서 계산을 수행한다는 것이 중요하다. 프로그램이 돌다가 사용자가 Interrupt signal을 ctrl+c를 눌러서 보낸다. Interrupt signal의 default action은 프로그램을 종료하는 것이다. 원래라면 종료가 되버린다. 그런데 이 예제는 중간에 interrupt signal을 받으면 반복문을 돌고있는중에 어느 시점에서 signal이 올지 알 수 없다. Signal handler를 등록해서 flag값을 변경한다.
signal을 받는다는 것은 프로그램에서 asynchronous한 이벤트이다. (언제 발생할지 모른다.) 여기서는 좀더 gracefully terminatie 하도록 수정을 한 예제이다. interrupt signal이 도착하면 signal handler를 등록하는 것이다. signal handler에서는 flag변수를 선언해서 flag변수값을 변경하는 signal handler이다. while문 조건식이 거짓이 되도록 살짝 바꾸고 signal handler에서 return 한것이다. while문을 빠져나가서 정상적으로 종료가 되도록한다.
반복문 돌다가 signal 도착하면 멈추고 signal handler routine이 실행되고 나면 다시 돌아와서 중단되었던 시점 진행한다.
signalterminate.c
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
static volatile sig_atomic_t doneflag = 0;
/* ARGSUSED */
static void setdoneflag(int signo) { //signal받았을때 while문을 빠져나오겠다
doneflag = 1;
}
int main (void) {
struct sigaction act;
int count = 0;
double sum = 0;
double x;
act.sa_handler = setdoneflag; /* set up signal handler */
act.sa_flags = 0; //별다른 옵션 없다
if ((sigemptyset(&act.sa_mask) == -1) ||
(sigaction(SIGINT, &act, NULL) == -1)) { //target signal은 SIGINT
perror("Failed to set SIGINT handler");
return 1;
}
while (!doneflag) {
x = (rand() + 0.5)/(RAND_MAX + 1.0);
sum += sin(x);
count++;
printf("Count is %d and average is %f\n", count, sum/count);
}
printf("Program terminating ...\n");
if (count == 0)
printf("No values calculated yet\n");
else
printf("Count is %d and average is %f\n", count, sum/count);
return 0;
}
setdoneflag는 void return 타입에 int를 갖는 함수로 doneflag값을 1로 설정하는 함수이다. doneflag는 while문에서 doneflag가 0일때 계속 계산하도록 한다.
계산을 signal 받았을때 doneflag를 1로 바꾸면 while문에서 다음조건식 검사할때 바뀌어있으므로 while문 빠져나가도록 조절한다. 그 signal 함수를 act.sa_handler에 등록한 것이다. sigaction을 이용해서 action을 등록한다. 타겟 시그널은 SIGINT가 된다. setdonefla를 &act로 등록하고 변경되기전은 NULL로 둔다. interrupt signal이 오면 setdoneflag함수를 호출해라. 계속 sin(x)값을 더해간다.
프로그램 실행이 순서대로 되지 않고 signal이 도착하면 점프해서 signal handler루틴이 진행하고 리턴되면 원래로 돌아온다. 고려해야되는 부분은 doneflag라는 변수는 critical section으로 처리해야 한다. 주로 다중 프로세스 또는 다중 스레드를 다룰때 사용한다. 어떤 변수가 있을때 변수를 여러 프로세스가 접근할 수 있다면 critical section으로 처리해야한다. 동시에 수정한다면 문제가 발생하기 때문이다(conflict)
여러 변수가 동시에 접근 못하도록 critical section으로 만들기 위해 mechanism을 제공한다. process간 동기화할 때 다시 얘기한다. 여러 프로세스, 여러 스레드 간에 동기화 하는 부분은 아니지만, 순서대로 access 하도록 제어를 해준다. 시스템 콜 함수 같은 것을 사용해야 한다. doneflag라는 변수는 메인 함수에서도 access 하고 signal handler에서도 access 할 수 있어야한다. signal handler와 main program이 access할 수 있는 변수이다. signal handler에 의해서 doneflag에 동시에 접근할 수 있기때문에. 여기서도 while문을 계속 반복해서 사용하는데 while문에서 doneflag를 읽을려 하는데 그때 signal이 도착을 했을때. -> 읽는 작업을 중단하고 signal 핸들러를 호출한다.
이 작업은 마치 양쪽에서 doneflag를 서로 다른 작업으로 access하는것과 같은 효과가 일어난다. 충돌문제가 똑같이 발생할 수 있다. doneflag를 critical section으로 만든다. 어떻게 critical section으로 만들었느냐? doneflag는 일반 int값이였는데 sig_atomic_t로 선언한게 critical section으로 만든것이다. 이 타입으로 선언하면 OS레벨에서 access하기 시작했으면 종료될때까지 OS가 막아준다. volatile 키워드로 변수를 사용하면 doneflag에 access할때 C compiler에 대해 항상 메모리에서 직접 참조를 해라 라고 선언한것이다. 최적화 과정중 register값을 그냥 참조한다. volatile로 선언하면 register에서 load하지말고 메모리에서 직접 읽어라. doneflag는 외부에서 변경될 여지가 있기 때문에. 외부는 signal handler를 의미한다. 매번 메모리에서 읽어라고 하는 것이다.
program 8.6
averagesin.c
반복문으로 수행하다가 10000번째 iteration마다 중간계산 결과를 buffer에다가 저장하고 그다음에 계속 반복하고 10000번째 계산 결과를 저장하고 이런식으로 반복작업을 수행. target signal에 SIGUSR1이 도착하면 signal handler에서는 buffer의 내용을 읽어서 화면에 출력해 준다. main 부분과 signal handler가 동시에 buffer에 접근할수도 있다. 그러므로 buffer access하는 부분을 critical section으로 바꿔야 한다.
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE 100
static char buf[BUFSIZE];
static int buflen = 0;
/* ARGSUSED */
static void handler(int signo) { /* handler outputs result string */
int savederrno;
savederrno = errno;
write(STDOUT_FILENO, buf, buflen);
errno = savederrno;
}
static void results(int count, double sum) { /* set up result string */
double average;
double calculated;
double err;
double errpercent;
sigset_t oset;
sigset_t sigset;
if ((sigemptyset(&sigset) == -1) ||
(sigaddset(&sigset, SIGUSR1) == -1) ||
(sigprocmask(SIG_BLOCK, &sigset, &oset) == -1) )
perror("Failed to block signal in results");
if (count == 0)
snprintf(buf, BUFSIZE, "No values calculated yet\n");
else {
calculated = 1.0 - cos(1.0);
average = sum/count;
err = average - calculated;
errpercent = 100.0*err/calculated;
snprintf(buf, BUFSIZE,
"Count = %d, sum = %f, average = %f, error = %f or %f%%\n",
count, sum, average, err, errpercent);
}
buflen = strlen(buf);
if (sigprocmask(SIG_SETMASK, &oset, NULL) == -1) //이전 상태로 되돌린다(USR1넣은거 되돌리기)
perror("Failed to unblock signal in results");
}
int main(void) {
int count = 0;
double sum = 0;
double x;
struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
if ((sigemptyset(&act.sa_mask) == -1) ||
(sigaction(SIGUSR1, &act, NULL) == -1) ) {
perror("Failed to set SIGUSR1 signal handler");
return 1;
}
fprintf(stderr, "Process %ld starting calculation\n", (long)getpid());
for ( ; ; ) {
if ((count % 10000) == 0)
results(count, sum);
x = (rand() + 0.5)/(RAND_MAX + 1.0);
sum += sin(x);
count++;
if (count == INT_MAX)
break;
}
results(count, sum);
handler(0); /* call handler directly to write out the results */
return 0;
}
results함수는 10000번째마다 buffer에 쓰는 함수. handler는 화면에 출력해주는 signal handler. snprintf는 buffer에 print하는 함수다. String값을 출력한다. 제일 끝에 nulll을 추가해준다.(snprintf)
메인에서는 for문에서 무한으로 반복하다가 10000번째에 results함수를 호출하고 usr handler signal오면 handler함수로 뛰어서 write 함수를 사용한다.
USR1 signal이 오면 중간계산 결과를 읽어서 출력해준다. 화면에 지금까지 계산결과를 읽어서 출력해준다. buffer를 main과 signal handler가 동시 접속한다.
어떻게 buffer부분을 critical section으로 만들것이냐. signal에 의해서 방해받지 않으면 된다. result 함수 내에서 buffer를 다 쓰고 난다음에 signal 제어하기 위해서 signalmask를 사용했는데 sigprocmask를 사용해서 signal을 하나 막으면 된다. (USR1 signal을 block시키고 buffer를 access한다) 방해받지 않고 buf를 access할 수 있다. 다시 문을 열고 buffer를 access하는 동안 critical section이 될 수 있다. 방해받지 않고 buffer를 access 할 수 있다. USR1 signal이 도착할 것이고 완료된 버퍼의 내용을 읽고 화면에 출력한다.
마지막 if에서 sigprocmask를 다시 호출해서 &oset으로 다시 되돌린다. (이전 상태로 되돌린다)혹시나 pending되었던 USR1 signal있으면 다시 process에 되돌릴 수 있는것이다.
Waiting for signals
main을 진행하다가 특정 signal이 오면 그 다음 작업을 진행하고 싶을때 다음 작업이 시작되기 위해 특정 signal이 온다. system call함수로 signal을 기다리는 함수가 제공된다. -> pause(), sigsuspend(), sigwait()
pause함수
#include <unistd.h>
int pause(void);
파라미터가 없다. 프로세스가 pause를 동작시키면 signal이 전달이 될때까지 calling thread를 suspend 시킨다.
user-defiend handler가 실행될 수 있는 시그널이 오면. pause를 이용해서 아무 시그널 오면 동작하는게 아니라 특정 시그널이 오면. 다른 시그널이 오면 다시 잠들고 하다가 원하는 signal이 오면 깨서 다음 작업을 수행하고 싶은것이다. pause함수는 항상 -1을 리턴하고 만약에 signal이 process에서 catch 되었으면 signal handler가 불린다. signal handler가 불려서 다 실행이 되고 단다음에 pause함수가 리턴이 된다. 원하는 signal이 올때까지 기다리도록 밑에 함수를 사용한 것이다. while문으로 원하는 signal 올때까지 기다림. -> sig_atomic_t 타입으로 sigreceived를 선언함. 0인 동안의 pause로 계속 suspend 하겠다. 다른 signal이 도착하면 signal handler가 불리고 같은지 비교하고 리턴한다. 그럼 pause함수도 리턴되고 sigreceived가 0이기 때문에 다시 pause함수 부르고 잠들겠다.
이 코드는 완벽한 코드가 아니다! (8.21)
문제 상황 :
타이밍의 문제 -> signal은 asynchronous이기 때문에 애매한 시점에 동작하게 되면 시스템이 오동작할 수 있다. while의 조건식을 검사해서 0이다라는 것을 확인하고 pause를 호출하려고 하는데 target signal이 이때 도착했다면 중단하고 signal handler가 불린다. sigreceived를 1로 바꾸고 리턴한다. pause함수를 부르고 thread는 suspend된다. on target signal을 알아차리지 못하고 잠들었다. 만약 target signal이 마지막이였다면 thread는 잠들 수 밖에 없다.
해결하기 위한 방법? -> signalmask를 제어해서 막은 다음에 진행하면 되지 않을까? while 위에서 막으면 언제 풀어줘야하나? 다시 풀고 pause함수를 부르면 되는건데 pause부르기 전에 unblock하면 되는데 이것도 문제이다. 문을 열어놓고 부르겠다 해도 두 함수호출은 automatic한 것이 아니라서 또 문제가 생길 수 있다. 동시에 수행하도록 해야 된다.
-> 시스템 콜 함수 sigsuspend를 사용한다.
Sigsuspend(pause는 불안하니까 이걸 사용해라)
#include <signal.h>
int sugsuspend(const sigset_t* sigmask);
파라미터로 sigset_t 타입의 포인터를 넘겨준다. -> 두가지 작업 동시 진행해준다.
파라미터로 넘어온 sigmask를 signal mask로 설정하고 호출한 process를 suspend시키는 작업을 동시에 수행해준다. 이 함수도 언제 깨어나냐 하면 프로세스가 signal을 캐치하면 리턴된다. pause함수와 마찬가지로. sigmask block시켰던 target을 unblock하는 걸로 이용하면 사용을 할 수 있겠다. target signal을 막아놓고 풀면서 suspend하기 위해서 sigsuspend호출할때 target signal을 뺀 signal set을 넣어준다.(sigmask는 target signal을 뺀 sigset) process가 suspend 되면서 문을 열어줘야 target signal이 도달할 수 있다. sigsuspend가 리턴이 되면 변경되었던 부분이 원래 상태로 자동으로 복구가 된다. sigsuspend를 잘 이해해야 한다.
sigsuspend가 깨어날려면 target signal이 와야한다. 다른 signal은 도착하지도 않기 때문에. while문이 아니라 if문으로 바꼈다. signal set 변수를 3가지를 준비한다. maskall에는 모든 signal을 담을 것이다. maskmost에는 target signal만 빼놓은 것. 다른 signal은 도착도 못한다. maskold는 이전 signal 저장을 위해서. 모든 signal로 다 채우고 다 채운다음에 sigdelset에서 targetsignal만 뺀것이다. amskmost signalset은 target signal만 빠짐. 아무 시그널이나 도착해도 pending이 된다. sigsuspend(&maskmost); targetsignal만 문을 통과할 수 있다.
static volatile sig_atomic_t sigreceived =0;
sigset_t maskall, maskmost, maskold;
int signum = SIGUSR1; //target signal
sigfillset(&maskall); //모든 signal
sigfillset(&maskmost); //target signal제외 모든 signal(일단 여기서는 fillset으로 다 채워줌)
sigdelset(&maskmost,signum);
sigprocmask(SIG_SETMASK,&maskall,&maskold);
if(sigreceived ==0 )
sigsuspend(&maskmost); // target signal 도착했을때만 깨어난다. -> while문 굳이 필요없다.
sigprocmask(SIG_SETMAKS,&maskold,NULL);
이전에는 다른 signal까지 모두 막았지만 여기서는 다른 signal도 통과했을때도 깨어날 수 있게 -> while문으로 변경됨. signalset_t 이 3가지 사용. maskblocked, maskold, maskunblocked, 2개의 signalset을 준비. 처음에 sigprocmask를 써서 &maskblocked의 현재 시그널 번호들을 가지고 와서 양쪽에 모두 maskunblocked도 기존의 original set 정보를 가지고 온다. 새로 설정되는게 아니라 변경되기전 signalmask값을 얻어오기 위해서 maskblocked에는 target signal 추가한 것. maskblocked는 original에서 target signal만 추가한것.
maskunblocked는 original에서 target signal 뺀 버전.
다른 버전 - target signal 말고 다른 signal은 허용
static volatile sig_atomic_t sigreceived =0;
sigset_t maskblocked, maskold, maskunblocked;
int signum = SIGUSR1;
//원래 signal set 정보를 다 가져옴(변경되기전을 가져오기 위함)
sigprocmask(SIG_SETMAKS, NULL, &maskblocked);
sigprocmask(SIG_SETMAKS, NULL, &maskunblocked);
sigaddset(&maskblocked, signum); //target signal을 더한다(original + target signal)
sigdelset(&maskunblocked, signum); //target signal을 뺸다 (original - target signal)
sigprocmask(SIG_BLOCK, &maskblocked, &maskold);
while(sigreceived ==0)
sigsuspend(&maskunblocked);
sigprocmask(SIG_SETMASK, &maskold, NULL);
target signal이 오면 막고 나머지는 while문을 실행하겠다. 막는게 중요하다. 다른 signal은 도착하도록 두겠다. process를 suspend시킴과 동시에 targetsignal에서 빼고 프로세스가 잠든것이다. targetsignal도 도착할 수 있고 아닌 signal도 도착하는것. target signal이 도착하면 1로 변경하고 return하면 위로 올라가서 1로 변경된것을 알고 while문을 빠져나가서 signal mask를 원래대로 돌려놓고 수행한다. sigsuspend 리턴될때 원복된다. 원래로 되돌리기 위해서 sigprocmask를 호출함.
Sigwait
#include <signal.h>
int sigwait(const sigset_t *restrict sigmask, int *restrict signo);
int* restrict signo 는 output parameter라고 생각하면 된다.
sigmask에 있는 signal이 오기전에 계속 block된다.
sigwait함수의 작동방식 :
호출하게 되면 프로세스는 block이 된다. 첫번째 파라미터(sigmask)에서 지정한 signal들 중에 아무거나 pending이 되면, (pending이 되었다는 것은 signal들이 signalmask에 의해 막혔다는 얘기) sigwait함수는 peding된 signal을 pending list에서 삭제한다. 그리고 그냥 return을 한다. (sigset_t에는 signal들을 담을 수 있다) return을 하면서 삭제한 signal번호를 signo(두번째 파라미터)로 반환을 해준다. (즉, 원하는 signal을 받으면 signo에 담아서 return 해주겠다는 의미)
sigwait함수를 가지고 어떻게 내가 원하는 signal이 올때까지 기다릴 수 있겠느냐 :
(sigsuspend와는 동작이 조금 다르다 -> sigsuspend함수는 signalset type의 parameter가 있었는데 이 signalset parameter에 우리가 기다리는 target signal을 뺀 signalset을 넣었었다.)
sigwait에서는 우리가 원하는 signal을 sigset에 넣어놓고 호출하게 된다. sigwait에서도 sigsupsend와 마찬가지로 sigprocmask로 target 하는 signal을 일단 막고 시작하는 것은 동일하다. 그래서 우리가 원하는 signal이 pending이 되야 sigwait함수가 pending된 것을 삭제하고 return을 해주는것, sigsuspend함수는 signal mask를 직접 건드렸었다. 그런데 sigwait함수는 signal mask를 전혀 건드리지 않는다)
ex>
1 #include <signal.h>
2 #include <stdio.h>
3 #include <unistd.h>
4
5 int main(void) {
6 int signalcount = 0;
7 int signo;
8 int signum = SIGUSR1; //target signal
9 sigset_t sigset;
10
11 if ((sigemptyset(&sigset) == -1) || //일단 비우고
12 (sigaddset(&sigset, signum) == -1) || // target signal
13 (sigprocmask(SIG_BLOCK, &sigset, NULL) == -1)) //target signal을 sigmask에서 block
14 perror("Failed to block signals before sigwait");
15 fprintf(stderr, "This process has ID %ld\n", (long)getpid());
16 for ( ; ; ) {
17 if (sigwait(&sigset, &signo) == -1) { //USR1이 pending되길 기다린다
18 perror("Failed to wait using sigwait");
19 return 1;
20 }
21 signalcount++;
22 fprintf(stderr, "Number of signals so far: %d\n", signalcount);
23 }
24 }
countsignals.c를 background에서 돌려보기.
Errors and Async-signal safety
process의 실행흐름이라는 것이 언제든 signal이 도착하면 하던 작업을 멈추고 signal handler가 호출이 되어야 하기 때문에 실행에 점프가 생긴다. 그럼에 따라서 발생하는 문제들이 있을 수 있다. 그래서 고려해야 하는 상황들이 있다.
1. signal에 의해서 interrupt된 POSIX functions(시스템 콜 함수) -> 함수를 호출했는데 그 시스템 콜 함수가 -1(에러)를 리턴하고 에러 코드가 interrupt인 경우.
: 다시 시작을 해야 될건지 말건지를 한번 살펴봐야 한다. -> 메뉴얼 페이지 같은 것을 확인해서 그 함수가 signal에 의해서 interrupt되는지 확인해 볼 필요가 있다. EINTR로 설정된다면 중간에 자기 task를 수행 못하고 외부 요인에 의해서 -1을 리턴한 함수이다. 그렇지 않은 함수도 있다.
어떤지 한번 살펴보고 interrupt 될 수 있는 함수라면 다시 호출을 해서 explicit하게 호출하던지 r_~함수로 개선한것처럼 다시 호출하던지. 아니면 직접 -1을 에러를 리턴한 경우 다시 호출하도록 다시 호출하던지. 먼저 내가 사용하는 함수가 interrupt될수있는지 아닌지 man page로 확인!
2. signal handler를 사용할 떄 signal handler가 nonreentrant 함수를 호출하는 경우 위험할 수 있다. signal handler안에서 signal handler함수를 구현할 때, 그 안에서 호출하는 다른 함수는 nonreentrant 함수를 호출하는 것을 피하는게 좋겠다. reentrant함수라는게 어떤 함수를 호출했는데 그 함수의 동작이 끝나지 않았는데 다시 누군가에 의해서 함수가 다시 호출이 되어도 문제없이 실행이 되는 함수이다. func()라는 함수가 있는데 이 함수를 호출하면 함수안에 있는 내용이 차례로 실행이 되다가 중단이 되고, 다른 thread가 func()를 호출하는 것이다. 그럼 실행하던 구문이 있는데 새로 func()가 호출되는데 문제없이 task가 수행되면 reentrant, task에 수행이 있어서 문제가 발생하면 nonreentrant함수라고 얘기를 한다.
결론은 signal handler함수 작성할때 함수안에서 호출하는 함수는 reentrant한 함수를 호출해라. signal 함수 안에서 호출해도 안전한 함수 -> async-signal safe한 함수이다. POSIX library에서 제공하는 많은 system call 함수 중에 많은 함수들이 async-signal safe한 함수가 아니다. 목록은 table 8.2에 나온다. table에 있는 것은 안전.
---> averagesin.c
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE 100
static char buf[BUFSIZE];
static int buflen = 0;
/* ARGSUSED */
static void handler(int signo) { /* handler outputs result string */
int savederrno;
savederrno = errno;
write(STDOUT_FILENO, buf, buflen);
errno = savederrno;
}
static void results(int count, double sum) { /* set up result string */
double average;
double calculated;
double err;
double errpercent;
sigset_t oset;
sigset_t sigset;
if ((sigemptyset(&sigset) == -1) ||
(sigaddset(&sigset, SIGUSR1) == -1) ||
(sigprocmask(SIG_BLOCK, &sigset, &oset) == -1) )
perror("Failed to block signal in results");
if (count == 0)
snprintf(buf, BUFSIZE, "No values calculated yet\n");
else {
calculated = 1.0 - cos(1.0);
average = sum/count;
err = average - calculated;
errpercent = 100.0*err/calculated;
snprintf(buf, BUFSIZE,
"Count = %d, sum = %f, average = %f, error = %f or %f%%\n",
count, sum, average, err, errpercent);
}
buflen = strlen(buf);
if (sigprocmask(SIG_SETMASK, &oset, NULL) == -1)
perror("Failed to unblock signal in results");
}
int main(void) {
int count = 0;
double sum = 0;
double x;
struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
if ((sigemptyset(&act.sa_mask) == -1) ||
(sigaction(SIGUSR1, &act, NULL) == -1) ) {
perror("Failed to set SIGUSR1 signal handler");
return 1;
}
fprintf(stderr, "Process %ld starting calculation\n", (long)getpid());
for ( ; ; ) {
if ((count % 10000) == 0)
results(count, sum);
x = (rand() + 0.5)/(RAND_MAX + 1.0);
sum += sin(x);
count++;
if (count == INT_MAX)
break;
}
results(count, sum);
handler(0); /* call handler directly to write out the results */
return 0;
}
--> signal handler함수 내에서 printf함수는 async signal safe한 함수가 아니다. 대신 write함수는 async-safe한 함수이다. (그래서 위에서 printf함수 대신에 write함수를 사용하였다)
3. errno변수를 다룰때
- main()이 있고 signal handler()함수가 있다고 가정할때 main()에서 a()라는 systemcall을 호출하고 -1이 리턴되었을때 errno=ERROR코드가 설정되었을때, error handling을 실행하는 와중에 signal이 와서 작업을 중단하고 signal handler가 호출되었는데 signal handler에서 진행하면서 이 안에서 또 시스템 콜 함수b()를 호출했는데 이 함수도 error가 리턴이 되었다(error코드 설정되는 함수) 이 errornumber함수에 새로운 것이 덮어씌워지게 된다. error코드가 main에서 설정되었던것이 덮어씌워진다. main에서 볼려고 했던 error 코드가 없어졌다. main에서 중단되었던 부분에서 error코드를봤는데 main에서 원래 error코드가 아니라 signal handler에서의 error코드를 보게 되는 문제점이다. 피할려면 signal handler함수에 진입했을때 혹시나 main에서 errorcode를 사용하려 했을때 errornumber를 임의의 temp =errno에 저장해두고 사용해라. 그다음에 b()함수를 호출하면 원래의 errornumber가 다른 함수에 있기 때문에 저장해놓은 값을 b()호출한뒤 되돌리면 이런 문제를 해결할 수 있다.
--> savederrno가 temp 역할 하고 있다.
static void handler(int signo) { /* handler outputs result string */
int savederrno;
savederrno = errno;
write(STDOUT_FILENO, buf, buflen);
errno = savederrno;
}
의심스러운 상황일때는 restart library 함수를 호출을 하자.
Program control
: program 실행 sequence를 jump 할 수 있는 system call함수의 동작에 대해서.
실행 구문을 점프하는 것이 필요한 경우
1. 프로그램들이 siganl을 이용해서 error handling할때 사용할 수 있다.
2. 긴계산 작업이였는데 ctrl+c 로 프로세스가 종료되는 대신에 처음부분으로 가서 돌아가는 경우.
Ex)
sequence를 변경하기 위해서는 indirect한 방법으로도 할 수 있는데 ctrl+c 로 interrupt signal이 왔을 때 응답으로 flag변수를 하나 설정해서 뭐냐에 따라서 어떤 구문을 실행하게 프로그램 짜기(복잡함)
-> signal이 왔을때 실행 sequence를 변경하기 위한 POSIX에서 제공하는 sigsetjmp , siglongjmp 를 사용할 수 있다.
jump 할 지점 설정하기, 실제 설정했던 지점으로 jump시키는 함수.
Sigsetjmp and siglongjmp
#include <setjmp.h>
void siglongjmp(sigjmp_buf env, int val);
int sigsetjmp(sigjmp_buf env, int savemask);
sigsetjmp()
- 점프할 지점 설정 -> 성공하면 0이 리턴. 리턴되는 case가 2가지이다. (fork()에서 자식,부모 다르게 리턴되는 것처럼)
---> 직접 sigsetjmp를 호출하면서 jump할 지점 설정했을때 0 리턴. 나중에 다시 점프해서 돌아왔을때 리턴값 받아왔을때 아래 실행. 이때는 longjmp 2번째 파라미터값이 이리로 리턴된다.
- 첫번째 파라미터 : sgjmp_buf env안에 현재 이시점에 실행 context를 저장해놓는 변수. 이 지점으로 점프할꺼다라는 것이 저장.
- 두번째 파라미터 : savemask -> 0이 아닌 값이면 정보를 저장할때 jump될때 실행했을 당시에 signalamsk도 저장한다.
0이면 저장하지 않고 현재 signal mask값을 저장할지 안할지 설정
sigsetjmp의 값이 0이면 jump할 위치를 설정한 것이다. 0이 아니면 다시 jump해서 돌아온것이다.
siglongjmp() -> 저장된 위치로 다시 제어를 전달
- 어디로 jump 할지 위치는 1번째 파라미터에 저장. 점프할 때 점프한 지점으로 리턴시킬 값이 2번째 파라미터. 첫번째 파라미터는 점프할때 여러지점에서 sigsetjmp 호출했으면 어디로 jump할건지 지정할때 첫번째 파라미터로 점프할 지점 구분할 수 있다.
ex>sigjmp.c
1 #include <setjmp.h>
2 #include <signal.h>
3 #include <stdio.h>
4 #include <unistd.h>
5
6 static sigjmp_buf jmpbuf; //점프할 지점을 설정할 때 사용하는 변수
7 static volatile sig_atomic_t jumpok = 0; // flag 변수 역할 수행, jump해도 되느냐 1이되면 점프해도 된다.
8
9 /* ARGSUSED */
10 static void chandler(int signo) {
11 if (jumpok == 0) return; //준비상황이 안됐으면 그냥 return
12 siglongjmp(jmpbuf, 1); //준비되었으면 jmpbuf에 설정된 곳으로 jump하고 return 1로 전달
13 }
14
15 int main(void) {
16 struct sigaction act;
17
18 act.sa_flags = 0;
19 act.sa_handler = chandler;
20 if ((sigemptyset(&act.sa_mask) == -1) ||
21 (sigaction(SIGINT, &act, NULL) == -1)) {//target signal을 SIGINT이다 ctrl+c누르면 jump를 하겠다
22 perror("Failed to set up SIGINT handler");
23 return 1;
24 }
25 /* stuff goes here */
26 fprintf(stderr, "This is process %ld\n", (long)getpid());
27 if (sigsetjmp(jmpbuf, 1)) //jmpbuf에 필요한 정보 저장, signalmask에 1저장-> 최초는 0-> 출력 x
28 fprintf(stderr, "Returned to main loop due to ^c\n"); //jump해서 돌아왔을때만 출력
29 jumpok = 1; //점프할 준비가 되었음.
30 for ( ; ; ) //대기중-> 안그러면 process 끝나버림.이 상황에서 ctrl+c누르면 signalhandler호출.-> sigsetjmp로 jump하는 것이다.
31 ; /* main loop goes here */
32 }
Programming with asynchronous I/O
-Asynchronous I/O : 비동기 I/O 수행하는 것이 signal과 상관이 있음. 이때까지 호출했었던 system call 함수는 synchronous함수를 호출한다. read() write()는 synchronous한 I/O. 함수를 호출한 다음에 함수 task완료될때까지 기다리고 task완료되면 다음으로 하는. read나 write나 요청한 바이트만큼 못해도 1바이트라도 하면 자신의 일을 수행한것이다. asynchronous로 호출이 되었다 하면 지금 읽을 데이터가 10byte를 읽으라고 했는데 10byte가 안되었으면 read가 블럭되서 기다리지 않고, 읽을 수 없는 상황이면 바로 return하고 백그라운드에서 OS에 의해서 계속 진행을 하게 된다. 함수는 리턴을 한다. 요청한 I/O가 끝나지 않았지만 마냥 기다리지 않고 다른 task를 수행할 수 있게 되는것이다. 대신 문제는 다른 task를 수행 하다가 요청한 background 진행되는 I/O가 완료되었다면 다른 task수행하고 있는것에서 어떻게 알 수있게 되느냐? 요청된 I/O가 완료되었을 때 알 수 있는 방법을 제공해 준다. (백그라운드에서 진행되고 있는 작업이 종료되었다는 사실을 알려주는 것이다)
-> aio_read(), aio_write(), aio_return(), aio_error()
별도로 확인하고 I/O가 일어난 byte수가 제공이 되는 것이 aio_return() 과 aio_error()함수이다. 대신 async는 sync보다 프로그램 로직이 복잡해질 수 있다.
#include <aio.h>
int aio_read(struct aiocb* aiocbp);
int aio_write(struct_aiocb* aiocbp);
(함수의 역할만 살펴봄)
parameter타입이 aiocb라는 구조체 타입이다. aiocb라는 구조체 변수를 먼저 준비해야한다. 구조체 안에 field중에 기본 3가지(write,read에서 있었던 파라미터)가 들어가있다.
aio_read() : 읽기 작업을 위한 요청을 async하게. 요청이 queue에 들어가서 진행된다. 성공은 0, 아니면 -1
aio_write() : 쓰기 작업을 위한 요청 async.
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 : sigevent type의 구조체를 사용해야될때가 있다. I/O가 끝났다는 사실을 통보받기. 함수를 직접 호출해서 완료가 되었는지 확인하는 방법. 또다른 방법은 field를 이용하는 방법. I/O가 완료되었을때 OS가 signal로 통보를 받을건지 말건지. 몇번 signal로 통보받을지 field에서 결정할 수 있다. 다른 것을 수행하다가 signal handler에서 수행하도록 코드를 짜면 된다. 완료되었다는 것을 통보받을 수도 있다. field안에 sigev_notify라는 field가 있고 NONE으로 설정하면 통지받지 않겠다는 뜻이고 몇번 signal로 할거냐면 signo field안에 통지받고자 하는 signal number를 설정해두면 된다.
aio_return()과 aio_error()에는 완료된 I/O의 return값을 받기 위한 위에서 따로 호출하는 함수. 완료된 I/O의 return값을 받아올 수있다. 먼저 비동기 I/O가 return되었는지 확인해야 한다. 아니면 수동으로 error()로 확인하면 된다. 진행상황을 monitor하는 함수이다. 만약에 0이면 완료되었다는 뜻이고 진행중이면 EINPROGRESS값이 반환된다. 아니면 error code값이 리턴된다.
aio_suspend : 호출한 프로세스를 기다린다. 파라미터로 지정한 asynchronous가 완료될때까지 기다릴때 호출할 수 있다. 완료가 되면 return을 하는 함수이다. 파라미터 1: 요청한 구조체의 array(여러개의 aiocb값) 2: array element의 개수 3: timeout값 마냥 기다리는 것이 아니고 timeout 완료되면 return을 하겠다.
aio_cancel : I/O를 중단시키고 싶다. cancel 시키고 싶다. 1: target fildes 2: control block을 지정. NULL이면 모든 요청 취소해 달라. fildes만 지정하면 된다. AIO_CANCELED이면 성공. AIO_ALLDONE이면 이미 완료가 되었다.
'CS > 시스템 프로그래밍' 카테고리의 다른 글
Critical sections and Semaphores (0) | 2021.12.10 |
---|---|
Thread Synchronization (0) | 2021.12.01 |
POSIX Threads (0) | 2021.11.23 |
Times and Timers (0) | 2021.11.16 |
UNIX Special Files (0) | 2021.11.03 |