Signals

난1렙이요·2024년 12월 16일
0

시스템 프로그래밍

목록 보기
18/22

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로 시작한다.
    • signal.h에 정의되어있다.
  • 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); /* 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 */
}
  • 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
    • Sigsuspend
    • Sigwait

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
profile
다크 모드의 노예

0개의 댓글