← System Programming DUJINLABS.COM

Process · Linux userspace / kernel ABI

process group, session, controlling terminal

shell job control이 PID 하나가 아니라 process group에 signal을 보내고 terminal foreground group을 바꾸는 이유를 정리합니다.

Series
09 / 37
Build
cc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids
Run
./job_ids
Kernel
Linux 6.18.37 LTS

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 연결을 끊는다.

구조 그림

그림 1. session, process group, controlling terminal 계층
Session SID=2100session leader=shell · controlling tty=/dev/pts/3

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를 공유한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
shell pipeline fork
setpgid job PGID 구성
tcsetpgrp foreground 지정
terminal key VINTR/VSUSP
group signal job 전체에 전달

PID, PGID, SID, foreground PGID 네 값을 한 표에 기록하면 daemonization과 shell job-control 문제를 훨씬 빨리 구분할 수 있다.

그림 3. 커널 내부에서 지나가는 주요 지점
tty input line discipline
tty_signal foreground pgrp lookup
kill_pgrp group signal queue
get_signal 각 thread 선택
handler/default stop/terminate

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

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로 변환

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids
01#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}

코드 조각별 설명

실제 코드 10행pid_t pgid = getpgrp

호출 process가 속한 process group ID를 얻는다. 대개 pipeline의 첫 child PID를 PGID로 사용한다.

실제 코드 11행getsid(0)

0은 호출 process 자신을 뜻한다. session ID는 session leader의 PID와 같다.

실제 코드 12행tcgetpgrp

stdin이 연결된 terminal의 foreground PGID를 읽는다. 단순 redirect나 pipe이면 ENOTTY가 정상이다.

실제 코드 18행foreground == pgid

현재 job이 foreground라면 terminal read와 generated signal을 정상적으로 받을 수 있다.

실제 코드 19행errno == ENOTTY

daemon이나 pipeline에서 controlling terminal이 없다는 상태를 오류 메시지로만 취급하지 않고 별도 실행 환경으로 분류한다.

세부 동작

01

pipeline 생성에는 parent-child 경쟁이 있다

shell은 첫 child PID를 PGID로 정하고 parent와 child 양쪽에서 setpgid를 호출해 어느 쪽이 먼저 실행돼도 group 구성이 끝나게 한다. child가 exec한 뒤에는 setpgid가 EACCES로 실패할 수 있다.

foreground job을 기다리는 동안 shell 자신은 SIGTTOU 같은 job-control signal 처리를 조정한다.

02

background terminal access는 제한된다

background process group이 controlling terminal에서 read하면 SIGTTIN으로 멈출 수 있다. TOSTOP flag가 있으면 write도 SIGTTOU 대상이다.

로그가 갑자기 멈춘 process를 deadlock으로 판단하기 전에 ps의 T state와 PGID/TPGID를 확인한다.

03

setsid 호출 조건

process group leader는 setsid를 호출할 수 없다. 전통적인 daemon 코드가 fork한 child에서 setsid를 호출하는 이유는 child PID가 기존 PGID와 다르도록 보장하기 위해서다.

현대 service manager 아래에서는 double-fork로 supervisor와의 관계를 끊기보다 foreground에서 실행하고 fd와 signal protocol을 명시하는 편이 낫다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
process groupsetpgid로 구성되고 마지막 member가 떠나면 사라진다PGID, orphaned 여부
sessionsetsid로 생기고 포함 process group이 모두 사라질 때 끝난다SID, leader, tty
controlling ttysession leader가 연결하고 hangup/revoke에서 관계가 끊긴다foreground pgrp, termios

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
background read가 멈춤SIGTTIN job-control stopps state, TPGID, signal disposition
setsid가 EPERM호출자가 process group leaderPID와 PGID 비교
Ctrl-C가 일부 child에만 전달pipeline PGID 설정 누락ps -o pid,pgid,sid,tpgid

직접 확인

  1. 프로그램을 터미널, pipe, background에서 각각 실행해 foreground PGID와 ENOTTY 결과를 비교한다.
  2. shell에서 sleep 100 | cat을 실행하고 ps -o pid,ppid,pgid,sid,tpgid,stat로 pipeline group을 확인한다.
  3. 간단한 parent가 두 child를 같은 PGID로 묶고 kill(-pgid, SIGTERM)으로 그룹 전체를 종료하게 만든다.
실행./job_ids
추적strace -e trace=getpid,getpgid,getsid,ioctl ./job_ids

원문