🗓️ 2023. 10. 12
⏱️ 11

Unix Signal

저쪽 신사분께서 보내신 메시지입니다

macOS 환경에서 작성된 글입니다.

Signal?

Signal실행중인 프로세스에게 프로그램 종료나 에러 핸들링같은 특정 동작을 실행시키게 하는 표준화된 메시지다.
저번에 살펴봤던 유닉스 도메인 소켓과 마찬가지로 프로세스간 통신(IPC)에 사용된다.
다른 프로세스나 같은 프로세스 상의 특정 스레드에게 이벤트를 알릴 때 비동기적으로 보내지며, 대표적으로 프로세스에 인터럽트를 걸거나 중지 혹은 (강제)종료할 때 이용된다.

시그널이 보내지면, 운영체제는 시그널을 전달하기 위해 해당 프로세스의 정상적인 실행흐름에 끼어든다.
만약 개발자가 시그널에 따른 핸들러를 등록해뒀으면 등록된 핸들러가 실행되고, 그렇지 않으면 기본 시그널 핸들러가 실행된다.
이때 개발자가 등록해둔 시그널 핸들러를 user defined signal handler, 기본 시그널 핸들러를 default signal handler라 부른다.

예를 들어, 작성한 node.js 코드에 SIGINT에 대한 핸들러가 등록돼있으면 Ctrl+C로 키보드 인터럽트를 걸어도 프로세스가 종료되지 않는다. 등록돼있지 않다면 기본 동작으로 프로세스가 종료된다.

주의사항

  • 예외로 SIGKILLSIGSTOP은 핸들러를 등록해둘 수 없다.
  • 여러 시그널이 동시에 발생했을 경우 race condition을 조심하자. 마찬가지로 동일한 시그널이 여러 번 왔을 때에도 주의가 필요하다.

대표적인 시그널

이제 어떤 시그널들이 있는지 알아보자.
우리가 은연중에 사용하고 있거나 개발할 때 도움될 수 있는 시그널 목록은 다음과 같다.

시그널코드설명
SIGHUP1터미널과 연결 종료
SIGINT2프로세스 인터럽트 요청 (Ctrl+C)
SIGKILL9프로세스 강제 종료
SIGUSR110유저 정의
SIGUSR212유저 정의
SIGCHLD17자식 프로세스가 종료, 중지, 재개
SIGCONT18SIGSTOP에 의해 중지된 프로세스를 재개
SIGSTOP19프로세스 중지
SIGTSTP20터미널에서 프로세스 중지 요청(Ctrl+Z)

맨페이지를 들어가보면 더 많은 시그널 종류와 각 시그널의 기본 동작(Ign, Term, Core 등)을 알 수 있다.
근데... 눈으로 직접 봐보지 않으면 도통 이게 뭔가 싶으니 콤퓨타로 확인해보자.

명령어

OS에서 지원하는 시그널은 kill -l로 확인할 수 있다.
접두사(prefix)인 SIG는 제외하고 표시된다.

macOS에서 지원하는 시그널macOS에서 지원하는 시그널

시그널을 보내려면 kill -{접두사 제외한 시그널 혹은 코드} {프로세스 ID}을 입력하면 된다.

# id가 1234인 프로세스에게 KILL 시그널 보내기
kill -KILL 1234

# 프로세스를 강제 종료해봤다면 익숙할 명령어 (-KILL과 같은 의미)
kill -9 1234

# 도커 환경에서 컨테이너에게 시그널 보내기
docker kill --signal=SIGHUP {컨테이너 이름}
docker kill --signal=HUP {컨테이너 이름}
docker kill --signal=1 {컨테이너 이름}

단축키

터미널상에서 단축키를 통해 보낼 수 있는 시그널들도 있다.
우리가 프로세스를 종료할 때 자주 사용해서 익숙한 친구들이다.

  • Ctrl+C : SIGINT
  • Ctrl+Z : SIGTSTP

Node.js에서

앞선 설명에서 확인할 수 있듯 시그널을 다루기 위해선 2가지 기능이 필요하다

  1. 다른 프로세스에게 시그널 보내기
  2. 다른 프로세스가 보낸 시그널 받기

node.js에선 process 객체를 통해 수행할 수 있다.
우선 시그널을 보내야 받아볼 수 있으니 시그널을 보내는 기능을 먼저 살펴보자.

시그널 보내기

process 객체의 kill() 메서드를 사용하면 되는데, 첫 번째 인자는 시그널을 보낼 프로세스의 id이고 두 번째 인자는 보낼 시그널의 종류다.
메서드 이름이 kill이지만 터미널 명령어도 kill이었고, C언어에서도 동일하게 kill() 함수로 시그널을 보내니 안심하자.

const otherProcessId = 1234
process.kill(otherProcessId, 'SIGINT');

만약 시그널을 보낼 프로세스가 존재하지 않는다면 ESRCH 에러가 발생한다.
(시그널을 보낼 때 발생할 수 있는 에러는 POSIX의 kill() 함수 문서에서 더 자세히 알 수 있다.)

try {
    const otherProcessId = 1234
    process.kill(otherProcessId, 'SIGINT');
} catch(err) {
    console.error(err);
    // node:internal/process/per_thread:233
    //   throw errnoException(err, 'kill');
    //   ^
    // Error: kill ESRCH
    //     at process.kill (node:internal/process/per_thread:233:13)
    //     at Object.<anonymous> (/Users/verycosy/Developer/Study/signal/send.js:2:21)
    //     at Module._compile (node:internal/modules/cjs/loader:1256:14)
    //     at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    //     at Module.load (node:internal/modules/cjs/loader:1119:32)
    //     at Module._load (node:internal/modules/cjs/loader:960:12)
    //     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    //     at node:internal/main/run_main_module:23:47 {
    // errno: -3,
    // code: 'ESRCH',
    // syscall: 'kill'
    // }
}

프로세스의 유무를 파악하기 위한 목적으로 시그널 인자로 정수 0을 사용할 수도 있다.

const otherProcessId = 1234
process.kill(otherProcessId, 0);

만약 프로세스가 없으면 마찬가지로 ESRCH 에러를 던진다.
프로세스가 존재하더라도 해당 프로세스에게 아무런 영향을 주지 않으니 걱정말자.

시그널 받기

시그널을 보낼 수 있게 됐으니 이제 시그널을 받아보자.
시그널을 이용할 땐 프로세스의 id와 보낼 시그널이 필요하다.
보낼 시그널은 우리가 정하면 되고, 실행중인 프로세스의 id는 node.js에서 process.pid로 확인할 수 있다.

console.log(`실행중인 프로세스 id : ${process.pid}`); 

테스트를 위해 시그널을 받을 script.js와 시그널을 보낼 signal.js를 준비하자.
signal.js를 준비하기 귀찮다면 Ctrl+C로 대체해도 된다.

// script.js
console.log(`프로세스 id : ${process.pid}`); // 프로세스 id : 1234

setInterval(() => {
    // 1초마다 빵긋빵긋 웃는다.
    console.log('^-^');
}, 1000);
// signal.js
process.kill(1234, 'SIGINT');

node script.js를 먼저 실행하고 곧이어 node signal.js를 실행하면 script.js가 종료된다.
알고있듯 Ctrl+C가 SIGINT를 발생시키기 때문에 node script.js를 실행한 채로 Ctrl+C를 눌러도 같은 결과가 나온다.

이제 시그널 핸들러를 등록해서 SIGINT가 발생하더라도 프로그램이 종료되지 않게 수정해보자.

// script.js에 다음 코드를 추가한다.
process.on('SIGINT', (signal) => {
  console.log(signal);
})

다시 script.js를 실행해보면 SIGINT 시그널을 보내도 프로그램이 종료되지 않는 모습을 볼 수 있다.
종료가 안 되니 kill 명령어나 Ctrl+Z로 종료시키자.

주의사항

  • Worker Thread에선 시그널을 이용할 수 없다.
  • 시그널은 OS 환경에 따라 동작이 다를 수 있다. Windows 환경이라면 코드를 작성하기 전에 공식 문서를 꼭 참고하자.
  • SIGUSR1은 node.js가 디버거를 실행시키기 위한 시그널로 예약되어 있다. 핸들러를 등록할 수는 있지만 디버거 실행에 문제가 생길 수 있으니 주의하자.

근데 이제 뭐함?

개념은 이제 알겠는데... 이걸 당최 언제 쓰면 좋을까?
프로세스끼리 통신할 때 쓴다는 걸 이론적으론 알고 있지만, 근본적으로 대체 프로세스는 왜 다른 프로세스와 통신해야만 하는 걸까?
이 물음에 답하려면 프로세스가 언제 다른 프로세스와 협력하는지를 알아야 한다.
무엇보다 시그널이 프로세스간 통신에 사용되는만큼 프로세스의 운용법를 모른다면 써먹기도 어려울 것이다.

그래서 다음 포스트에선 프로세스에 대해 상세히 알아볼 예정이다.
그 다음 포스트에선 두 내용을 합쳐 가장 실전적인 예시인 우아한 종료(Graceful shutdown)를 직접 구현해볼 거다.

다음 포스트에서 만납시다! (제발)

참고

돌아가기
© 2024 VERYCOSY.