8차스터디 - 일괄 배치 (이지만 OS이야기만 주구장창)

김태훈·2026년 4월 10일

하지만 즐거운...

파일 디스크립터

파일 디스크립터는 “파일 번호”가 아니라, 커널이 관리하는 I/O 대상에 접근하기 위한 참조값이다

파일 디스크립터를 단순히 파일을 가리키는 숫자로 이해하면 범위가 너무 좁다.
실제로는 파일뿐 아니라 소켓, 파이프, 표준 입력/출력처럼 커널이 관리하는 다양한 I/O 대상을 가리키는 핸들에 가깝다.

즉, 프로세스는 파일 디스크립터를 통해 어떤 대상에 대해 read/write 같은 I/O 작업을 수행한다.
이 관점에서 보면 파일, 네트워크 소켓, 파이프가 서로 완전히 다른 것이 아니라, 사용자 입장에서는 공통된 I/O 모델로 다뤄지는 대상이라는 점이 중요하다.

epoll, poll, select

C10K 를 극복하기 위해 등장한 select

  • 정해진 fd의 개수 (1024개)
  • 1024개 전체 사용하지 않더라도 이를 넘겨야함
  • O(n)의 fd 모니터링

poll

  • fd를 고정된 번호의 비트로 관리하는게 아니라, 구조체배열 형태로 관리하여 fd 번호의 유연성 증가
  • 하지만 그래도 관리되고 있는 fd는 전체 순회 O(n)

epoll

  • 커널이 ready list를 관리 (실제 이벤트가 발생한 fd만)
  • epoll_ctl로 감시 fd 등록
  • fd 가 가리키고 있는 버퍼에 데이터 도착시 내부적으로 ready list에 넣음
  • O(1)로 준비된 fd 확인 가능

3. OS의 blocking과 I/O 대기

이벤트 루프는 단순히 “계속 돌면서 처리하는 스레드”가 아니다.
실제로는 OS가 제공하는 I/O 대기 메커니즘 위에서 동작한다.

네트워크 서버는 여러 연결을 다뤄야 하므로, 각 연결마다 무작정 스레드를 막아 세우는 방식보다
어떤 파일 디스크립터에 읽을 데이터가 준비되었는지 OS에 물어보고,
준비된 것만 처리하는 방식이 필요하다.
이때 등장하는 것이 select, poll, epoll 같은 I/O multiplexing 방식이다.

그래서 이벤트 루프를 제대로 이해하려면 프레임워크 API보다 먼저,
커널이 I/O 준비 상태를 어떻게 알려주는지,
그리고 스레드가 무엇을 기다리며 block되는지를 이해해야 한다.

OS스레드 상태

OS는 스레드를
“무슨 일을 하고 있느냐”보다
지금 CPU에서 실행 가능한 상태인가, 아니면 무엇인가를 기다리고 있는가의 관점으로 본다.

즉, OS 입장에서 중요한 건 아래 두 가지다.

  1. 이 스레드는 당장 CPU를 주면 실행할 수 있는가
  2. 아니면 어떤 자원이나 이벤트를 기다리느라 멈춰 있는가

그래서 OS의 스레드 상태는 자바의 BLOCKED, WAITING 같은 분류와는 조금 다르다.
자바 상태가 애플리케이션/JVM 관점의 의미라면,
OS 상태는 스케줄링과 자원 대기 관점에 더 가깝다.

Running

스레드가 실제로 CPU 위에서 실행 중인 상태다.
이건 말 그대로 지금 일을 하고 있는 상태다.
CPU 코어 수가 4개라면, 동시에 running일 수 있는 스레드 수도 최대 4개다.

Runnable

스레드는 실행할 준비가 끝났지만, 아직 CPU를 받지 못한 상태다.
즉,

  • 코드 실행 가능
  • 락이나 I/O 때문에 막힌 것도 아님
  • 그냥 CPU 차례를 기다리는 중

이 상태는 “쉬고 있다”가 아니라
당장 돌릴 수 있는데 CPU가 부족하거나 스케줄 순서를 기다리는 상태라고 보는 게 맞다.

Sleeping / Waiting

스레드가 어떤 조건이 충족되기를 기다리며 멈춰 있는 상태다.
이건 다시 두 가지로 나눠서 보는 게 좋다.

  1. Interruptible sleep
    이벤트나 신호를 받으면 깰 수 있는 대기 상태다.
    예를 들면:
    소켓 데이터 도착 대기, 타이머 대기, 어떤 이벤트 발생 대기
    리눅스에서 보통 S 상태로 보게 된다.

  2. Uninterruptible sleep
    주로 커널 I/O 완료를 기다리는 상태다.
    예를 들면
    디스크 I/O 대기,일부 커널 자원 대기.
    리눅스에서는 흔히 D 상태라고 부른다.
    이 상태는 load average와도 연결되기 때문에 운영에서 특히 중요하다.

왜 Runnable과 Sleeping을 구분해야 하나

둘 다 “지금 실행 중이 아님”이라는 점은 같지만, 의미는 완전히 다르다.

Runnable
CPU만 받으면 바로 실행 가능하다.
즉 병목이 CPU 쪽일 가능성이 크다.

Sleeping
CPU를 줘도 바로 실행할 수 없다.
무언가를 기다리고 있기 때문이다.

그래서 LoadAverage에는 왜 uninterruptible도 포함됨?

이 스레드는 CPU를 바로 쓰진 못하지만, 시스템 입장에선 여전히 끝나지 않은 작업.
CPU의 부하가 아닌, 시스템의 부하이다.

근데 왜 uninterruptible하게 설계했을까?

어차피 디스크를 다 읽는 작업이 완료시 이벤트성으로 알려주게하면 안되었을까??

그 대기를 중간에 깨워도 유의미한 작업인 경우가 없고, 오히려 커널 내부 상태와 장치 상태의 일관성을 안전하게 유지하기 더 어렵기 때문.
중간에 커널이 요청 큐에 넣고, 디스크 작업을 걸고, 버퍼/page cache/블록 계층 상태를 잡고, 완료 시점에 후처리 -> 이거 어케보장?
또한, 디스크 응답이 아직 안 옴 / 필요한 페이지가 아직 안 올라옴 / 블록 장치 작업이 아직 안 끝남 -> 어케보장?

이벤트루프 스레드 동작방식

blocking I/O
blocking 소켓에 대해 while loop에서 read()를 호출하면, 데이터가 없을 때 스레드는 busy waiting을 하는 것이 아니라 read() 안에서 block된다. 이 경우 스레드는 데이터가 도착할 때까지 sleep 상태로 들어갈 수 있다.

이벤트 루프
이벤트 루프는 보통 non-blocking 소켓을 사용하므로, read() 자체가 오래 block되는 구조가 아니다. 대신 epoll_wait()나 poll() 같은 I/O multiplexing 호출에서 이벤트가 올 때까지 block되고,
준비된 소켓에 대해서만 read()를 수행한다.

profile
기록하고, 공유합시다

0개의 댓글