Basic Signal Concept
- 시그널은 이벤트가 발생한 것을 알려주려 프로세스에게 보내는 일종의 통지다.
- 시그널은 생성된 후 부터 전달 될 때 까지가 lifetime이다.
- 생성되었지만 아직 전달되지 않은 시그널을 pending signal이라고 한다.
- 프로세스는 signal handler에 의해서 전달된 signal을 포착한다.
- sigaction function
- 프로그램은 sigaction이라는 함수를 통해 signal handler를 등록할 수 있으며, signal handler는 유저가 직접 지정할 수 있다.
- Sigaction은 또한 이미 저장된 SIG_DFL이나 SIG_IGN를 등록할 수 있다.
- SIG_DFL : default action, 기본으로 등록
- SIG_IGN : ignore the signal, 시그널을 무시
- Process signal mask
- signal중 전달되지 못하도록 막아야 하는 signal 정보를 가지고 있다.
- 막힌 시그널은 무시되는 시그널처럼 프로세스에 도달하지 못한다.
- 무시된 시그널과 다른 점은 막힌 시그널은 pending list에 들어가서 다시 전달될 때 까지 기다리게 된다.
Generating signals
- 모든 시그널은 고유한 이름과 ID값을 가지고 있다.
- 이름은 SIG로 시작한다.
- kill 명령어를 통해 signal을 생성할 수 있다.
- kill은 강제 종료하는데 많이 사용되지만, 사실은 시그널을 전송하는 명령어이다.
- kill 명령어는 2개의 매개변수가 온다.
- 보낼 시그널의 종류 : 고유한 이름이나 ID를 보낸다.
- 받는 프로세스 ID : 프로세스의 PID를 보낸다.
- kill –l을 통해 kill 명령어들의 목록을 볼 수 있다.
Kill function
#include <signal.h>
int kill(pid_t pid, int sig);
- pid_t pid : 받는 프로세스의 PID이다.
- 만약 0이 오면, 호출한 프로세스의 그룹인 모든 프로세스를 종료시킨다.
- 만약 -1이 오면, 허가된 모든 프로세스를 종료시킨다.
- sig : 사용하는 시그널의 ID다.
Raise function
#include <signal.h>
int raise(int sig);
- raise함수는 kill함수와 비슷하지만 pid값을 받지 않는다.
- 프로세스를 지정하지 않고 나 자신에게 시그널을 보낸다.
- return value는 0이면 성공이며, 0이 아닌 오류 코드이면 errno를 보낸다.
Unsigned alarm
#include <signal.h>
unsigned alarm(unsigned seconds);
- 설정한 시간 초가 지난 후 나 자신에게 알람 시그널(SIGALRM)을 보낸다.
- 시간 초를 0으로 하면 지금 실행하고 있는 알람을 취소하는 역할을 한다.
- 알람 시그널을 받았을 때의 기본 설정은 프로세스의 종료이다.
- return value
- 알람 시그널을 보낼 때 까지의 시간을 반환한다.
- 만약 설정해 놓은 알람이 없으면 0을 반환한다.
- error는 반환하지 않는다.
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 set은 시그널을 받아서 저장하는 하나의 저장소이다.
- 5개의 함수로 조정할 수 있다.
- int sigaddset(sigset_t* set, int signo) : 지정한 signal set에 signal을 추가한다.
- int sigdelset(sigset_t* set, int signo) : 지정한 signal set에서 signal을 삭제한다.
- int sigemptyset(sigset_t* set) : 지정한 signal set을 모두 비운다.
- int sigfillset(sigset_t* set) : 지정한 singal set에 등록할 수 있는 시그널을 모두 채운다.
- int sigismember(const sigset_t* set, int signo) : 지정한 signal set에 signal이 있는 지를 확인다.
Signal mask
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
- Signal mask
- 지금 block 할 signal 목록들을 전달해준다.
- sigset_t 타임의 ls이다.
- sigprocmask 함수를 통해 signal mask를 조정할 수 있다.
- sigprocmask()
- int how : 주어진 시그널을 어떻게 처리할지를 알려주는 매개변수다.
- SIG_BLOCK : 주어진 sigset을 block하도록 singla mask에 추가한다.
- SIG_UNBLOCK : block하도록 추가해놓은 sigset을 삭제한다.
- SIG_SETMASK : 주어진 sigset으로 signal mask를 바꾼다.
- const sigset_t *restrict set : 미리 만들어놓은 sigset을 넘겨준다.
- sigset_t *restrict oset : sigpromaske를 하기 전에 signal mask의 정보를 넘겨준다.
- return value는 성공하면 0이며, 에러가 발생하면 -1이다.
- sigprocmask는 single thread에서만 사용한다.
- 특정 signal(SIGSTOP, SIGKILL)은 block되지 않는다.
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);
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction) (int, siginfo_t *, void *);
}
- signal을 받거나 무시하는 건 sigaction 함수를 통해 가능하다.
- 매개변수는 다음과 같다.
- int sig : 받았을 때 특정 행동을 수행해야하는 시그널
- const struct sigaction *restrict act : 이 구조체로 정의한 action을 수행한다.
- struct siganction *restrict oact : 이전 action을 반환한다.
- Return value는 성공하면 0이며, 실패하면 -1이다.
- sigaction 구조체는 아래와 같이 이루어져 있다.
- void (*sa_handler)(int) : function pointer로, signal handler 함수를 등록할 수 있다. return value는 void이고, int를 매개변수로 받는 함수만 등록할 수 있다. 여기에 SIG_DFL, SIG_IGN을 등록할 수 있는데, 각각 기본 명령을 수행하거나 무시하라는 함수이다.
- sigset_t sa_mask : signal handler 함수가 실행 될 동안 block시킬 signal이 존재한다면 여기서 등록할 수 있다.
- int sa_flags : 특별한 옵션을 설정할 수 있다.
- void (*sa_sigaction) (int, siginfo_t *, void *) : signal handler를 등록할 수 있다. 맨 처음 매개면수로 넘겨주기 힘든 signal handler를 쉽게 넘겨줄 수 있다.
signal handler
- signal handler는 특정 signal이 들어왔을 때 수행할 함수를 말한다.
- SIG_DFL : 기본적인 행동을 한다.
- SIG_IGN : 무시한다.
- 기본 POSIX에서는, void를 return하며, int형 매개변수 하나를 가진다.
- POSIX Extension에서는 sa_sigaction이나 void *를 통해 다양한 signal handler를 등록할 수 있게 되었다.
Waiting for signals
- 프로그램이 특정 Signal을 받고 동작하게 하는 함수들이 존재한다.
Pause function
#include <unistd.h>
int pause(void);
- Pause함수는 이 함수를 실행시킨 함수를 유예시킨다.
- signal이 전달이 되면 함수를 다시 실행한다.
- 어떤 signal이든 전달이 되기만 하면 pause함수는 return하고 함수가 다시 시작된다.
- 언제나 -1을 return한다.
Sigsuspend function
– #include <signal.h>
int sigsuspend(const sigset_t* sigmask);
- Sigsuspend 함수는 파라미터로 sigset을 받는다.
- Sigsuspend 함수는 파라미터로 넘어온 sigset을 signal mask로 설정하는 작업을 함과 동시에 호출한 함수를 유예시킨다.
- 프로세스가 signal을 감지하면 함수가 다시 시작된다.
- 언제나 -1을 return한다.
Sigwait function
#include <signal.h>
int sigwait(const sigset_t *restrict sigmask, int *restrict signo);
- Sigwait 함수는 파라미터로 sigset과 int*를 받는다.
- Sigwait 함수는 파라미터로 넘어온 첫번째 파라미터 sigset을 signal mask로 설정한다.
- signal mask에 있는 특정한 signal이 들어오면, 들어온 signal 종류를 mask에서 지워준다.
- 지워진 signal의 정보를 두번째 파라미터로 반환한다.
- 성공적으로 완료하면 0을 return하며, 오류가 생기면 -1을 return한다.
- Sigsuspend와의 차이점은 다음과 같다.
- Sigsuspend는 우리가 원하는 Signal을 뺀 signal mask를 파라미터로 보냈다. Sigwait는 원하는 시그널을 빼지 않는다.
- Sigsuspend는 직접적으로 signal mask를 바꾸며 유예시킨다. Sigwait는 signal mask를 바꾸지 않는다.
Errors and Async-signal safety
signal을 다루는데 있어서 생기는 3가지의 대표적인 문제점이 존재한다.
- signal에 의해서 interrupt된 함수를 다시 시작할 것인가?
- interrupt될 수 있는 함수는 EINTR의 에러 넘버를 가지며 -1을 return한다.
- interrupt된 함수는 다시 호출을 하거나 특정한 처리를 해주어야 한다.
- 그렇기 때문에 함수가 interrupt될 수 있는지 먼저 확인을 해줘야 한다.
- signal handler가 nonreentrant function을 call하면 어떻게 하는가?
- 함수의 동작이 끝나지 않았을 때 다른 곳에서 호출해도 문제 없이 수행되는 함수를 reentrant function이라고 한다.
- 만약 문제가 생긴다면 nonreentrant function이다.
- signal handler에서 호출하는 함수는 reentrant function이여야 문제가 발생하지 않는다.
- 문제가 발생하지 않는 함수들을 Async-signal safe 하다고 한다. 이러한 함수만 call하는 것이 좋다.
- errno를 다룰 때 어떻게 해야 하는가?
예시를 한번 들어보자.
- main함수에서 A()라는 함수를 call한다.
- A() 함수가 에러가 걸린다. 에러 종류를 확인하기 위해 buffer에 있는 errno = ERROR가 된다. 이후 함수가 에러가 날 때를 처리하기 위해 handler() 함수로 들어간다.
- handler() 함수 내부에서도 에러가 발생한다. 에러 종류를 확인하기 위해 buffer에 있는 errno가 A의 에러를 나타내는 ERROR에서 handler() 함수 내부의 에러를 나타내는 ERR로 바뀐다.
- 결론적으로 A()에 대한 에러 종류를 확인을 못하게 된다.
- 위 예시에서 나온 문제를 해결하기 위해선, errno의 값을 임시로 저장해놓았다가 함수가 끝나기 전에 덮어씌워주는 해결법이 존재한다.
- 이외에도 여러가지 해결법이 존재하므로 신경을 써줘야 한다.
Program control
- 프로그램은 때때로 시그널을 이용해서 에러를 다룰 수 있다. 아래는 예시 상황이다.
- 긴 시간이 걸리는 일을 하던 중 정지 명령이 와서 결과를 잃어버리는 것을 피하기 위함.
- Ctrl-c는 처음부터 다시 수행하게 만들기 때문에, 보내진 시그널에 대응하는 방안을 마련해야 함
- 간접적으로 시그널을 처리한다.
- Ctrl-c에 flag를 설정하여 반응한다.
- 이 방법은 복잡하기 때문에 잘 쓰이지 않는다.
- 직접적으로 시그널을 처리한다.
- sigsetjmp / siglongjmp 함수를 이용하여 직접 내가 원하는 위치로 실행 시점을 옮길 수 있다.
- 이를 통해 시그널을 처리해 줄 수 있다.
sigsetjmp/siglongjmp
#include <setjmp.h>
void siglongjmp(sigjmp_buf env, int val);
int sigsetjmp(sigjmp_buf env, int savemask);
- Sigsetjmp()
- Siglongjmp()가 호출되어 다시 돌아오는 시점을 설정한다. 다시말해 jump에 대한 초기화를 실행한다.
- env : jump해서 다시 여기로 왔을 때 필요한 정보들을 저장해놓는 변수이다.
- savemask : 만약 zero값이 아니면, 현재의 signal mask 정보를 같이 env에 저장한다.
- 처음 접근 되었을 때 0을 바로 return하며, jump해서 돌아왔을 때는 longjmp에서 지정한 val을 return한다.
- Siglongjmp()
- Sigsetjmp() 함수가 실행된 위치로 되돌아간다.
- env : 어디로 jump할 지를 고른다.
- val : jump해서 돌아왔을 때 sigsetjmp()의 return값을 설정해준다. 이를 통해 여러 곳에서 jump해도 구분해줄 수 있다.
Programming with asynchronous I/O
- 어떤 application은 I/O를 실행하면서 프로그램을 계속 수행한다. 다시 말해 asynchronous하게 I/O를 실행할 수 있다.
- 원래는 I/O 작업이 끝나면 멈췄던 프로그램을 계속 수행한다. 여기서는 동시에 진행하므로 언제 끝나는지 알 수 없다.
- 그러므로 I/O 작업이 끝나는 시기를 아는 것이 중요하다.
- POSIX:AIO에서는 다음 함수들을 통해서 asynchronous I/O를 구현해놓았다.
- aio_read()
- aio_write()
- aio_return()
- aio_error()
Struct aiocb structure
- aio 함수들을 알기 위해선 구조체 aiocb를 알아야 한다. aiocb는 아래의 구성요소로 구성되어 있다.
- 파일의 read나 write는 3가지 요소가 필요하다. 아래는 기본적인 요소이다.
- int aio_fildes : 파일 디스크립터
- volatile void *aio_buf : 버퍼
- size_t aio_nbytes : 몇 바이트 수행하는지
- off_t aio_offset : 파일 오프셋을 의미한다. I/O의 시작 위치를 정한다.
- int aio_reqprio : 요청의 우선순위를 정한다.
- struct sigevent aio_sigevent
- signal을 통해서 I/O가 끝나는 것을 확인할 수 있도록 하는 구조체이다.
- aio_sigevent.sigev_notify를 SIGEV_NONE으로 설정하면, OS는 signal을 만들지 않는다.
- aio_sigevent.sigev_notify를 SIGEV_SIGNAL으로 설정하면, OS는 signal로 통지를 받는다. 몇 번 시그널을 받는지를 aio_sigevent.sigev_signo를 통해 설정한다.
- int aio_lio_opcode : 여러개의 비동기 I/O를 사용할 때 사용한다.
#include <aio.h>
int aio_read(struct aiocb* aiocbp);
int aio_write(struct_aiocb* aiocbp);
- aio_read()
- 읽기 작업 요청이 queue에 들어간다.
- aiocbp -> aio_nbytes의 수 만큼 읽어서 aiocbp -> aio_buf에서 읽는다.
- 성공하면 0, 실패하면 -1
- aio_write()
- 쓰기 작업 요청이 queue에 들어간다.
- 성공하면 0, 실패하면 -1
#include <aio.h>
ssize_t aio_return(struct aiocb* aiocbp);
int aio_error(const struct aiocb* aiocbp);
- aio_return()
- I/O가 수행된 후의 결과물을 받는다.
- Return values는 read/write한 byte가 반환된다.
completes
- aio_error()
- asynchronous I/O의 진행 상황을 관측한다.
- Return values
- 성공적으로 완료되면 0을 return한다.
- 실행하고 있으면 EINPROGRESS를 return한다.
- 실패하면 errno code를 return한다.
#include <aio.h>
int aio_suspend(const struct aiocb * const list[], int nent,
const struct timespec* timeout);
- aio_suspend는 asynchronous I/O가 다 수행될 때 까지 기다리는 함수이다.
- 매개변수는 다음과 같다.
- const struct aiocb* const list[] : aiocb 구조체 배열을 받음으로써 여러개의 함수를 수행할 수 있다.
- int nent : 구조체의 개수를 따로 적어주는 칸이다.
- const struct timespec* timeout : 함수 실행이 끝났는지 판정해주는 부분이다.
- 만약 timeout 값이 NULL이 아니면, aio_suspend는 특정한 시간이 될 때 까지 기다린다.
- 만약 timeout 값이 NULL이면, 적어도 하나가 완료가 될 때까지 무조건 기다린다.
- 성공하면 0, 실패하면 -1
#include <aio.h>
int aio_cancel(int fildes, struct aiocb* aiocbp);
- aio_cancel은 요청한 asynchronous I/O를 취소시키는 함수이다.
- 이 함수는 기존 read/write 함수를 취소시킬 수 없음과는 달리 asynchronous I/O 함수인 - 매개변수는 다음과 같다.
- int fildes : I/O를 취소할 파일의 파일 디스크립터를 받는다.
- struct aiocb* aiocbp : 실제로 취소할 구조체를 받는다.
- aiocbp가 NULL이면, 수행되고 있는 모든 function을 취소한다.
- Return values
- 성공하면 AIO_CANCELED
- 적어도 하나라도 취소되지 못하면 AIO_NOTCANCELED
- 이미 모든 것이 취소되었다면 AIO_ALLDONE
- 나머지는 -1