그림으로 배우는 Linux 구조 - 2 / 프로세스


Process

새로운 프로세스를 생성하는 목적은 다음 두 종류 입니다.

  1. 동일한 프로그램 처리를 여러 프로세스에 나눠서 처리하기 (Ex. 웹 서버에서 다수의 요청 받기)
  2. 다른 프로그램을 생성하기 (Ex. bash에서 각종 프로그램을 새로 생성)

프로세스 생성을 실제로 실행하는 방법으로 리눅스는 fork() 함수와 execve() 함수를 사용합니다. 내부적으로는 각각 clone(), execve() 시스템 콜을 호출합니다. 목적이 1이라면 fork() 함수만 사용하고, 목적이 2라면 fork() 함수와 execve() 함수 둘 다 사용합니다.

같은 포르세스를 두 개로 분열시키는 fork() 함수

fork() 함수를 호출하면 해당 프로세스의 복사본을 만들고 양쪽 모두 fork() 함수에서 복귀합니다. 원본 프로세스를 Parent process, 생성된 프로세스를 Child Process 라고 부릅니다. 순서는 다음과 같습니다.

  1. 부모 프로세스가 fork() 함수 호출합니다.
  2. 자식 프로세스용 메모리 영역을 확보한 후 그곳에 부모 프로세스의 메모리를 복사합니다.
  3. 부모 프로세스와 자식 프로세스는 둘 다 fork() 함수에서 복귀합니다. 부모 프로세스와 자식 프로세스는 나중에 설명하듯 fork() 함수 반환값이 서로 달라서 처리 분기가 가능합니다.

linux2_1

부모 프로세스에서 자식 프로세스로 메모리를 복사하는 작업은 Copy-on-Write 기능 덕분에 무척 적은 비용으로 끝납니다. 따라서 리눅스에서 동일한 프로그램 작업을 여러 프로세스로 나눠서 처리할 때 생기는 오버헤드는 많지 않습니다.

다른 프로그램을 기동하는 execve() 함수

fork() 함수로 프로세스 복사본을 만들었으면 자식 프로세스에서 execve() 함수를 호출합니다. 그러면 자식 프로세스는 새로운 프로그램으로 바뀝니다.

  1. execve() 함수를 호출합니다.
  2. execve() 함수 인수로 지정한 실행 파일에서 프로그램을 읽어서, 메모리에 배치하는 데 필요한 정보를 가져옵니다.
  3. 현재 프로세스의 메모리를 새로운 프로세스 데이터로 덮어 씁니다.
  4. 프로세스를 새로운 프로세스의 최초에 실행할 명령(Entry point)부터 실행하기 시작합니다.

즉, fork() 함수는 프로세스 개수가 늘어나느 것이지만 전혀 다른 프로그램을 생성하는 경우라면 어떤 프로세스를 새롭게 치환하는 형태가 됩니다.

#!/urs/bin/python3
import os, sys

# fork() 함수 호출 시
# 자식 프로세스 생성을 실패하면 리턴 값이 -1 이 된다.
# 성공하면 부모 프로세스는 자식 프로세스의 pid를 받게 된다. 자식 프로세스는 0 값을 받게된다.  
# ret는 두 개의 반환값이 나오게 되며 (fork() 함수 호출 이 후 부터는 두 개의 프로세스가 독립적으로 아래 코드를 실행한다)
ret = os.fork()

if ret > 0:
    print("Parent Process: PID={}, Child Process: PID={}".format(os.getpid(), ret))
    exit()
elif ret == 0:
    print("Child Process: PID={}, Parent Process: PID={}".format(os.getpid(), os.getppid()))
    os.execve("/bin/echo", ["echo", "pid={}에서 안녕".format(os.getpid())], {})
    exit()

sys.exit(1)

Parent Process: PID=6870, Child Process: PID=6872
Child Process: PID=6872, Parent Process: PID=6870
pid=6872에서 안녕

fork() 함수를 호출한 후에 자식 프로세스는 execve() 함수에 의해서 인수로 지정한 “echo pid=에서 안녕" 명령어로 바뀝니다. execve() 함수가 동작하려면 실행 파일은 프로그램 코드와 데이터 이외에도 다음과 같은 데이터가 필요합니다.

  • 코드 영역의 파일 오프셋, 크기 및 메모리 맵 시작 주소
  • 데이터 영역의 파일 오프셋, 크기 및 메모리 맵 시작 주소
  • 최초로 실행할 명령의 메모리 주소(엔트리포인트)


프로세스의 부모 자식 관계

프로세스를 새로 생성하려면 부모 프로세스가 자식 프로세스를 생성해야 한다고 했습니다. 그러면 부모 프로세스의 부모 프로세스의..따라가다 보면 최종적으로 어디까지 가게 될까요?

컴퓨터 전원을 켜면 다음과 같은 순서로 시스템이 초기화됩니다.

  1. 컴퓨터 전원을 켭니다.
  2. BIOS나 UEFI같은 펌웨어를 기동하고 하드웨어를 초기화합니다.
  3. 펌웨어가 GRUB 같은 부트 로더를 기동합니다.
  4. 부트 로더가 OS 커널(리눅스 커널)을 기동합니다.
  5. 리눅스 커널이 init 프로세스를 기동합니다.
  6. init 프로세스가 자식 프로세스를 기동하고 그리고 그 자식 프로세스를…이렇게 이어져서 프로세스 트리 구조를 만듭니다.
$ pstree -p
systemd(1)─┬─agetty(2620)
           ├─agetty(2623)
           ├─chronyd(2151)
           ├─amazon-ssm-agen(2605)─┬─ssm-agent-worke(2662)─┬─ssm-session-wor(92108)─┬─sh(92125)───sudo(92171)───su(92172)───bash(92173)───pstree(92188)
           │                       │                       │                        ├─{ssm-session-wor}(92109)
           │                       │                       │                        ├─{ssm-session-wor}(92110)
           │                       │                       │                        ├─{ssm-session-wor}(92111)
           │                       │                       │                        ├─{ssm-session-wor}(92112)
           │                       │                       │                        ├─{ssm-session-wor}(92113)
           │                       │                       │                        ├─{ssm-session-wor}(92115)
           │                       │                       │                        └─{ssm-session-wor}(92114)
           │                       │                       ├─{ssm-agent-worke}(2663)
           │                       │                       ├─{ssm-agent-worke}(2664)
           │                       │                       ├─{ssm-agent-worke}(2665)
           │                       │                       ├─{ssm-agent-worke}(2666)
           │                       │                       ├─{ssm-agent-worke}(2667)
           │                       │                       ├─{ssm-agent-worke}(2675)
           │                       │                       ├─{ssm-agent-worke}(2683)
           │                       │                       ├─{ssm-agent-worke}(2684)
           │                       │                       ├─{ssm-agent-worke}(2687)
           │                       │                       └─{ssm-agent-worke}(2688)
           │                       ├─{amazon-ssm-agen}(2634)
           │                       ├─{amazon-ssm-agen}(2635)
           │                       ├─{amazon-ssm-agen}(2636)
           │                       ├─{amazon-ssm-agen}(2639)
           │                       ├─{amazon-ssm-agen}(2647)
           │                       ├─{amazon-ssm-agen}(2648)
           │                       └─{amazon-ssm-agen}(2649)
           ├─containerd-shim(2915)─┬─kube-proxy(3282)─┬─{kube-proxy}(3403)
           │                       │                  ├─{kube-proxy}(3404)
           │                       │                  ├─{kube-proxy}(3405)
           │                       │                  └─{kube-proxy}(3406)
           │                       ├─pause(3122)
           │                       ├─{containerd-shim}(2924)
           │                       ├─{containerd-shim}(2925)
           │                       ├─{containerd-shim}(2926)
           │                       ├─{containerd-shim}(2939)
           │                       ├─{containerd-shim}(2945)
           │                       ├─{containerd-shim}(2947)
           │                       ├─{containerd-shim}(2955)
           │                       ├─{containerd-shim}(2957)
           │                       ├─{containerd-shim}(2959)
           │                       ├─{containerd-shim}(2966)
           │                       └─{containerd-shim}(2967)
......

pstree 명령어를 사용하면 프로세스의 부모 자식 관계를 트리 구조로 표시합니다. 결과를 보면 모든 프로세스의 조상은 pid=1인 init 프로세스 (pstree 명령어 출력 결과에서 systemd로 표시된)라는 걸 알 수 있습니다. 그 외에도 bash(92173)에서 pstree(92188)을 실행했다는 것도 알 수 있습니다.

fork() 함수와 execve()함수 이외의 프로세스 생성 방법 > 어떤 프로세스에서 새로운 프로그램을 생성하기 위해 fork(), execve() 함수를 순서대로 호출하는 건 번거로운 작업입니다. 이럴 때 유닉스 계통 OS의 C 언어 인터페이스 규격인 POSIX에 정의된 posix_spawn()함수를 사용하면 간단히 처리할 수 있습니다.


프로세스 상태

시스템에서 동작하는 프로세스를 기동한 시각 및 사용한 CPU 시간 합계는 ps aux의 START 필드 및 TIME 필드에서 확인 가능합니다.

ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.1  0.0  42328  6200 ?        Ss   10:03   0:21 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
......
root        2745  2.7  0.7 1743948 115388 ?      Ssl  10:03   8:17 /usr/bin/kubelet --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime-endpoint unix:///run/containerd/containerd.sock --image-credential-provider-config /
root        2915  0.0  0.0 722536 12188 ?        Sl   10:03   0:08 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id ffb64db7958f90eeac9f3009e39b0da81eeef657cb359dc00dbe0f5876539175 -address /run/containerd/containerd.sock
65535       3102  0.0  0.0    972     4 ?        Ss   10:03   0:00 /pause
root        3282  0.0  0.3 766536 50072 ?        Ssl  10:03   0:03 kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config --hostname-override=ip-10-20-10-13.ap-northeast-2.compute.internal
472         5914  0.1  0.7 810528 112964 ?       Ssl  10:03   0:22 grafana server --homepath=/usr/share/grafana --config=/etc/grafana/grafana.ini --packaging=docker cfg:default.log.mode=console cfg:default.paths.data=/var/lib/grafana/ cfg:default.paths.logs=/var/log/grafana cfg:def
ssm-user   92125  0.0  0.0 122128  3432 pts/0    Ss   14:55   0:00 sh
root       92171  0.0  0.0 237728  7276 pts/0    S    14:55   0:00 sudo su -
root       92172  0.0  0.0 188388  4028 pts/0    S    14:55   0:00 su -
root       92173  0.0  0.0 122128  3448 pts/0    S    14:55   0:00 -bash
root       95833  0.0  0.0 160216  3896 pts/0    R+   15:08   0:00 ps aux

출력 결과를 보면 bash(92173)는 14:55분에 시작되었고 글을 쓰는 시간이 15:13분으로 약 20분정도의 시간 동안 CPU를 사용한 건 1초도 되지 않았따는 말이 됩니다. 다른 프로세스들도 마찬가지입니다. 각 프로세스는 실행된 후 어떤 이벤트가 발생할 때까지 CPU를 사용하지 않고 가만히 있는 Sleep 상태로 기다리고 있었습니다. bash(92173)는 사용자 입력이 있을 때까지 할 일이 없으므로 사용자 입력을 기다립니다. 프로세스 상태는 ps 출력 결과에서 STAT 필드를 보면 알 수 있습니다. STAT 필드의 첫 번째 글자가 S인 프로세스는 슬립 상태를 뜻합니다. 한편, CPU를 사용하고 싶어하는 프로세스는 Runnable(실행 가능)상태라고 부릅니다. 이때 STAT 첫 글자는 R입니다. 실제로 CPU를 사용하는 상태는 Running(실행) 상태라고 합니다. 프로세스를 종료하면 Zombie(좀비 상태, STAT 필드가 Z)가 되고 조금 있다가 소멸합니다.

linux2_4

시스템의 모든 프로세스가 슬립 상태라면 논리 CPU에서는 무슨 일이 일어날까요? 그럴 때 논리 CPU는 Idle process 라고 하는 ‘아무 일도 하지 않는’ 특수한 프로세스를 동작시킵니다. Idle process는 ps에서는 보이지 않습니다. 이런 Idle process를 만드는 가장 단순한 구현 방법으로는 새로운 프로세스가 생성되거나 슬립 상태인 프로세스가 깨어날 때까지 쓸데 없는 반복문을 실행하는 방법이 있습니다. 하지만 이런 방법은 전기 낭비에 불과하므로 보통은 사용하지 않습니다. 대신에 CPU 특수 명령을 사용해서 논리 CPU를 휴식 상태로 전환하고, 하나 이상의 프로세스가 실행 가능 상태가 될 때까지 소비 전력을 억제하면서 대기합니다.


프로세스 종료

프로세스를 종료하려면 exit_group() 시스템 콜을 호출합니다. fork.py와 fork-and-exec.py처럼 exit() 함수를 호출하면 내부에서는 이 시스템 콜을 부르는 함수가 호출됩니다. exit_group() 함수 내부에서 커널이 메모리 같은 프로세스가 사용한 자원을 회수합니다.

프로세스가 종료하면 부모 프로세스는 wait()나 waitpid() 같은 시스템 콜을 호출해서 다음과 같은 정보를 얻을 수 있습니다.

  • 프로세스 반환값. exit() 함수의 인수를 256으로 나눈 나머지. Ex. Exit Code 1
  • 시그널에 따라 종료했는지 여부
  • 종료할 때까지 얼마나 CPU 시간을 사용했는지 정보

bash에 내장된 wait 명령어를 사용하면 백그라운드로 실행 중인 프로세스에 wait() 시스템 콜을 호출해서 프로세스 종료 상태를 얻을 수 있습니다.


좀비 프로세스와 고아 프로세스

부모 프로세스가 자식 프로세스 상태를 wait() 계열 시스템 콜을 해서 얻을 수 있다는 말은 반대로 이야기하면, 자식 프로세스가 종료되어도 부모 프로세스가 이런 시스템 콜을 호출할 때까지 종료된 자식 프로세스는 시스템 내에 어떠한 형태로든 존재한다는 뜻 입니다. 이렇게 종료했지만 부모가 종료 상태를 확인하지 않은 상태의 프로세스를 가리켜 좀비 프로세스(Zombie Process)라고 부릅니다.

일반적으로 시스템에 좀비 프로세스가 가득해서 자원만 잡아먹지 않도록, 부모 ㅎ프로세스는 자식 프로세스 종료 상태를 제때 회수해서 남아 있는 자원을 커널로 돌려줘야 합니다. 만약 시스템을 기동했는데 좀비 프로세스가 대량으로 존재한다면 부모 프로세스에 버그가 있지 않은가 의심해보는게 좋습니다.

wait() 계열 시스템 콜을 실행하기 전에 부모 프로세스가 종료되면 해당하는 자식 프로세스는 고아 프로세스(Orphan process)가 됩니다. 커널은 init를 고아 프로세스의 새로운 부모로 지정합니다. init에 좀비 프로세스가 달려들면 난감한 문제가 있지만, 정기적으로 wait() 계열 시스템 콜 실행을 호출해서 시스템 자원을 회수합니다.


시그널

프로세스는 기본적으로 어떤 실행 순서에 따라 실행됩니다. 시그널(Signal)은 어떤 프로세스가 다른 프로세스에 어떤 신호를 보내서 외부에서 실행 순서를 강제적으로 바꾸는 방법입니다. 시그널에는 여러 종류가 있지만 대표적으로 SIGINT를 자주 사용합니다. SIGINT 시그널은 bash 같은 셸에서 Ctrl+C 를 눌렀을 때 발생합니다. SIGINT를 받은 프로세스는 곧바로 종료하는 것이 기본 값입니다. 프로그램이 어떤 식으로 만들어졌는지 관계없이 시그널을 호출한 순간 바로 프로세스가 종료하기 때문에 편리해서 리눅스 사용자는 SIGINT 시그널을 알게 모르게 사용하고 있습니다.

시그널을 보내는 방법으로 bash 이외에도 kill 명령어가 있습니다. 예를 들어 SIGINT를 보내고 싶다면 kill -INT 를 실행합니다.

  • SIGCHLD: 자식 프로세스 종료 시 부모 프로세스에 보내는 시그널. 보통은 시그널 핸들러 내부에서 wait() 계열 시스템 콜 실행을 호출합니다.
  • SIGSTOP: 프로세스 실행을 일시적으로 정지합니다. Bash에서 Ctrl+Z를 누르면 실행 중인 프로그램 동작을 정지시킬 수 있는데 이때 bash는 프로세스에 이 시그널을 보냅니다.
  • SIGCONT: SIGSTOP 등으로 정지한 프로세스 실행을 재개합니다.

SIGINT를 받은 프로세스는 보통 곧바로 종료한다고 설명했지만 꼭 그런 것은 아닙니다. 프로세스는 각 시그널에 시그널 핸들러(Signal Handler)를 미리 등록해 둡니다. 프로세스를 실행하다가 해당하는 시그널을 수신하면 실행 중인 처리를 일단 중단하고 시그널 핸들러에 등록한 처리를 동작시킨 다음에, 원래 장소로 돌아가서 이전에 하던 동작을 재개합니다. 시그널을 무시하도록 설정할 수도 있습니다.

linux2_6

시그널 핸들러를 사용하면 Ctrl+C를 눌러도 종료하지 않는 처치 곤란한 프로그램을 만들 수 있습니다.
intignore.py

#!/usr/bin/python3

import signal

# SIGINT 시그널을 무시하도록 설정
# 첫 번째 인수는 핸들러를 설정할 시그널(여기서는 sginal.SIGINT)
# 두 번째 인수에는 시그널 핸들러(여기서는 signal.SIG_IGN)를 지정
signal.signal(signal.SIGINT, signal.SIG_IGN)

while True:
    pass
python3 intignore.py 
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C

Ctrl+C를 눌렀을 때 출력되며 곤란하게 됩니다. 이렇게 안 죽는 프로그램을 종료하고 싶다면 Ctrl+Z로 intignore.py를 백그라운드 처리로 바꾸고 kill로 종료하는 방법 등을 사용합니다.

아래는 참고로 SIGTERM을 처리할 수 있도록 간단한 예시를 만들어 봄.
signal-handler.py

import time
import signal

def handler(signum, frame):
    print("signum: ", signum)
    print("frame: ", frame)
    print("Ctrl+C 신호를 수신했습니다.")


signal.signal(signal.SIGTERM, handler)

while True:
    print('대기중...')
    time.sleep(10)
$ kill -SIGTERM 23274

대기중...
signum:  15
frame:  <frame at 0x7f48bcb29c40, file '/home/ubuntu/environment/workspace/ch02/signal-example.py', line 15, code <module>>
SIGTERM 신호를 수신했습니다.
대기중...
signum:  15
frame:  <frame at 0x7f48bcb29c40, file '/home/ubuntu/environment/workspace/ch02/signal-example.py', line 15, code <module>>
SIGTERM 신호를 수신했습니다.

이렇게 하면 signal.SIG_IGN 시그널 핸들러를 사용하지 않고 개별적으로 정의한 handler를 사용하게 되며 SIGTERM 시그널을 수신했을 경우, handler에 의해 처리되며 print 처리만 하고 프로그램은 종료되지 않는다. 따라서 GracefulShutdown을 위해서는 SIGTERM에 대한 적절한 핸들러를 만들어 처리해주어야 한다.

SIGKILL은 시그널 중에서도 특별한 존재입니다. 이 시그널을 받은 프로세스는 반드시 종료됩니다. 시그널 핸들러를 이용한 동작 변경은 불가능합니다. 그런데 가끔 SIGKILL을 보내도 종료하지 않는 흉악한 프로세스가 있습니다. 이런 프로세스는 어떤 이유(네트워크나 디스크 자원 요청 대기 등)로 오랫동안 시그널을 받아 들이지 않는 uninterruptible sleep이라는 특별한 상태에 빠져 있습니다. 이런 상태에 빠진 프로세스는 ps aux 결과의 STAT 필드 첫 글자가 D로 출력됩니다.


쉘 작업 관리

세션

세션은 사용자가 gterm 같은 Terminal emulator 또는 ssh 등을 사용해서 시스템에 로그인했을 때의 로그인 세션에 대응하는 개념입니다. 모든 세션에는 해당 세션을 제어하는 Terminal(단말)이 존재합니다. 세션 내부 프로세스를 조작하고 싶으면 terminal을 이용해서 shell을 비롯한 프로세스에 지시하거나 프로세스 출력을 받습니다. 보통은 pty/ 이렇게 이름 붙은 가상 단말이 각각의 세션에 할당 됩니다.

세션에는 SessionID 또는 SID라고 부르는 값이 할당 됩니다. 그리고 Session leader라고 하는 프로세스가 존재하고 보통은 Bash 같은 Shell 입니다. 세션 리더 PID는 SID와 같습니다.

$ ps ajx
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
......
   1974   23484   23484   23484 pts/5      27617 Ss    1000   0:00 bash -l
......
  23484   27617   27617   23484 pts/5      27617 R+    1000   0:00 ps ajx
  ......

여기서 bash(23484)가 세션 리더인 세션(SID=23484)이 존재하고 이 세션에 ps ajx(PID=21617, SID=23484)가 소속된 걸 알 수 있습니다. 예제에서는 pts/5 이름으로 가상 단말이 할당되어 있습니다.

세션에서 할당된 단말이 행업(Hang up)하거나 해서 연결이 끈히겸ㄴ 세션 리더에는 SIGHUP 시그널이 갑니다. Terminal 창을 닫을 때도 동일한 상황이 됩니다. 이때 bash는 자신이 관리하던 작업을 종료시키고 자신도 종료합니다. 실행 시간이 오래 걸리는 프로세스를 실행 중에 bash가 종료되는 것을 원하지 않는 경우에는 다음과 같은 방법을 사용하면 편리합니다.

  • nohup 명령어: SIGHUP을 무시하도록 설정하고 프로세스를 기동합니다. 만약 세션이 종료되어서 SIGHUP 시그널이 오더라도 프로세스를 종료하지 않습니다.
  • bash의 disown 내장 명령어: 실행 중인 작업을 bash 관리 대상에서 제외합니다. 그러면 bash가 종료해도 해당하는 작업에는 SIGHUP을 보내지 않습니다.
프로세스 그룹

프로세스 그룹(Process group)은 여러 프로세스를 하나로 묶어서 한꺼번에 관리합니다. 세션 내부에는 여러 개의 프로세스 그룹이 존재합니다. 기본적으로 shell이 만든 작업이 프로세스 그룹에 해당한다고 생각하면 됩니다.

어떤 세션이 다음과 같이 되어 있다고 가정합니다.

  • 로그인 Shell은 Bash
  • bash에서 go build <코드명> & 실행
  • bash에서 ps aux less 실행

이 때 bash는 go build & 와 ps aux에 대응하는 2개의 프로세스 그룹을 작성합니다.

프로세스 그룹을 사용하면 해당하는 프로세스 그룹에 소속된 모든 프로세스에 시그널을 보낼 수 있습니다. Shell은 이 기능을 이용해서 작업을 제어합니다. 여러분도 kill 명령어로 프로세스ID를 지정하는 인수에 음수값을 지정하면 프로세스 그룹에 시그널을 보낼 수 있습니다. 예를 들어 PGID가 100인 프로세스 그룹에 시그널을 보내고 싶다면 kill -100 이렇게 실행하면 됩니다.

어떤 세션 내부에 있는 프로세스 그룹은 두 종류로 나뉩니다.

  • Foreground process group
    • Shell의 foreground 작업에 대응합니다. 세션당 하나만 존재하고 세션 단말에 직접 접근 가능합니다.
  • Background process group
  • Shell의 background 작업에 대응합니다. Background 프로세스가 단말을 조작하려고 하면 SIGSTOP을 받았을 때 처럼 실행이 일시 중단되고, fg 내장 명령어 등으로 프로세스가 foreground process group이 될 때까지 이 상태가 유지됩니다.

단말에 직접 접근하려면 foreground process group이 된 이후에 가능합니다.

linux2_8

$ ps ajx | less
PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
1592    3824    3824    3824 pts/3       4311 Ss    1000   0:00 bash -l
......
3824    4311    4311    3824 pts/3       4311 R+    1000   0:00 ps ajx
3824    4312    4311    3824 pts/3       4311 S+    1000   0:00 less

프로세스 그룹에는 고유의 ID인 PGID가 할당됩니다.

ps ajx 출력 결과에서 STAT 필드에 +가 붙은 프로세스가 포그라운드 포르세스 그룹에 속한 프로세스 입니다.


데몬

간단하게 말하면 데몬은 상주하는 프로세스 입니다. 보통은 프로세스라면 사용자가 실행하고 어떤 작업 처리가 끝나면 종료합니다. 하지만 데몬은 조금 다르게 시스템 시작부터 종료할 때까지 계속해서 존재하며 실행됩니다. 데몬은 다음과 같은 특징이 있습니다.

  • 단말의 입출력이 필요 없기 때문에 단말이 할당되지 않습니다.
  • 로그인 세션을 종료해도 영향을 받지 않도록 독자적인 세션을 가집니다.
  • 데몬을 생성한 프로세스가 데몬 종료 여부를 신경 쓸 필요 없이 init가 부모가 됩니다.

여기서는 ssh 서버로 동작중인 sshd를 살펴봅시다.

$ ps ajx
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   0:04 /sbin/init
......
      1     697     697     697 ?             -1 Ss       0   0:00 sshd: /usr/sbin/sshd -D -o AuthorizedKeysCommand /usr/share/ec2-instance-connect/eic_run
......
   3824    4911    4911    3824 pts/3       4911 R+    1000   0:00 ps ajx

부모 프로세스는 init(PPID가 1)이고 세션 ID는 PID와 동일합니다. 그리고 TTY 필드값이 단말과 연결되지 않은 걸 뜻하는 ? 입니다. 데몬에는 단말이 존재하지 않으므로 단말의 행업을 뜻하는 SIGHUP을 다른 용도로 사용합니다. 보통은 데몬이 설정 파일을 다시 읽는 시그널로 사용합니다.





Reference

  • 그림으로 배우는 리눅스 구조 (다케우치 사토루)
  • E-Book

Tag: [ book  linux  process  posix  ]