2장 : 프로세스 관리(기초편)

프로세스ps aux 명령어를 사용해서 현재 실행 중인 모든 프로세스를 확인하는 방법과, 리눅스에서 프로세스가 어떻게 관리되는지를 살펴보는 과정이다.

1. 개요

1️⃣ 리눅스에서 프로세스를 확인하는 방법

리눅스에서는 프로세스를 관리하고 모니터링하는 여러 명령어가 존재한다. 가장 기본적인 명령어가 바로 ps

🔹 ps aux 명령어란?

$ ps aux
  • a : 모든 사용자의 프로세스를 표시

  • u : 프로세스를 소유한 사용자 정보 포함

  • x : 터미널과 연결되지 않은 프로세스도 포함 (데몬 프로세스 등)

🔹 ps aux 실행 결과 예시

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
sat      19261  0.0  0.0  13840  5360 ?        S    18:24   0:00 sshd: sat@pts/8
sat      19262  0.0  0.0  12120  5232 pts/8    Ss   18:24   0:00 -bash
sat      19280  0.0  0.0  12752  3692 pts/0    R+   18:25   0:00 ps aux
  • 줄마다 하나의 프로세스를 표시

  • ssh 서버인 sshd(PID=19261)가 bash(PID=19262)를 실행하고 거기서 ps aux를 실행했음

  • ps 명령어는 --no-header 옵션으로 헤더 출력을 제거 가능

🔹 각 필드 설명

필드
설명

USER

프로세스를 실행한 사용자

PID

프로세스 ID

%CPU

CPU 사용량 (비율)

%MEM

메모리 사용량 (비율)

VSZ

가상 메모리 사용량 (KB 단위)

RSS

실제 물리적 메모리 사용량 (KB 단위)

TTY

연결된 터미널 (없으면 ?)

STAT

프로세스 상태 (S, R, Z 등)

START

프로세스가 시작된 시간

TIME

CPU 실행 시간

COMMAND

실행된 명령어

2️⃣ ps aux 실행 결과 분석

위의 예제에서는 프로세스 관계를 확인할 수 있다.

  1. ssh 서버 (sshd, PID 19261)이 사용자 sat의 로그인 요청을 받아들임.

  2. sshdbash (PID 19262) 셸을 실행함.

  3. 사용자는 터미널에서 ps aux (PID 19280)을 실행하여 프로세스 목록을 확인.

즉, 프로세스 간 부모-자식 관계가 있다는 걸 알 수 있다.

3️⃣ 프로세스 개수 확인

리눅스 시스템에서 실행 중인 프로세스 개수를 확인하는 방법:

$ ps aux --no-header | wc -l
  • --no-header : 헤더(열 이름)를 제외하고 출력

  • wc -l : 줄(line) 수를 세어 프로세스 개수 확인

결과:

216

즉, 현재 시스템에는 216개의 프로세스가 실행 중이라는 의미

✅ 프로세스 관리 명령어 모음

명령어

설명

사용 예시

ps aux

현재 실행 중인 모든 프로세스 목록 출력

ps aux → 전체 프로세스 확인

ps -ef

풀 포맷(full format)으로 프로세스 정보 표시

ps -ef → UID, PID 등 자세한 정보 표시

top

실시간 프로세스 모니터링 (CPU, 메모리 사용량 확인)

top

→ CPU 점유율 높은 프로세스 확인

htop

top의 개선 버전, 컬러 UI 제공 및 키보드/마우스 지원

htop → 인터랙티브한 프로세스 관리

pidof

특정 프로세스의 PID(프로세스 ID) 찾기

pidof nginxnginx의 PID 출력

pgrep

특정 문자열을 포함하는 프로세스명으로 PID 검색

pgrep -l sshdsshd 관련 프로세스 찾기

kill

특정 PID의 프로세스를 종료

kill 1234 → PID 1234 종료

kill -9

프로세스를 강제 종료 (SIGKILL 시그널)

kill -9 1234 → 강제 종료

pkill

프로세스명을 기반으로 종료

pkill nginxnginx 종료

pkill -f

전체 명령어 문자열을 기준으로 종료

pkill -f "python my_script.py"

nice

프로세스 실행 시 우선순위 설정 (기본값 0, -20이 가장 높고 19가 가장 낮음)

nice -n 10 myapp → 낮은 우선순위로 실행

renice

실행 중인 프로세스의 우선순위 변경

renice -5 1234 → PID 1234의 우선순위를 -5로 변경

watch

특정 명령을 주기적으로 실행하여 변화를 모니터링

watch -n 1 ps aux → 1초마다 ps aux 실행

strace

프로세스가 호출하는 시스템 호출(syscall) 실시간 추적

strace -p 1234 → PID 1234의 시스템 호출 추적

lsof

프로세스가 열고 있는 파일 목록 확인

lsof -p 1234 → PID 1234가 사용하는 파일 확인

netstat

네트워크 연결 상태 및 프로세스 확인

netstat -tulnp → 포트와 프로세스 매핑

ss

netstat의 대체 명령어, 더 빠르고 자세한 네트워크 정보 제공

ss -tulnp → 프로세스와 포트 정보 확인

uptime

시스템 가동 시간 및 평균 부하(load average) 확인

uptime → 시스템이 얼마나 오랫동안 실행되었는지 확인

vmstat

CPU, 메모리, 스왑 사용량 실시간 모니터링

vmstat 1 → 1초 간격으로 시스템 상태 확인

iotop

디스크 I/O 사용량을 실시간 모니터링

iotop → 가장 많이 디스크를 사용하는 프로세스 확인

free -m

메모리 사용량 확인 (-m 옵션으로 MB 단위 표시)

free -m → 사용 가능한 RAM 확인

df -h

디스크 사용량 확인 (-h 옵션으로 사람이 읽기 쉬운 형식)

df -h → 파일 시스템별 디스크 사용량 확인

결론

watch -n 1 ps aux
  • ps aux를 사용하면 현재 실행 중인 모든 프로세스를 확인할 수 있음.

  • ps aux --no-header | wc -l을 사용하면 현재 실행 중인 프로세스 개수를 알 수 있음.

  • 리눅스는 다양한 프로세스를 백그라운드와 포그라운드에서 관리하며, 이를 위해 프로세스 스케줄링과 상태 관리가 이루어짐.

2. 프로세스 생성

1️⃣ 프로세스 생성 목적

  1. 동일한 프로그램 처리를 여러 프로세스에 나눠서 처리하기(Ex. 웹서버에서 다수의 요청 받기)

  • fork() 함수만 사용

  1. 다른 프로그램을 생성하기(ex. bash에서 각종 프로그램을 생성)

  • fork()와 execve() 함수 둘 다 사용!

목적에 따라 프로세스 생성을 실제로 실행하는 방법으로는 리눅스는 fork() 함수와 execve() 함수를 사 용

2️⃣ 같은 프로세스를 두 개로 분열시키는 fork() 함수

1) fork() 함수란?

fork() 함수는 현재 실행 중인 프로세스를 복사해서 새로운 프로세스를 생성하는 함수이다.

즉, 같은 프로세스를 두 개로 분열시키는 역할을 한다.

fork 동작은 부모 프로세스가 자기 자신을 복제하는 시스템 호출로, 유닉스 계열 운영 체제에서 프로세스를 만드는 주된 방식

2) fork() 함수의 동작 과정

  1. 부모 프로세스가 fork() 호출

    • fork()가 실행되면 커널이 새로운 프로세스를 생성한다.

  2. 커널이 부모 프로세스의 메모리를 자식 프로세스에 복사

    • 하지만 실제로는 Copy-on-Write(CoW) 덕분에 메모리 전체를 즉시 복사하지 않는다.

    • 자식 프로세스가 특정 메모리를 변경할 때만 복사됨.

    • 부모 프로세스의 주소 공간을 Binary 통째로 복사

  3. 부모와 자식 프로세스가 각각 fork()에서 복귀

    • 부모 프로세스fork()의 반환값으로 자식 프로세스의 PID를 받음.

    • 자식 프로세스fork()의 반환값으로 0을 받음.

    • 이를 이용해 부모와 자식을 구별할 수 있음.

    • fork 함수 호출 이후 코드부터 각자의 메모리를 사용하여 실행

  4. 부모 프로세스(326)

    • PID (프로세스 ID): 326 → 부모 프로세스의 고유 ID

    • PPID (부모 프로세스 ID): 123 → 부모 프로세스도 상위 부모(예: init 프로세스)가 있음

    • CPID (자식 프로세스 ID): 368fork()를 실행한 후 생성된 자식 프로세스의 PID

  5. 자식 프로세스(368)

    • PID (프로세스 ID): 368 → 새롭게 생성된 자식 프로세스의 고유 ID

    • PPID (부모 프로세스 ID): 326 → 자식 프로세스의 부모가 PID 326(부모 프로세스)임을 나타냄

    • CPID (자식 프로세스 ID): -1fork() 직후에는 자식 프로세스가 새 자식 프로세스를 생성하지 않았기 때문에 -1로 표시

3) fork()를 사용한 프로세스 생성 예제 (Python)

아래 Python 코드를 실행하면 fork()가 실행된 후 부모와 자식 프로세스가 각각 분기된다.

📌 fork.py (프로세스 분기 코드)

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

ret = os.fork()  # 새로운 프로세스 생성

if ret == 0:
    # 자식 프로세스 실행
    print("자식 프로세스: pid={}, 부모 프로세스의 pid={}".format(os.getpid(), os.getppid()))
    sys.exit(0)  # 자식 프로세스 종료
elif ret > 0:
    # 부모 프로세스 실행
    print("부모 프로세스: pid={}, 자식 프로세스의 pid={}".format(os.getpid(), ret))
    sys.exit(0)  # 부모 프로세스 종료

📌 실행 결과

$ ./fork.py
부모 프로세스: pid=132767, 자식 프로세스의 pid=132768
자식 프로세스: pid=132768, 부모 프로세스의 pid=132767

이 결과를 보면 프로세스가 두 개로 나뉜 것을 확인할 수 있다.

  • 부모 프로세스(132767)가 fork()를 호출하여 자식 프로세스(132768)를 생성.

  • 이후 fork()의 반환값을 이용해 부모와 자식을 구별할 수 있다.

4) fork()의 반환값을 이용한 부모-자식 구분

fork()를 호출한 후 반환값을 확인하면 된다.

반환값

설명

0

자식 프로세스에서 반환됨

>0 (PID)

부모 프로세스에서 반환되며, 생성된 자식 프로세스의 PID를 가짐

<0

오류 발생 (프로세스 생성 실패)

이 특성을 이용해 부모와 자식 프로세스의 실행 흐름을 분기할 수 있다.

5) fork() 호출 후 부모와 자식의 실행 흐름

  • fork() 이후 부모와 자식은 완전히 독립된 프로세스로 실행된다.

  • 하지만 자식 프로세스는 부모의 메모리 복사본을 사용하기 때문에 초기 상태는 동일하다.

부모 프로세스 (PID=132767) 실행
 ├── fork() 호출 → 새로운 프로세스 생성

 ├── 부모 프로세스는 `fork()`의 반환값으로 `132768`을 받음
 │   └── 부모 프로세스: pid=132767, 자식 프로세스의 pid=132768

 ├── 자식 프로세스는 `fork()`의 반환값으로 `0`을 받음, 자식 프로세스는 fork()의 결과로 자신이 부모로부터 생성되었다는 것만 알 수 있다.
 │   └── 자식 프로세스: pid=132768, 부모 프로세스의 pid=132767

fork() 실행 전

부모 프로세스 (PID=132767) 실행 중

fork() 실행 후

부모 프로세스 (PID=132767)  ──>   자식 프로세스 (PID=132768)
  • 부모는 fork()의 반환값으로 132768 (자식의 PID)를 받음.

  • 자식은 fork()의 반환값으로 0을 받음.

  • 부모의 PID는 그대로 유지되며, 새로운 프로세스(자식)가 생긴 것뿐이다.

6) fork()의 메모리 복사 방식 (Copy-on-Write)

  • fork()부모 프로세스의 메모리를 그대로 복사하는 것처럼 보인다.

    • 하지만 실제로는 Copy-on-Write(CoW) 방식을 사용한다.

    • 즉, 자식이 데이터를 변경할 때만 새로운 메모리 공간을 할당한다.

    • 부모 프로세스와 자식 프로세스가 차지하고 있는 메모리 위치가 다르기 때문에 메모리 관련 정보도 변경된다.

    • 복제된 프로세스는 부모 프로세스의 동일한 메모리 영역복사하여 생성되나, 부모와 자식은 독립적으로 실행되며, 서로 영향을 주지 않는 별도의 프로세스로 동작한다.

    • 마치, github에 포크해오면 원본 repo에 영향을 주지 않지만 내부는 동일하게 가져오는 것과 같은 원리

7) exec()fork()의 차이

  • fork()기존 프로세스를 복사하여 새 프로세스를 생성한다.

  • exec()현재 프로세스를 새로운 프로그램으로 변경한다.

pid_t pid = fork();
if (pid == 0) {
    // 자식 프로세스
    execl("/bin/ls", "ls", "-l", NULL);  // 현재 프로세스를 "ls -l"로 변경
}

이렇게 하면 자식 프로세스가 ls -l 명령을 실행하게 된다.

8) 정리

  1. fork()현재 프로세스를 복사하여 새로운 프로세스를 생성한다.

  2. 부모 프로세스는 fork()의 반환값으로 자식의 PID를 받음.

  3. 자식 프로세스는 fork()의 반환값으로 0을 받음.

  4. Copy-on-Write(CoW) 방식으로 메모리 복사 비용을 최소화한다.

    1. 기존 프로그램을 하드디스크에서 로드하는 방식이 아니라 메모리에서 복사하는 방식이기 때문에 생성 속도가 빠르다.

    2. fork()를 이용하면 여러 개의 프로세스를 생성할 수 있으므로 다중 작업이 가능하다.

    3. 자식 프로세스가 종료되면, 자식이 사용했던 메모리 및 자원을 부모 프로세스가 정리할 수 있다.

      • 이를 통해 불필요한 리소스 낭비를 방지할 수 있다.

  5. exec()를 호출하면 새로운 프로그램으로 실행 프로세스를 교체할 수 있다.

9) 부모와 자식의 데이터는 별개이다!

왜 fork라는 함수 이름을 가졌을까?

fork()의 호출 및 생성 과정을 시각적으로 보면 프로세스가 두 갈래로 나뉘는 것처럼 보이는데, 말그대로 포크와 유사합니다.

유닉스 철학에서의 비유하자면 유닉스 시스템에서, fork()는 새로운 작업을 병렬적으로 처리하는 데 필수적인 도구입니다.

시스템의 흐름에서 실행 중인 프로세스를 복제하여 독립적인 두 작업이 서로 다른 경로를 갈라져 수행된다는 점이 중요하다는 것!

우리는 이 독립적이라는 키워드에 집중해보자. 부모 프로세스와 자식 프로세스의 지역변수는 느낌적인 느낌만으로도 각각 다른 값을 가진다는 것을 알 수 있다

전역변수는 뭔가 공유를 할것만 같은 느낌이 들지만, 전역변수조차도 두 프로세스에서 각각 다른값을 가진다.

10) fork()와 GitHub Fork

🔹 fork()와 GitHub Fork의 공통점

  1. 원본과 독립적인 복사본 생성

    • fork()는 부모 프로세스의 메모리를 복사하여 새로운 자식 프로세스를 만든다.

    • GitHub의 포크도 원본 레포지토리를 그대로 복사하여 독립적인 레포지토리를 만든다.

  2. 초기 상태는 동일하지만, 변경은 개별적으로 가능

    • fork()에서 부모와 자식은 처음에는 같은 메모리를 공유하지만, 이후에는 서로 다른 실행 흐름을 가질 수 있다.

    • GitHub에서도 처음에는 원본과 같지만, 이후에는 각 포크에서 개별적으로 코드 변경 가능하다.

  3. 원본에 영향을 주지 않음

    • fork()를 호출해도 부모 프로세스는 원래대로 실행되고, 자식 프로세스는 독립적으로 실행된다.

    • GitHub에서 레포를 포크해도 원본 레포지토리는 영향을 받지 않는다.

🔹 차이점

  • fork()메모리를 복사해서 프로세스를 생성하지만, GitHub의 Fork는 파일과 코드 히스토리를 복사하는 개념이다.

  • fork()의 자식 프로세스는 부모 프로세스 종료 후에도 독립적으로 실행될 수 있지만, GitHub의 포크된 레포지토리는 원본과 다시 합쳐질 수도 있다.

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

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

1) execve() 함수란?

execve() 함수는 현재 프로세스를 새로운 프로그램으로 변경하는 함수이다. 즉, 기존의 프로세스를 유지하는 것이 아니라, 완전히 다른 프로그램으로 교체하는 역할을 한다.

  • fork()부모 프로세스를 그대로 복사하여 자식 프로세스를 만드는 함수이지만,

  • execve()현재 실행 중인 프로세스를 완전히 새로운 프로그램으로 바꾸는 함수이다.

execve 함수는 바이너리 파일이나 스크립트 파일을 실행시키는 함수로, 다른 프로그램을 실행하고 자신은 종료한다.

2) execve() 함수의 동작 과정

  1. execve() 호출

    • 새로운 프로그램을 실행하기 위해 execve("/bin/echo", ["echo", "Hello"], {}) 같은 호출을 한다.

  2. 커널이 실행 파일을 읽음

    • execve()가 호출된 프로세스의 메모리를 새로운 프로그램의 데이터로 덮어씀.

    • 이 과정에서 실행 파일의 코드 영역과 데이터 영역을 메모리에 로드한다.

  3. 현재 프로세스를 새로운 프로그램으로 치환

    • 이전 프로세스의 코드, 데이터, 스택, 힙 영역이 모두 새로운 프로그램의 내용으로 대체됨.

  4. 새로운 프로그램이 엔트리 포인트(entry point)에서 실행 시작

    • main() 함수 같은 프로그램의 시작 지점(엔트리 포인트)부터 실행됨.

즉, execve()새로운 프로그램을 실행하지만, 기존의 프로세스 ID(PID)는 그대로 유지된다.

프로세스가 종료되는 것이 아니라 메모리 내용만 새로운 프로그램으로 변경되는 것이다.

execve()는 새로운 프로세스를 만드는 것이 아니다

  • fork()는 새로운 프로세스를 생성하는 함수다. 즉, 새로운 PID를 가진 자식 프로세스가 생성됨.

  • 하지만 execve()기존 프로세스를 그대로 유지하면서, 그 프로세스의 코드와 데이터를 새로운 프로그램으로 덮어씌운다.

  • 즉, execve()를 호출해도 PID는 변하지 않지만, 실행 중인 프로그램이 완전히 바뀌는 것이다.

프로세스 vs. 프로그램

프로세스(Process)란?

  • 운영체제가 실행 중인 하나의 작업 단위.

  • CPU에서 실행되며, 고유한 PID(Process ID)를 가짐.

  • 메모리(코드, 데이터, 스택, 힙 등)와 함께 관리됨.

프로그램(Program)이란?

  • 디스크에 저장된 실행 가능한 바이너리(예: /bin/ls, /usr/bin/python3)

  • 실행되지 않은 정적인 상태.

💡 즉, "프로그램"은 실행되기 전의 코드이고, "프로세스"는 실행 중인 프로그램을 의미한다.

3) fork() + execve()를 사용한 예제 (fork-and-exec.py)

다음 코드에서는 fork()를 사용하여 자식 프로세스를 생성한 후, 자식 프로세스에서 execve()를 호출하여 새로운 프로그램(/bin/echo)을 실행한다.

📌 fork-and-exec.py

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

ret = os.fork()  # 새로운 프로세스 생성

if ret == 0:
    # 자식 프로세스에서 실행
    print("자식 프로세스: pid={}, 부모 프로세스 pid={}".format(os.getpid(), os.getppid()))
    
    # execve()를 호출하여 자식 프로세스를 새로운 프로그램으로 교체
    os.execve("/bin/echo", ["echo", "pid={}에서 안녕".format(os.getpid())], {})
    
    # execve()가 실행되면 아래 코드는 실행되지 않음
    sys.exit(0)  
elif ret > 0:
    # 부모 프로세스에서 실행
    print("부모 프로세스: pid={}, 자식 프로세스 pid={}".format(os.getpid(), ret))
    sys.exit(0)  # 부모 프로세스 종료

4) 실행 결과

$ ./fork-and-exec.py
부모 프로세스: pid=5843, 자식 프로세스 pid=5844
자식 프로세스: pid=5844, 부모 프로세스 pid=5843
pid=5844에서 안녕

실행 흐름 분석

  1. 부모 프로세스(5843)가 fork()를 실행하여 자식 프로세스(5844)를 생성한다.

  2. 부모 프로세스는 fork()에서 자식 프로세스의 PID(5844)를 받음.

  3. 자식 프로세스는 fork()에서 0을 반환받아 실행됨.

  4. 자식 프로세스에서 execve("/bin/echo", ...)를 실행하여 현재 프로세스를 echo 명령으로 변경.

  5. 기존 자식 프로세스의 메모리는 삭제되고, echo 프로그램의 코드와 데이터로 덮어씌워짐.

  6. echo 명령이 실행되어 "pid=5844에서 안녕"을 출력.

즉, 자식 프로세스는 execve()를 실행하면서 기존의 프로그램이 완전히 사라지고, echo가 실행된 것이다.

5) fork()execve()의 차이점

기능

fork()

execve()

역할

부모 프로세스를 복사하여 새로운 프로세스 생성

현재 프로세스를 새로운 프로그램으로 교체

실행 후 프로세스 개수

기존 + 새로운 프로세스 (2개)

기존 프로세스가 새로운 프로그램으로 변경됨 (여전히 1개)

메모리

부모 프로세스의 메모리 복사

기존 메모리를 새로운 프로그램으로 덮어씀

사용 예시

멀티프로세싱 구현, 부모-자식 프로세스 생성

새로운 프로그램 실행 (bash, ls, python 등)

6) fork() + execve()를 함께 사용하는 이유

  1. 멀티프로세스를 만들고 싶다면 fork()를 사용

  • 예: 웹 서버에서 여러 클라이언트 요청을 처리하기 위해 새로운 프로세스를 생성

  • fork()를 사용하면 새로운 프로세스(PID가 다른 프로세스)가 생성됨.

  1. 기존 프로세스를 새로운 프로그램으로 바꾸고 싶다면 execve()를 사용

  • execve()를 사용하면 현재 프로세스의 실행 코드가 새로운 프로그램으로 바뀜.

  • 예: ls 명령 실행, bash 같은 셸 실행

  1. 보통 fork()를 먼저 호출한 후 execve()를 실행하는 패턴을 사용

  • 부모 프로세스는 계속 실행되면서, 자식 프로세스는 execve()를 통해 특정 프로그램을 실행할 수 있음.

  • 새로운 프로세스를 만든 후, 그 프로세스에서 다른 프로그램을 실행할 수 있음.

  • 예: 터미널에서 새로운 명령어를 실행할 때, 셸(Bash)이 fork()로 새 프로세스를 만들고, execve()로 새로운 프로그램을 실행함.

  • fork()execve()를 함께 사용하면 멀티프로세스를 만들면서 새로운 프로그램을 실행할 수 있음.

6) execve() 실행을 위해 필요한 정보

execve() 함수가 실행되려면 새로운 프로그램의 코드와 데이터뿐만 아니라 몇 가지 추가적인 정보가 필요하다. 이 정보들은 실행 파일에 저장되어 있으며, 운영체제(커널)는 이를 읽어와 메모리에 적절히 배치해야 한다.

필요한 정보

  1. 코드 영역

    • 실행 파일에서 코드(명령어)가 있는 위치 (파일 오프셋)

      • 오프셋 : 어떤 데이터가 파일이나 메모리에서 어느 위치에 저장되어 있는지를 나타내는 값

    • 코드 크기

    • 메모리에서 코드가 로드될 주소 (메모리 맵 시작 주소)

  2. 데이터 영역

    • 프로그램의 전역 변수와 초기화된 데이터가 저장되는 영역

    • 파일에서 데이터가 위치한 오프셋

    • 데이터 크기 및 메모리 맵 시작 주소

  3. 엔트리 포인트 (Entry Point)

    • 프로그램이 처음 실행될 때 시작할 위치

    • main() 함수가 호출되기 전의 실제 코드 시작 주소

7) 실행 파일의 구조와 ELF 포맷

리눅스에서 실행 파일은 보통 ELF(Executable and Linking Format) 포맷을 사용한다. ELF 파일에는 프로그램을 실행하는 데 필요한 모든 정보가 포함되어 있다.

이 정보를 확인하기 위해 readelf 명령어를 사용할 수 있다.

$ readelf -h pause

출력 예시

Entry point address: 0x400400
  • Entry Point Address (0x400400) → 이 주소에서 프로그램 실행이 시작됨.

8) ELF 파일의 섹션 정보 확인

ELF 실행 파일은 여러 섹션(section)으로 구성된다. 각 섹션에는 코드, 데이터, 심볼 테이블 등 다양한 정보가 포함된다.

$ readelf -S pause

출력 예시

섹션명
설명
메모리 주소
파일 오프셋
크기

.text

실행 코드 영역

0x400400

0x400

0x172

.data

데이터 영역 (전역 변수 등)

0x601020

0x1020

0x10

항목

설명

해당 값

코드 영역 (TEXT SECTION)

실행 파일에서 **코드(명령어)**가 위치하는 곳

.text

코드의 파일 오프셋

실행 파일 내에서 코드가 어디부터 시작하는지 나타내는 값

0x400

코드 크기

코드(명령어)의 총 크기

0x172 (370 바이트)

메모리 맵 시작 주소

실행될 때 메모리에 로드될 주소

0x400400

데이터 영역 (DATA SECTION)

전역 변수, 초기화된 데이터가 저장되는 공간

.data

데이터의 파일 오프셋

실행 파일 내에서 데이터가 어디부터 시작하는지 나타내는 값

0x1020

데이터 크기

데이터 영역의 총 크기

0x10 (16 바이트)

메모리 맵 시작 주소

실행될 때 메모리에 로드될 주소

0x601020

엔트리 포인트 (Entry Point)

프로그램이 처음 실행될 때 시작할 위치 (main() 호출 전)

0x400400

설명

  • .text실제 실행 코드(명령어)

  • .data전역 변수, 초기화된 데이터

  • .bss → 초기화되지 않은 전역 변수

이 정보를 기반으로 운영체제는 코드를 실행할 적절한 메모리 주소에 배치한다.

8) 프로그램 실행 시 메모리 맵 확인

프로그램이 실행되면 운영체제는 ELF 파일의 코드와 데이터를 메모리에 로드한다.

메모리 맵을 확인하면, 파일에서 읽어온 코드와 데이터가 메모리 어디에 위치하는지 알 수 있다.

메모리 맵 확인 방법

  1. pause 프로그램을 실행한 후 백그라운드로 실행

$ ./pause &
[3] 12492
  1. 프로세스의 메모리 맵 확인

$ cat /proc/12492/maps

출력 예시

rw-p : 읽기 쓰기가 가능한 데이터 영역, r--p : 데이터 영역, r--xp : 코드 영역
00400000-00401000 r-xp 00000000 08:02 788371 .../pause
00600000-00601000 r--p 00000000 08:02 788371 .../pause
00601000-00602000 rw-p 00001000 08:02 788371 .../pause
주소 범위
권한
파일 오프셋
설명

00400000-00401000

r-xp

00000000

코드 영역 (TEXT SECTION)

00600000-00601000

r--p

00000000

데이터 영역 (READ-ONLY SECTION)

00601000-00602000

rw-p

00001000

읽기/쓰기 가능한 데이터 영역 (DATA SECTION)

10) 핵심 정리

  1. execve()를 실행하면 운영체제는 실행 파일(ELF)에서 필요한 정보를 읽고, 프로세스의 메모리를 새로운 프로그램으로 교체한다.

  2. 운영체제는 ELF 파일을 로드하면서 다음 정보를 사용한다.

    • 코드 섹션 (.text) → 실행할 코드가 있는 메모리 주소

    • 데이터 섹션 (.data) → 전역 변수, 초기화된 데이터

    • 엔트리 포인트 (Entry Point) → 실행 시작 주소 (0x400400)

  3. 프로그램이 실행되면 /proc/<pid>/maps에서 메모리 맵을 확인할 수 있다.

  4. 실행 파일의 섹션 정보는 readelf -S 명령어로 확인 가능하다.

  5. 메모리 맵을 보면, 실행 파일의 코드와 데이터가 메모리에 어떻게 배치되는지 알 수 있다.


4️⃣ ASLR로 보안 강화

1) -no-pie 옵션이란?

-no-pie 옵션은 PIE(Position Independent Executable) 기능을 비활성화하는 GCC(컴파일러) 옵션이다. 기본적으로 최신 리눅스 배포판에서는 PIE가 활성화된 상태로 프로그램을 빌드한다.

  • PIE(위치 독립 실행 파일, Position Independent Executable)이란?

    • 실행 파일이 고정된 주소가 아닌, 실행될 때마다 다른 주소에 로드되는 실행 파일을 의미한다.

    • ASLR(Address Space Layout Randomization)이라는 보안 기능을 활용하여 실행 위치를 매번 다르게 설정한다.

-no-pie 옵션을 사용하면 PIE 기능을 끄고 실행 파일이 고정된 주소에서 실행되도록 만든다.

2) ASLR(Address Space Layout Randomization)이란?

ASLR의 주요 기능

  1. 프로그램 실행 시 메모리 배치를 랜덤하게 변경 → 해커가 코드 실행 위치를 예측하기 어렵게 함.

  2. 버퍼 오버플로우 공격 등을 어렵게 만듦 → 특정 메모리 주소를 이용한 공격을 방지.

ASLR 활성화 여부 확인

$ cat /proc/sys/kernel/randomize_va_space
2  # 2면 ASLR 활성화 (기본 설정)

ASLR 비활성화(테스트용)

$ sudo sysctl -w kernel.randomize_va_space=0

3) -no-pie 옵션이 필요한 이유

기본적으로 최신 리눅스에서는 모든 실행 파일이 PIE로 빌드된다. 그러나 -no-pie 옵션을 사용하면 실행 파일을 PIE가 아닌 정적 주소에 배치되는 형태로 빌드할 수 있다.

PIE vs. -no-pie 실행 파일 차이

$ file pause
pause: ELF 64-bit LSB shared object, ...
  • PIE 적용됨 → "shared object"라고 표시됨.

  • 실행할 때마다 메모리 매핑 주소가 바뀜.

$ file pause
pause: ELF 64-bit LSB executable, ...
  • PIE 적용되지 않음 (-no-pie 사용) → "executable"이라고 표시됨.

  • 항상 고정된 메모리 주소에서 실행됨.

4) ASLR이 적용된 실행 파일의 메모리 매핑 확인

$ cc -o pause pause.c
$ ./pause &
[5] 15406
$ cat /proc/15406/maps
559c5778f000-559c57790000 r-xp 00000000 08:02 788372
.../pause
...
$ ./pause &
[6] 15536
$ cat /proc/15536/maps
5568d2506000-5568d2507000 r-xp 00000000 08:02 788372
.../pause
  • 실행할 때마다 메모리 주소가 다름ASLR이 적용됨.

PIE를 비활성화하고 실행하면?

$ cc -o pause -no-pie pause.c
$ ./pause &
[7] 15678
$ cat /proc/15678/maps
00400000-00401000 r-xp 00000000 08:02 788372
.../pause
  • 실행할 때마다 고정된 주소(0x400000)에서 실행됨. → PIE가 적용되지 않음.

cat /proc/1687/maps
cat /proc/1691/maps

5) -no-pie 옵션을 사용하는 이유

  1. 메모리 주소가 바뀌지 않게 하려면 -no-pie를 사용해야 한다.

    • PIE가 적용된 실행 파일은 실행할 때마다 다른 주소에 로드되므로, 메모리 맵을 확인할 때마다 값이 달라질 수 있다.

    • 특정 주소에서 실행되는지 확인하는 실습을 하려면 PIE를 비활성화해야 한다.

  2. ELF 파일을 분석할 때 PIE를 비활성화하는 것이 더 직관적이다.

    • 실행 파일을 디버깅하거나 파일 오프셋과 메모리 주소를 비교할 때 PIE가 있으면 값이 변동된다.

    • 예제 코드의 실행 결과가 변하지 않도록 하기 위해 PIE를 끄고 테스트한다

6) 다시 ASLR을 사용하려면

 gcc -fPIE -pie -o pause pause.c

3. 프로세스의 부모 자식 관계때마다 메모리 주소가 다름

1️⃣ 프로세스의 조상은 무엇인가?

우리는 fork()를 통해 부모 프로세스가 자식 프로세스를 생성한다는 것을 알고 있다. 그렇다면 부모 프로세스의 부모는 누구인지, 궁극적으로 가장 최상위 프로세스는 무엇인지 확인해보자.

컴퓨터가 부팅될 때 다음과 같은 순서로 시스템이 초기화된다.

1. 컴퓨터 전원을 켠다

  • 전원이 공급되면 CPU는 기본적으로 0xFFFFFFF0 주소에서 실행을 시작한다.

  • 여기에는 BIOS (Basic Input/Output System) 또는 UEFI (Unified Extensible Firmware Interface)가 위치한다.

    • BIOS는 컴퓨터가 켜질 때 가장 먼저 실행되는 펌웨어로, 하드웨어를 초기화하고 운영체제를 부팅하는 역할을 한다.

    • 메인보드(마더보드)에 내장된 펌웨어로, 컴퓨터 전원을 켜자마자 가장 먼저 실행된다.

    • 운영체제는 아니고 하드웨어 수준의 소프트웨어

    구분
    BIOS
    UEFI

    등장 시기

    오래됨 (1980년대)

    비교적 최신 (2010년대)

    부팅 속도

    느림

    빠름

    인터페이스

    텍스트 기반

    마우스 지원 그래픽 기반

    부팅 가능 디스크 크기

    2TB 이하

    2TB 이상 가능

    보안 기능

    거의 없음

    Secure Boot 등 보안 강화

    현재 위치

    점점 사라지는 중

    대부분의 최신 시스템에서 사용 중

    • 요즘 컴퓨터는 거의 다 BIOS 대신 UEFI를 사용하고 있다. 하지만 아직도 BIOS라는 용어는 널리 쓰이고 있음음

2. BIOS 또는 UEFI가 실행되어 하드웨어를 초기화

  • BIOS/UEFI는 CPU, RAM, 키보드, 마우스, 디스크 등의 하드웨어를 초기화한다.

  • 이후 부팅 가능한 장치를 찾고(예: SSD, HDD, USB) 운영체제를 로드할 준비를 한다.

3. 부트로더(예: GRUB) 실행

  • BIOS/UEFI가 부팅 장치에서 부트로더(Bootloader)를 로드한다.

  • 부트로더는 운영체제를 메모리에 올리는 역할을 한다.

  • 대표적인 부트로더:

    • GRUB (GNU GRUB): 리눅스에서 가장 많이 사용됨.

    • LILO (Linux Loader): 예전 리눅스에서 사용됨.

    • Windows Boot Manager: 윈도우에서 사용됨.

  • 부트로더는 BIOS/UEFI가 운영체제를 메모리에 올리기 위해 실행하는 작은 프로그램

    • 운영체제(OS)를 실행시키기 위한 다리 역할을 한다고 보면 됨

    • BIOS나 UEFI는 단순한 펌웨어라서 운영체제를 직접 실행하진 못한다.

    • 그래서 BIOS/UEFI는 부팅 가능한 장치에서 부트로더를 찾아 실행하고, 부트로더는 그다음 운영체제 커널을 로딩해서 실행시켜주는 거임

4. 부트로더가 OS 커널을 실행

  • 부트로더는 운영체제(OS) 커널을 로드하여 실행한다.

  • 여기서는 리눅스 커널이 실행된다고 가정한다.

  • 커널은 메모리, 프로세스, 파일 시스템, 장치 드라이버 등을 초기화한다.

5. 리눅스 커널이 init 프로세스를 실행

  • 커널이 초기화되면, 가장 먼저 실행하는 프로세스가 init 프로세스(PID=1) 이다.

  • init모든 사용자 프로세스의 부모가 되는 최상위 프로세스이다.

6. init이 여러 자식 프로세스를 생성하여 시스템을 구성

  • init백그라운드 서비스(데몬)를 실행하고, 사용자가 로그인할 수 있도록 셸을 실행한다.

  • 네트워크, 디스크 관리, 디스플레이 서버(Xorg), 로그 관리 등의 시스템 서비스를 시작한다.

  • 이 과정에서 fork()execve()를 사용하여 새로운 프로세스를 생성한다.

즉, 현재 실행 중인 모든 프로세스의 최상위 부모는 init 프로세스(PID=1)이다.

2️⃣ 프로세스 계층 구조 확인

리눅스에서 pstree 명령어를 사용하면 프로세스의 부모-자식 관계를 트리 형태로 확인할 수 있다.

$ pstree -p

출력 예시

이 출력이 의미하는 것

  • systemd(1) → 현재 리눅스 배포판에서 init 프로세스를 대체하는 systemd가 실행됨.

    • 모든 프로세스는 PID=1을 가진 systemd(또는 init)의 자식 프로세스이다.

    • 예를 들어, bash(19262)가 실행된 후 사용자가 pstree(19638) 명령을 실행했기 때문에, pstreebash의 자식 프로세스가 된 것을 볼 수 있다.

3️⃣ 프로세스를 생성하는 또 다른 방법: posix_spawn()

일반적으로 새로운 프로세스를 실행하려면 fork()로 자식 프로세스를 만들고 execve()로 새로운 프로그램을 실행해야 한다.

그러나 POSIX 표준에서는 이를 더 간단하게 처리할 수 있도록 posix_spawn() 함수를 제공한다.

📌 posix_spawn()을 사용한 프로세스 생성 예제

#!/usr/bin/python3
import os

# posix_spawn()을 사용하여 echo 명령어 실행
os.posix_spawn("/bin/echo", ["echo", "posix_spawn()로 생성되었습니다"], {})

print("echo 명령어를 생성했습니다")

실행 결과

  • posix_spawn()을 사용하면 fork()execve()를 따로 호출할 필요 없이 한 번에 새로운 프로세스를 생성하고 실행할 수 있다.

  • fork()execve()를 따로 호출하는 것보다 코드가 간결하고 직관적이다.

4️⃣ fork()execve()를 사용한 전통적인 프로세스 생성

위의 posix_spawn()과 동일한 기능을 fork()execve()로 구현하면 다음과 같다.

#!/usr/bin/python3
import os

ret = os.fork()

if ret == 0:
    # 자식 프로세스에서 execve() 실행
    os.execve("/bin/echo", ["echo", "fork()와 execve()로 생성되었습니다"], {})
elif ret > 0:
    print("echo 명령어를 생성했습니다")

실행 결과

  • fork()가 실행되면 부모 프로세스와 자식 프로세스가 동시에 실행된다.

  • 자식 프로세스는 execve()를 실행하여 자신을 echo 프로그램으로 치환한다.

5️⃣ posix_spawn() vs fork() + execve() 비교

방법
특징
장점
단점

fork() + execve()

전통적인 프로세스 생성 방식

유연성이 높음, 세부적인 설정 가능

코드가 길어짐, fork()execve()를 꼭 호출해야 함

posix_spawn()

POSIX 표준의 간단한 프로세스 생성 방식

코드가 간결하고 직관적임

세부 설정이 어렵고 fork()보다 제어가 제한됨

  • posix_spawn()간단한 프로세스 실행에는 유용하지만, 복잡한 프로세스 관리는 fork()를 사용하는 것이 더 적합하다.

  • 예를 들어 셸(Bash)처럼 프로세스를 관리하는 프로그램을 만들 때는 fork()를 직접 호출하는 것이 좋다.

프로세스 전이 상태 그림

6️⃣ 정리

  1. 모든 프로세스의 조상은 init(또는 systemd) 프로세스(PID=1)이다.

  2. pstree 명령어를 사용하면 부모-자식 프로세스 관계를 확인할 수 있다.

  3. 새로운 프로세스를 실행하는 방법

    • fork() + execve()를 사용하여 전통적인 방식으로 실행

    • posix_spawn()을 사용하여 간단하게 실행

  4. posix_spawn()fork()execve()를 따로 호출하는 것보다 간단하지만, 세부적인 제어가 어렵다.

  5. 셸과 같은 프로그램을 만들 때는 fork()를 직접 호출하는 것이 더 적합하다.

4. 프로세스 상태

1️⃣ 프로세스 상태(Process States)란?

운영체제에서 실행되는 모든 프로세스는 항상 특정한 상태에 속해 있으며, 필요에 따라 상태가 변경된다.

리눅스에서 실행 중인 프로세스의 상태는 ps aux 명령어의 STAT 필드를 보면 확인할 수 있다. TIME 필드에서 확인 가능하다.

$ ps aux
USER       PID %CPU MEM   VSZ  RSS TTY      STAT  START   TIME COMMAND
sat       19262  0.0  0.0  5336 3632 pts/0   Ss    18:24   0:00 -bash

위 출력에서 STAT 필드의 값이 Ss인데, 여기서 첫 번째 문자인 S(Sleep, 슬립 상태)가 프로세스의 현재 상태를 의미한다.

즉, 이 bash 프로세스는 사용자가 입력할 때까지 슬립 상태로 대기 중이라는 뜻이다.

2️⃣ 주요 프로세스 상태

운영체제에서 프로세스는 다음과 같은 주요 상태를 가질 수 있다.

  1. 실행 가능 상태(Ready)

  • CPU에서 실행될 수 있지만, 현재 실행 중은 아닌 상태.

  • STAT 값: R (Running or Runnable).

  • 실행 가능한 프로세스가 여러 개 있으면 CPU 스케줄러가 하나를 선택하여 실행.

  1. 실행 상태(Running)

  • CPU에서 실행 중인 상태.

  • STAT 값: R.

  • 한 번에 하나의 논리 CPU(Core)에서만 실행될 수 있다.

  • 일정 시간(타임 슬라이스)이 지나면 다른 프로세스에게 CPU를 넘겨주고 다시 실행 가능(Ready) 상태로 이동.

  1. 슬립 상태(Sleeping)

  • I/O 작업이나 이벤트가 발생할 때까지 기다리는 상태.

  • STAT 값: S (Interruptible Sleep) 또는 D (Uninterruptible Sleep).

  • 예시:

    • 터미널에서 bash가 사용자 입력을 기다리는 경우.

    • 네트워크 요청을 보내고 응답을 기다리는 프로세스

  1. 종료 상태(Terminated)

  • 프로세스가 실행을 완료하고 종료된 상태.

  • STAT 값: 없음 (일반적으로 ps에서 더 이상 표시되지 않음).

  1. 좀비 상태(Zombie)

  • 프로세스가 종료되었지만, 부모 프로세스가 종료 상태를 아직 수거하지 않은 상태.

  • STAT 값: Z (Zombie).

  • 부모 프로세스가 wait() 또는 waitpid()를 호출해야 좀비 프로세스가 사라짐.

  • 좀비 프로세스가 많아지면 시스템 리소스 낭비가 발생할 수 있음.

3️⃣ 프로세스 상태 변화 과정

프로세스는 실행 중 다양한 이벤트에 따라 상태를 변경한다.

    프로세스 생성

  [실행 가능 상태]  ──────→  [실행 상태]  ──────→  [슬립 상태]
        ↑                 ↓                     ↑
      (스케줄링)      (타임 슬라이스 종료)      (이벤트 발생)
          │                                      ↓
          └────────────── [좀비 상태] ──────→  [완전 종료]

📌 상태 변화 과정 설명

  1. 프로세스가 생성되면 실행 가능 상태(Ready)로 들어간다.

  2. CPU 스케줄러가 선택하면 실행 상태(Running)로 변환된다.

  3. CPU 타임 슬라이스(Time Slice)가 끝나면 다시 실행 가능 상태로 돌아간다.

  4. 입출력 요청이나 이벤트 대기를 하면 슬립 상태(Sleeping)로 전환된다.

  5. 이벤트(입출력 완료 등)가 발생하면 다시 실행 가능 상태(Ready)로 돌아간다.

  6. 프로세스가 종료되면 좀비 상태(Zombie)가 되었다가 최종적으로 시스템에서 사라진다.

STAT 값

설명

R

실행 상태(Running) 또는 실행 가능 상태(Ready)

S

슬립 상태(Sleeping) (I/O 작업 대기 중)

D

Uninterruptible Sleep (I/O 대기 중, 신호 무시)

T

중지 상태(Stopped) (예: SIGSTOP 신호를 받은 경우)

Z

좀비 프로세스(Zombie)

예제 해석

  • -bash 프로세스의 STAT 값이 Ss → 슬립 상태이며, 세션 리더(Session Leader)임을 의미.

  • init 프로세스는 Ss 상태 → 실행 중이지만 현재 대기 중.

4️⃣ CPU가 아무것도 하지 않을 때는 어떻게 되나?

만약 시스템의 모든 프로세스가 슬립 상태라면 CPU는 어떻게 동작할까? 이때 CPU는 "아이들 프로세스(idle process)"를 실행한다.

  • 아이들 프로세스(idle process)

    • CPU가 실행할 프로세스가 없을 때 동작하는 특수한 프로세스.

    • CPU가 불필요한 작업을 수행하지 않도록 하고, 전력을 절약하는 역할을 함.

    • ps 명령어에서는 보이지 않음.

📌 아이들 상태의 의미

  1. CPU는 실행할 프로세스가 없을 때 "반복문"을 실행하지 않고 전력을 절약해야 함.

  2. CPU는 저전력 모드(Idle Mode)로 들어가며, 새로운 프로세스가 실행될 때까지 대기함.

  3. 노트북이나 스마트폰에서 배터리를 절약할 수 있는 이유 중 하나.

5️⃣ 타임 슬라이스

좋아! 이번엔 멀티태스킹의 핵심 개념 중 하나

타임 슬라이스(Time Slice)란?

Time Slice란 운영체제가 하나의 프로세스에게 CPU를 잠시 동안만 사용할 수 있도록 할당하는 시간 단위야.

  • CPU는 한 번에 하나의 프로세스만 실행할 수 있어.

  • 그런데 현대 OS는 여러 프로그램(프로세스)이 동시에 실행되는 것처럼 보여야 하잖아?

  • 이걸 위해 운영체제가 CPU 시간을 쪼개서 돌아가며 각 프로세스에게 나눠주는 방식을 사용함.

  • 이때 쪼개진 CPU 사용 시간의 단위를 "타임 슬라이스" 라고 해.

어떻게 작동하나?

  1. 어떤 프로세스가 CPU를 할당받고 실행 시작

  2. 일정 시간(타임 슬라이스)이 지나면

  3. 운영체제가 강제로 CPU를 회수하고 다른 프로세스에게 넘김

  4. 이 과정을 반복하면서 여러 프로세스가 돌아가며 실행

이런 방식은 선점형 스케줄링(Preemptive Scheduling)이라고도 해.

예시

  • 타임 슬라이스가 10ms(밀리초)라고 가정하자.

  • 프로세스 A, B, C가 실행 대기 중이면 CPU는 이렇게 순환해:

[0~10ms] → A 실행  
[10~20ms] → B 실행  
[20~30ms] → C 실행  
[30~40ms] → A 실행 (다시 돌아옴)  
...

사용자는 이 모든 게 엄청 빠르게 반복되기 때문에 마치 여러 프로그램이 동시에 실행되는 것처럼 보이는 거야!

타임 슬라이스의 크기는 어떻게 정하나?

운영체제가 내부적으로 정함. 보통 리눅스에서는 10~100ms 정도, 시스템 설정과 커널 버전에 따라 다름.

타임 슬라이스가 짧으면
타임 슬라이스가 길면

더 자주 프로세스 전환 → 반응성 ↑

컨텍스트 스위치 적음 → 성능 ↑

컨텍스트 스위치 비용 ↑

특정 프로세스가 CPU 오래 점유 가능

6️⃣ 비슷한 개념

컨텍스트 스위치(Context Switch)란?

CPU가 실행 중인 프로세스를 멈추고, 다른 프로세스로 전환할 때 필요한 저장/복원 작업을 말해.

  • 하나의 CPU가 여러 프로세스를 실행하려면 지금까지 하던 일의 상태를 저장하고 다른 프로세스의 상태를 복원해서 이어서 실행해야 하잖아?

  • 이 전환 작업 전체를 컨텍스트 스위치라고 해.

컨텍스트(Context)에 포함된 정보

  • CPU 레지스터 값

  • 프로그램 카운터(다음 실행할 명령어 주소)

  • 스택 포인터

  • 메모리 상태 등

📌 비유하자면:

게임을 하다가 저장하고 → 다른 사람으로 로그인해서 이어하는 것 다시 돌아오면 저장한 지점부터 다시 시작하는 것과 같아!

언제 일어나는가?

  • 타임 슬라이스가 끝났을 때 (→ 선점)

  • 입출력 요청으로 CPU를 못 쓰게 될 때 (→ 자발적)

  • 더 높은 우선순위의 프로세스가 나타날 때 (→ 선점)

단점

  • 저장/복원 비용이 존재해서 너무 자주 발생하면 시스템 성능 저하될 수 있음.

스케줄러(Scheduler)란?

CPU를 어떤 프로세스에게 언제 할당할지 결정하는 운영체제의 핵심 기능.

  • 타임 슬라이스가 끝나거나, 어떤 프로세스가 슬립 상태가 되면 → 스케줄러가 다음에 실행할 프로세스를 선택함.

  • 마치 CPU를 사용할 "순번 정해주는 관리자" 같은 존재야.

스케줄러의 주요 목표

  1. 공정성: 모든 프로세스가 CPU를 어느 정도 균등하게 사용할 수 있도록

  2. 효율성: CPU가 놀지 않고 최대한 바쁘게 동작하도록

  3. 반응성: 사용자가 빠르게 응답을 받을 수 있도록

스케줄링 알고리즘 예시

알고리즘
설명

Round Robin

고정된 타임 슬라이스만큼 돌아가며 실행 (Time Slice 기반)

SJF (Shortest Job First)

실행 시간이 짧은 작업을 먼저 실행

Priority Scheduling

우선순위가 높은 프로세스부터 실행

CFS (Completely Fair Scheduler)

리눅스에서 사용되는 공정성 기반 스케줄러 (동적, 하이브리드 방식)

타임 슬라이스 + 컨텍스트 스위치 + 스케줄러 연결 흐름

[타임 슬라이스 만료]

[스케줄러가 다음 프로세스 선택]

[컨텍스트 스위치 발생]

[선택된 프로세스가 CPU 실행]

예를 들어:

  • 프로세스 A가 10ms 동안 CPU를 썼다 → 타임 슬라이스 끝남

  • 스케줄러가 프로세스 B를 선택

  • 컨텍스트 스위치로 A의 상태 저장 + B의 상태 복원

  • 이제 B가 CPU를 차지함

요약

개념
설명

Time Slice

프로세스에게 CPU를 일정 시간 동안 할당하는 단위

Context Switch

프로세스를 전환할 때 CPU 상태를 저장하고 복원하는 과정

Scheduler

어떤 프로세스를 언제 실행할지 결정하는 운영체제의 핵심 기능

7️⃣ 정리

  1. 프로세스는 실행되는 동안 다양한 상태를 오고 감.

  2. 주요 프로세스 상태

    • 실행 가능(Ready) → R

    • 실행 중(Running) → R

    • 슬립 상태(Sleeping) → S 또는 D

    • 좀비 상태(Zombie) → Z

    • 종료(Terminated) → ps에서 사라짐

  3. 프로세스는 CPU가 필요할 때 실행 상태가 되고, 필요하지 않을 때 슬립 상태로 대기함.

  4. 모든 프로세스가 슬립 상태면 CPU는 아이들 프로세스를 실행하며, 전력을 절약함.

  5. 노트북과 스마트폰이 배터리를 절약할 수 있는 이유는 CPU가 아이들 상태로 대기하기 때문.

5. 프로세스 종료

1️⃣ false 명령어란?

  • 항상 종료 코드 1을 반환하는 명령어

  • 리눅스에서 종료 코드 0은 성공, 0이 아닌 값은 실패를 의미한다.

$ false
$ echo $?   # 종료 코드 확인
1

2️⃣ wait-ret.sh 스크립트 설명

#!/bin/bash

false &
# false 명령어를 백그라운드에서 실행 (즉시 끝남)
# '$!' : 방금 실행된 백그라운드 프로세스의 PID
wait $!
# 해당 PID의 프로세스가 종료될 때까지 기다림

echo "false 명령어가 종료되었습니다: $?"
# '$?' : 직전 명령어(wait)의 종료 상태(즉, false의 종료 코드)

실행 결과 예시

$ ./wait-ret.sh
false 명령어가 종료되었습니다: 1
  • false1을 반환하므로, wait 뒤의 $? 결과도 1이 되는 것.

  • 이걸 통해 백그라운드 프로세스의 종료 코드를 얻을 수 있다.

3️⃣ 프로세스가 종료될 때 실제 내부에서 일어나는 일

1) exit() 호출

  • 프로그램이 exit() 함수를 호출하면,

  • 내부적으로 리눅스 커널은 exit_group() 시스템 콜을 실행함.

  • 이는 현재 프로세스(및 스레드 그룹 전체)를 종료시킴.

2) 커널이 처리하는 일

  • 커널은 프로세스가 사용하던 자원(메모리, 파일, etc.)을 회수함.

  • 프로세스는 완전히 사라지는 것이 아니라 "좀비(zombie)" 상태가 됨.

    • 왜냐면 부모 프로세스가 아직 종료 상태를 수거하지 않았기 때문!

4️⃣ 부모 프로세스는 어떻게 자식의 종료 상태를 알 수 있을까?

함수
설명

wait()

자식 프로세스 하나의 종료를 기다림

waitpid(pid)

특정 PID의 자식 프로세스 종료를 기다림

waitid()

더 많은 제어를 제공하는 버전 (POSIX 표준)

이 시스템 콜들은 다음과 같은 정보들을 커널로부터 받아올 수 있어:

  • 종료 코드 (exit()에 전달된 값)

  • 시그널에 의해 죽었는지 여부 (예: SIGKILL, SIGSEGV)

  • 종료까지 걸린 시간 (CPU 사용량)

5️⃣ 다시 정리하면…

  1. false는 종료 코드 1을 반환하는 명령어이다.

  2. false &로 실행하면 백그라운드 프로세스가 되고,

  3. wait $!해당 PID의 프로세스가 끝날 때까지 기다림

  4. wait 명령 뒤의 $?그 프로세스의 종료 코드

  5. 내부적으로는 exit_group() → 커널이 자원 회수 → 부모는 wait()로 종료 상태 확인

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

좋아! 이 이미지 내용은 좀비 프로세스고아(Orphan) 프로세스에 대한 개념을 깔끔하게 정리하고 있어. 내용을 요약하고 정리해서 다시 설명해볼게.


1) 좀비 프로세스 (Zombie Process)

프로세스는 종료됐지만, 부모가 wait() 시스템 콜로 종료 상태를 확인하지 않아서 아직 완전히 제거되지 않은 상태의 프로세스

📌 특징

  • 이미 exit() 또는 exit_group()으로 종료된 상태임.

  • 하지만 부모가 종료 코드를 수거하지 않아서 커널에서 완전히 제거되지 않음.

    • 부모 프로세스가 자식 프로세스 상태를 wait() 계열 시스템 콜을 해서 어들 수 있다는 말은 반대로 이야기 하면, 자식 프로세스가 종료되어도 부모 프로세스가 이런 시스템 콜을 호출할 때까지 종료된 자식 프로세스는 어떠한 형태로든 존재한다는 것

  • ps 명령어의 STAT 컬럼에서 Z(Zombie) 로 표시됨.

  • 프로세스 테이블에는 여전히 존재하며 PID도 살아 있음.

📌 왜 위험할까?

  • 좀비가 많아지면 프로세스 테이블 자원을 낭비하게 됨.

  • 심할 경우 새로운 프로세스를 생성할 수 없게 될 수도 있음.

2) 고아 프로세스 (Orphan Process)

부모 프로세스가 먼저 종료되고, 자식 프로세스가 아직 살아 있는 상태의 프로세스

📌 특징

  • 부모가 없어졌기 때문에 새로운 부모가 필요함.

  • 리눅스에서는 커널이 자동으로 init 프로세스(PID=1)새 부모로 지정해줌.

  • 즉, 고아가 된 자식 프로세스는 init의 보호를 받게 됨.

3) init 프로세스의 역할

  • init시스템의 최상위 부모 프로세스이며,

  • 고아가 된 자식 프로세스의 새 부모가 됨.

  • 또한, init주기적으로 wait()를 호출하여 고아가 된 좀비 프로세스들을 자동으로 수거함.

  • 이 덕분에 고아 프로세스라도 좀비 상태가 오래 유지되지 않음.

즉, 부모가 없다고 시스템 자원을 떠돌게 하지 않고, 커널이 자동으로 정리해주는 구조가 잘 준비되어 있다는 뜻

✅ 예시로 요약하면

상황
설명
해결 방식

자식이 종료되었지만 부모가 wait() 안 함

좀비 프로세스

부모가 wait() 해야 자원 회수됨

부모가 먼저 종료됨

고아 프로세스

커널이 init 프로세스로 위탁, init이 수거

✅ 실제 시스템에서 확인 방법

  1. 좀비 확인

$ ps aux | grep Z
  1. 고아 프로세스 확인은 직접 확인하기 어려우나 부모 PID가 1(init)로 바뀌는 것을 통해 간접적으로 확인 가능:

$ ps -o pid,ppid,stat,comm

7. 시그널

1️⃣ 시그널이란?

시그널(Signal)은 프로세스에 비동기적으로 전달되는 이벤트 알림이야.

  • 보통 프로세스는 순차적으로 코드를 실행하지만, 시그널은 외부에서 흐름을 중단시키고 특정 처리를 하게 만드는 기능이다.

  • 대표적으로 사용자가 Ctrl+C를 누르면 발생하는 SIGINT가 있다.

  • 프로세스는 각 시그널에 시그널 핸들러를 미리 등록해둔다.

    • 시그널을 받으면 프로세스는 해당 시그널에 대한 핸들러(signal handler)로 처리할 수 있음

    • 실행 중인 처리를 일단 중단하고, 시그널 핸들러에 등록한 처리를 동작시킨 다음 원래 장소로 돌아가서 이전에 하던 동작 재개한다.

2️⃣ 주요 시그널 종류

시그널
설명
기본 동작

SIGINT

Ctrl+C 입력 시

프로세스 종료

SIGTERM

kill 명령어 기본 시그널

종료 요청

SIGKILL

무조건 죽임, 핸들러 무시

강제 종료

SIGSTOP

Ctrl+Z 또는 정지 요청

실행 일시 정지

SIGCONT

SIGSTOP 이후 실행 재개

재개

SIGCHLD

자식 종료 시 부모에게 전달

wait()로 수거

  • 전체 시그널은 man 7 signal에서 확인할 수 있다.

3️⃣ SIGINT 무시하는 예제 (intignore.py)

#!/usr/bin/python3
import signal

# SIGINT(Ctrl+C)를 무시하도록 설정
signal.signal(signal.SIGINT, signal.SIG_IGN)

# 무한 루프
while True:
    pass
  • SIGINT는 기본적으로 프로세스를 종료하지만, 위 코드에서는 signal.SIG_IGN 으로 무시하도록 설정했기 때문에 Ctrl+C를 눌러도 종료되지 않는다.

4️⃣ 시그널의 동작 흐름

프로세스가 시그널을 받으면 다음과 같은 흐름이 발생해:

  1. 기존 동작 중단

  2. 시그널 핸들러 함수 실행

  3. 핸들러 함수 종료 후 원래 흐름 복귀

예외적으로 SIGKILL 같은 시그널은 핸들러로 가는 대신 즉시 강제 종료됨.

5️⃣ 그래도 종료시키고 싶다면?

시그널을 무시하거나 종료되지 않는 프로그램은 다음 방법으로 처리할 수 있다.

✔ Ctrl+Z로 백그라운드 전환 후 종료

$ ./intignore.py
^Z  ← Ctrl+Z
$ bg
$ kill -TERM <pid>

✔ 그래도 안 되면 SIGKILL

$ kill -KILL <pid>
  • SIGKILL핸들러 무시 + 즉시 종료

  • 커널이 직접 처리하기 때문에 반드시 종료됨

6️⃣ SIGKILL도 안 통하는 경우

시그널조차 무시하는 "진짜 죽지 않는 프로세스"도 있음

이런 프로세스는 보통 다음 상태야:

  • STAT = D (Uninterruptible sleep)

  • 보통 디스크 I/O, 네트워크 요청 중에 발생

  • 이 상태에서는 SIGINT, SIGTERM, SIGKILL 모두 무시됨

  • 커널 레벨에서 블로킹 중이기 때문에 해결 불가

  • 시스템 재부팅 외에는 방법이 없는 경우도 많아

$ ps aux | grep D

→ STAT 필드에서 D가 보이면 해당 상태

8. 셸 작업 관리 구현

1️⃣ 셸 작업이란?

bash 같은 셸이 관리하는 백그라운드 작업 단위를 작업(Job)이라고 한다.

$ sleep infinity &     # 백그라운드 실행
[1] 6176               # 작업 번호 [1], PID는 6176
$ jobs                 # 현재 실행 중인 작업 목록 확인
$ fg 1                 # 1번 작업을 포그라운드로 전환
^Z                     # Ctrl+Z → 작업 일시 정지
  • jobs : 현재 작업 목록 조회

  • fg / bg : 포그라운드 / 백그라운드 전환

  • ^Z : 포그라운드 작업 정지(SIGSTOP 발생)

2️⃣ 세션(Session)이란?

사용자가 터미널에서 로그인하면 생성되는 논리적인 작업 단위

  • 터미널(tty/pts 등)에 로그인할 때마다 하나의 세션이 생성

  • 모든 세션에는 해당 세션을 제어하는 단말이 존재한다.

  • 세션 ID 또는 SID라는 부르는 값이 세션에 할당됨

    • 세션 리더라고 하는 프로세스가 존재

    • 보통 셸(bash/zsh)이 세션 리더가 됨 → 세션 리더 PID= 세션 ID

  • 세션은 단말(TTY)에 연결됨 (예: pts/0, tty1)

  • 세션 관련 정보는 ps ajx로 확인 가능

📌 세션 구조 예시

세션 A (pty/0) : bash → go build
세션 B (pty/1) : zsh → ps aux | less
세션 C (pty/2) : zsh → calc

세션 리더와 단말 연결

  • bash 같은 셸은 로그인 세션의 리더 역할을 함 → 즉, 세션 ID(SID) = bash의 PID

  • ps ajx 명령어를 보면, SID 컬럼으로 어떤 세션에 속해 있는지 확인 가능

  • TTY 필드는 해당 프로세스가 어떤 단말(터미널)에 연결되어 있는지를 보여줌 → 예: pts/0, tty1

$ ps ajx
PPID   PID     PGID    SID     TTY     TPGID   STAT  COMMAND
19261  19262   19262   19262   pts/0   19647   Ss    -bash
19262  19647   19647   19262   pts/0   19647   R+    ps ajx

📌 여기서:

  • bash(19262) → 세션 리더 (SID=19262)

  • ps ajx도 같은 세션에 소속됨 (SID=19262, TTY=pts/0)

필드

의미

예시 (bash)

PPID

부모 프로세스 ID

19261 ← sshd 등

PID

이 프로세스의 ID

19262 ← bash

PGID

프로세스 그룹 ID

19262 ← bash 그룹 리더

SID

세션 ID (세션 리더의 PID)

19262 ← bash가 세션 리더

TTY

연결된 단말

pts/0

TPGID

포그라운드 그룹 ID

19647 ← 현재 터미널 주인

STAT

상태

Ss ← 슬립 + 세션 리더

COMMAND

실행된 명령어

-bash

SIGHUP 시그널이란?

터미널(단말) 연결이 끊기면 세션 리더인 bash 등에게 자동으로 보내지는 시그널

  • "Hang Up"의 줄임말

  • 터미널 에뮬레이터(GNOME Terminal, SSH, Tmux 등) 창을 닫거나 연결이 끊기면 발생

  • bash는 SIGHUP을 받으면:

    1. 자신이 관리하던 백그라운드 작업에 먼저 SIGHUP 전달

    2. 그 후 자신도 종료

즉, bash가 종료되면 그 아래 작업들도 같이 죽게 되는 구조

그럼 죽는 순서를 생각해보자

상황 1: bash(19262)가 종료된다면?

  • bash는 이 세션의 리더고, 터미널을 관리하고 있음.

  • bash가 종료되면:

    • 세션과 연결된 단말도 닫힘 (pts/0이 끊긴 것처럼 됨)

    • → 커널은 그 세션의 하위 프로세스들에게 SIGHUP 시그널을 보냄

📌 즉, ps ajx(PID=19647)도 같이 종료될 가능성이 큼!

단, 이건 "bash가 SIGHUP을 전파한다면"이라는 조건이야.

이런 상황에서 작업을 살리려면?

실행 중인 중요한 작업을 터미널 종료에도 살려두고 싶다면 아래 두 가지 방법을 써야 해:

nohup 명령어 사용

$ nohup ps ajx &
  • SIGHUP 시그널을 무시하고 명령어를 실행

  • 터미널이 종료되어도 작업은 살아남음

  • 표준 출력을 nohup.out으로 리디렉션함

disown 내장 명령어 사용 (bash 전용)

$ your_command &
$ disown
  • 이미 실행 중인 작업을 bash의 작업 목록에서 제거

  • 이후 bash가 종료되어도 해당 작업에는 SIGHUP이 전송되지 않음

  • 즉, bash가 죽어도 작업은 계속 실행됨

💥 일반적인 경우 (처리 안 함 → 하위 프로세스도 종료됨)

[ 터미널 ]


 [ bash (세션 리더) ] 
     ├── sleep 1000    ← 하위 프로세스
     └── ps ajx        ← 하위 프로세스

┌───────────────────────────────────────────┐
│ (터미널 닫힘 or bash 종료)               │
│ → SIGHUP 시그널 전파                     │
│ → 하위 프로세스도 같이 종료됨            │
└───────────────────────────────────────────┘

nohup 또는 disown 사용한 경우

[ 터미널 ]


 [ bash (세션 리더) ] 
     └── nohup long_job.sh &  ← 하위 프로세스 (SIGHUP 무시)

┌───────────────────────────────────────────┐
│ (bash 종료 or 터미널 종료)               │
│ → long_job.sh 는 SIGHUP 무시              │
│ → 종료되지 않고 계속 실행됨              │
└───────────────────────────────────────────┘

tmux / screen 사용한 경우

[ 터미널 ]


 [ tmux 세션 ]


 [ bash (tmux 내부에서 동작) ]
     └── ./my_script.sh &     ← 하위 프로세스

┌───────────────────────────────────────────┐
│ (터미널 종료)                             │
│ → tmux는 살아 있음                       │
│ → 하위 프로세스도 영향 X                  │
│ → 나중에 다시 접속해서 계속 작업 가능    │
└───────────────────────────────────────────┘
상황
하위 프로세스
설명

그냥 실행 (bash 종료)

❌ 종료됨

SIGHUP 전파

nohup 또는 disown 사용

✅ 유지됨

SIGHUP 무시

tmux, screen 사용

✅ 유지됨

터미널 독립 실행 환경

3️⃣ BASH란?

Bash (Bourne Again SHell) 는 리눅스와 macOS에서 가장 널리 쓰이는 셸(Shell) 중 하나야.

셸(Shell)이라는 게 뭐야?

  • 사람(사용자)과 운영체제(커널) 사이에서 명령어를 전달해주는 인터페이스

  • 키보드로 명령어를 입력하면, 셸이 그걸 파싱(해석)해서 커널에 전달해 실행해줌

셸의 예:

셸 종류
설명

bash

리눅스 기본 셸, 대부분 디폴트로 제공

zsh

화려한 자동완성, 테마 기능 풍부

sh

가장 기본적인 POSIX 셸

fish

사용자 친화적인 modern 셸

tcsh

csh의 확장 버전

그럼 Bash는 어떤 역할을 해?

역할
설명

명령어 실행기

사용자가 입력한 명령어를 해석해서 실행함 (ls, cd, ps 등)

스크립트 실행기

.sh 스크립트 파일도 실행할 수 있음 (#!/bin/bash)

프로세스/세션 리더

로그인하면 bash가 실행되면서 세션 리더가 됨

작업 제어 (job control)

&, fg, bg, jobs, disown 같은 작업 관리 기능 제공

환경 관리

환경변수 설정 (PATH, HOME, PS1 등)

시그널 전달

자식 프로세스에게 SIGHUP, SIGINT 등을 전달함

Bash는 어디서 써?

  • 리눅스 터미널에서 명령어 칠 때 → 바로 그게 bash

  • .bashrc, .bash_profile 같은 설정 파일로 사용자 환경 구성

  • 배포 스크립트, 자동화 스크립트도 bash로 작성 가능

#!/bin/bash
echo "Hello, world!"

Bash는 단순히 프로그램이다

  • /bin/bash 또는 /usr/bin/bash 경로에 있는 하나의 실행 파일

  • 프로세스 테이블에도 bash라는 이름으로 나타남

  • 로그인하면 bash 프로세스가 실행되고, 여기에 너의 명령어를 입력하는 거야!

4️⃣ 프로세스 그룹(Process Group)이란?

셸이 실행한 명령 단위로 묶인 프로세스 집합, 여러 프로세스를 하나로 묶어서 한꺼번에 관리

  • go build &, ps aux | less → 각각 독립적인 프로세스 그룹

  • PGID (Process Group ID) 로 식별됨

  • 기본적으로 셸이 만든 작업이 프로세스 그룹에 해당한 다고 생각하면 됨

  • 셸은 작업을 프로세스 그룹 단위로 관리

  • 아래 예시

    • bash는 go build <소스코드명>& ps aux less에 대응하는 2개의 프로세스 그 룹(작업)을 작성

- 로그인 셸은 bash
- bash에서 go build <소스코드명> & 실행
- bash에서 ps aux less 실행

✅ 그룹 전체에 시그널 보내기

$ kill -<PGID>
# 예: PGID가 100이면 → kill -100

→ PGID가 100인 모든 프로세스에 시그널 전송

5️⃣ 포그라운드 & 백그라운드 프로세스 그룹

유형
설명
특징

포그라운드 그룹

셸에서 현재 직접 조작하는 그룹

단말 입력을 받을 수 있음, 세션당 하나 존재

백그라운드 그룹

백그라운드로 실행된 작업

단말 접근 시 SIGSTOP 발생처럼 실행 일시 중단, fg 내장 명령어 등으로 프로세스가 포그라운드 프로세스 그룹(또는 포그라운드 작업)이 될 때까지 이 상태가 유지

  • 단말(tty)은 동시에 한 그룹만 접근 가능

  • fg, bg 명령어로 포/백 전환 가능

  • ps ajx 명령어에서 STAT 필드에 +가 붙으면 포그라운드 그룹

  • 단말에 직접 접근하려면 포그라운드 프로세스 그룹(포그라운드 작업)이 된 이후에 가능

6️⃣ ps ajx로 세션/그룹 확인하기

$ ps ajx
필드
설명

PID

프로세스 ID

PGID

프로세스 그룹 ID

SID

세션 ID

TTY

연결된 단말

STAT

상태 (+: 포그라운드, S: 슬립, R: 실행 중)

예시:

PPID   PID     PGID    SID     TTY     TPGID   STAT   COMMAND
19261  19262   19262   19262   pts/0   19653   Ss     -bash
19262  19653   19653   19262   pts/0   19653   R+     ps ajx
19262  19654   19653   19262   pts/0   19653   S+     less
  • bash는 세션 리더(PID = SID = 19262)

  • ps ajx, less는 같은 PGID = 19653 → 파이프라인으로 묶인 하나의 작업

  • + 붙은 STAT → 현재 단말의 포그라운드 프로세스 그룹

7️⃣ 세션이 끊기면 발생하는 SIGHUP 시그널

  • 사용자가 터미널을 닫거나 연결이 끊기면 → 세션 리더(bash 등)에 SIGHUP 시그널이 감

  • bash는 SIGHUP을 받으면 자신이 관리하던 작업을 모두 종료

✅ 죽이지 않게 하는 방법

명령어
설명

nohup

SIGHUP 무시 후 백그라운드 실행

disown

작업을 bash의 관리 대상에서 제외

$ nohup ./long_job.sh &
$ disown

8️⃣ 요약

개념
설명

세션(Session)

로그인 단위. 셸이 리더, 단말에 연결됨

프로세스 그룹

셸이 만든 명령 단위. 작업 단위로 묶임

작업(Job)

셸이 추적/제어하는 백그라운드 작업

PGID

프로세스 그룹 ID

SID

세션 ID (보통 셸의 PID)

포그라운드/백그라운드

단말 접근 여부에 따라 구분

jobs, fg, bg

셸에서 작업을 관리하는 명령어

kill -PGID

프로세스 그룹 전체에 시그널 전송

데몬

좋아! 이번엔 유닉스/리눅스 시스템에서 자주 듣게 되는 **데몬(Daemon)**에 대해 정리해줄게. 이건 단순한 "백그라운드 프로세스"랑은 차이가 있어. 아래처럼 이모지 번호로 쭉 정리할게!


1️⃣ 데몬(Daemon)이란?

시스템 백그라운드에서 상시 동작하는 프로세스

  • 사용자 명령으로 잠깐 실행되는 프로세스와 달리,

  • 시스템 부팅 시 자동으로 시작되어 꺼질 때까지 계속 동작

  • 주로 서비스 제공, 모니터링, 서버 역할을 담당함 예: sshd, cron, httpd, systemd, mysqld

2️⃣ 데몬의 주요 특징

특징
설명

단말 미연결

입출력을 단말(TTY)에 연결하지 않음

독립 세션

고유의 세션을 가짐 (SID = PID)

init이 부모 프로세스

부모가 없거나 죽으면 PID 1(init)에게 위탁됨

로그인 세션 무관

사용자가 로그아웃해도 계속 동작

📌 즉, 로그인 세션과 완전히 독립적으로 동작하는 "상주 서비스" 역할을 함

3️⃣ 데몬 확인 예시

$ ps ajx

예:

PPID   PID    PGID   SID    TTY   STAT   COMMAND
   1    960    960    960    ?     Ss     sshd: /usr/sbin/sshd -D ...

🔍 여기서 보면:

  • PPID = 1 → 부모가 init

  • TTY = ? → 단말 없음

  • SID = PID → 독립 세션

이런 조건이 데몬 프로세스의 전형적인 특징이야!

4️⃣ 데몬 vs 일반 프로세스 차이

항목
일반 프로세스
데몬 프로세스

실행 방식

사용자가 명령어로 실행

시스템 부팅 시 자동 실행

단말(TTY)

셸, 터미널에 연결

연결 X

종료 시점

작업 완료 시 종료

시스템 종료 전까지 계속 실행

부모

셸(bash 등)

보통 init (PID 1)

예시

ls, vim, gcc

sshd, cron, systemd

5️⃣ SIGHUP 시그널의 다른 의미

  • 일반 프로세스: SIGHUP → 터미널 끊기면 종료 신호

  • 데몬 프로세스: SIGHUP설정 파일 재로드용 시그널로 자주 사용됨

$ kill -HUP <데몬 PID>

예를 들어:

$ sudo kill -HUP $(pidof sshd)

sshd가 설정 파일을 다시 읽고 재시작하지 않고 적용함

6️⃣ 데몬은 꼭 완벽히 조건을 갖춰야만 하나?

꼭 그렇진 않아. 실무에서는 다음과 같은 경우도 데몬이라고 부르기도 해:

  • 부모는 init이 아님

  • TTY가 잠깐 연결되기도 함

  • 독립 세션이 아닐 수도 있음

하지만 상시 백그라운드에서 동작한다면 편의상 데몬이라 부름. 그래서 python my_script.py &도 “데몬처럼” 동작할 수 있어.

7️⃣ 진짜 데몬은 어떻게 만들어?

C에서는 다음 3단계로 만드는 게 기본 흐름이야:

  1. fork() 후 부모 종료 → 자식은 백그라운드

  2. setsid() 호출 → 새로운 세션으로 독립

  3. 파일 디스크립터 stdin, stdout, stderr 닫기 → TTY 분리

(원하면 이걸 C 코드 예제로도 보여줄게!)

8️⃣ 요약

구분
데몬

정의

상시 동작하는 시스템 백그라운드 프로세스

TTY 연결

❌ 없음

세션

독립 세션 (SID=PID)

부모

보통 init

종료

수동 종료 or 시스템 종료

예시

sshd, cron, systemd, nginx

번외

systemd에 데몬 등록하기 (.service 유닛 작성)

리눅스에서 요즘 거의 모든 데몬은 systemd에 의해 관리돼. 직접 만든 쉘 스크립트나 실행 파일도 데몬처럼 부팅 시 자동 실행할 수 있어!

✅ 예: 내 스크립트를 데몬으로 등록

① 실행할 스크립트 만들기

$ sudo nano /usr/local/bin/my_daemon.sh
#!/bin/bash
while true; do
  echo "$(date): running..." >> /var/log/my_daemon.log
  sleep 10
done
$ sudo chmod +x /usr/local/bin/my_daemon.sh

② systemd 유닛 파일 만들기

$ sudo nano /etc/systemd/system/my-daemon.service
[Unit]
Description=My Custom Daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/my_daemon.sh
Restart=always
User=root

[Install]
WantedBy=multi-user.target

③ 등록하고 시작하기

$ sudo systemctl daemon-reexec       # systemd 재시작
$ sudo systemctl enable my-daemon    # 부팅 시 자동 실행 등록
$ sudo systemctl start my-daemon     # 지금 실행
$ sudo systemctl status my-daemon    # 상태 확인

journalctl -u my-daemon 으로 로그도 확인 가능!

Python에서 데몬 스레드 만들기

Python에서는 스레드를 데몬처럼 만들 수 있어. 메인 스레드가 종료되면 같이 종료됨, 백그라운드 작업에 적합!

✅ 예제 코드

import threading
import time

def background_task():
    while True:
        print("백그라운드에서 동작 중...")
        time.sleep(2)

# 데몬 스레드 설정
t = threading.Thread(target=background_task)
t.daemon = True   # 여기가 포인트!
t.start()

# 메인 스레드 종료되면 데몬도 같이 종료됨
print("3초 뒤 메인 종료")
time.sleep(3)
print("메인 스레드 종료")

📌 핵심

옵션
설명

t.daemon = True

스레드를 데몬으로 설정

메인 스레드 종료 시

데몬 스레드도 자동 종료됨

참고

Last updated