QUESTION
Ctrl-C는 어느 process에 SIGINT를 보내는가?
terminal driver는 key를 읽은 shell process 하나에 SIGINT를 보내지 않는다. controlling terminal의 foreground process group 전체에 signal을 보낸다. pipeline을 하나의 job으로 다루려면 shell이 모든 child를 같은 PGID로 묶어야 한다.
session은 process group들의 상위 단위이며 session leader가 controlling terminal을 얻을 수 있다. setsid는 새 session, 새 process group을 만들고 기존 controlling terminal 연결을 끊는다.
STRUCTURE
구조 그림
Foreground PGID=2400
- grep PID 2400
- sort PID 2401
- terminal VINTR → SIGINT
Background PGID=2500
- sleep PID 2500
- tty read → SIGTTIN
- job table entry
Shell PGID=2100
- job control owner
- tcsetpgrp
- waitpid(-PGID)
terminal의 foreground 대상은 PID 하나가 아니라 process group이다. 같은 pipeline의 process들이 하나의 PGID를 공유한다.
CALL PATH
호출 흐름
PID, PGID, SID, foreground PGID 네 값을 한 표에 기록하면 daemonization과 shell job-control 문제를 훨씬 빨리 구분할 수 있다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/sys.c | ksys_setsid(), setpgid() | session과 process group 생성 규칙 |
| drivers/tty/tty_jobctrl.c | tty_check_change(), __tty_check_change() | background group의 terminal 접근 검사 |
| drivers/tty/n_tty.c | isig(), n_tty_receive_signal_char() | terminal control character를 group signal로 변환 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids01#define _POSIX_C_SOURCE 200809L
02#include <errno.h>
03#include <stdio.h>
04#include <termios.h>
05#include <unistd.h>
06
07int main(void)
08{
09 pid_t pid = getpid();
10 pid_t pgid = getpgrp();
11 pid_t sid = getsid(0);
12 pid_t foreground = tcgetpgrp(STDIN_FILENO);
13
14 printf("pid=%ld pgid=%ld sid=%ld\n",
15 (long)pid, (long)pgid, (long)sid);
16 if (foreground >= 0)
17 printf("tty foreground pgid=%ld%s\n", (long)foreground,
18 foreground == pgid ? " (our group)" : "");
19 else if (errno == ENOTTY)
20 puts("stdin is not a controlling terminal");
21 else
22 perror("tcgetpgrp");
23 return 0;
24}
CODE NOTES
코드 조각별 설명
pid_t pgid = getpgrp호출 process가 속한 process group ID를 얻는다. 대개 pipeline의 첫 child PID를 PGID로 사용한다.
getsid(0)0은 호출 process 자신을 뜻한다. session ID는 session leader의 PID와 같다.
tcgetpgrpstdin이 연결된 terminal의 foreground PGID를 읽는다. 단순 redirect나 pipe이면 ENOTTY가 정상이다.
foreground == pgid현재 job이 foreground라면 terminal read와 generated signal을 정상적으로 받을 수 있다.
errno == ENOTTYdaemon이나 pipeline에서 controlling terminal이 없다는 상태를 오류 메시지로만 취급하지 않고 별도 실행 환경으로 분류한다.
DETAILS
세부 동작
pipeline 생성에는 parent-child 경쟁이 있다
shell은 첫 child PID를 PGID로 정하고 parent와 child 양쪽에서 setpgid를 호출해 어느 쪽이 먼저 실행돼도 group 구성이 끝나게 한다. child가 exec한 뒤에는 setpgid가 EACCES로 실패할 수 있다.
foreground job을 기다리는 동안 shell 자신은 SIGTTOU 같은 job-control signal 처리를 조정한다.
background terminal access는 제한된다
background process group이 controlling terminal에서 read하면 SIGTTIN으로 멈출 수 있다. TOSTOP flag가 있으면 write도 SIGTTOU 대상이다.
로그가 갑자기 멈춘 process를 deadlock으로 판단하기 전에 ps의 T state와 PGID/TPGID를 확인한다.
setsid 호출 조건
process group leader는 setsid를 호출할 수 없다. 전통적인 daemon 코드가 fork한 child에서 setsid를 호출하는 이유는 child PID가 기존 PGID와 다르도록 보장하기 위해서다.
현대 service manager 아래에서는 double-fork로 supervisor와의 관계를 끊기보다 foreground에서 실행하고 fd와 signal protocol을 명시하는 편이 낫다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
process group | setpgid로 구성되고 마지막 member가 떠나면 사라진다 | PGID, orphaned 여부 |
session | setsid로 생기고 포함 process group이 모두 사라질 때 끝난다 | SID, leader, tty |
controlling tty | session leader가 연결하고 hangup/revoke에서 관계가 끊긴다 | foreground pgrp, termios |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| background read가 멈춤 | SIGTTIN job-control stop | ps state, TPGID, signal disposition |
| setsid가 EPERM | 호출자가 process group leader | PID와 PGID 비교 |
| Ctrl-C가 일부 child에만 전달 | pipeline PGID 설정 누락 | ps -o pid,pgid,sid,tpgid |
LAB
직접 확인
- 프로그램을 터미널, pipe, background에서 각각 실행해 foreground PGID와 ENOTTY 결과를 비교한다.
- shell에서 sleep 100 | cat을 실행하고 ps -o pid,ppid,pgid,sid,tpgid,stat로 pipeline group을 확인한다.
- 간단한 parent가 두 child를 같은 PGID로 묶고 kill(-pgid, SIGTERM)으로 그룹 전체를 종료하게 만든다.
./job_idsstrace -e trace=getpid,getpgid,getsid,ioctl ./job_idsPRIMARY REFERENCES