QNX RTOS: 5-7. Server Designs

2025. 12. 2. 22:53운영체제/QNX

1. 싱글 스레드 서버: 가장 단순한 기본형

  • 가장 기본적인 서버 구조는:
    for (;;) {
        rcvid = MsgReceive(chid, &msg, sizeof(msg), &info);
        // msg 처리
        MsgReply(rcvid, EOK, &reply, sizeof(reply));
    }

     

  • 여러 클라이언트가 있더라도:
    • Client1 → 처리 → Reply
    • 그 다음 Client2 → 처리 → Reply
    • … 이런 식으로 순차 처리만 해도,
  • 각 요청을 처리하는 시간이 짧고, 전체 부하가 크지 않으면
    한 스레드로도 충분함.

장점:

  • 코드가 단순함 (동기 처리, 공유 데이터 race 걱정 적음)
  • Mutex, lock, 복잡한 동기화 로직이 거의 필요 없음

단점:

  • 어떤 요청 하나가 오래 걸리면 (예: 큰 연산, 장시간 I/O):
    • 그 동안 다른 클라이언트는 MsgReceive까지 도달 못 해서 기다림
    • 특히 마감시간(Deadline)이 짧은 고우선순위 클라이언트가 와도,
      • 서버가 이전 요청 처리에 묶여 있으면 제때 못 처리할 수 있음

 

즉, “응답 시간이 긴 요청이 섞여 있으면 싱글 스레드로는 위험”.


2. 멀티 스레드 서버: 워커 스레드 풀 구조

그래서 나오는 전형적인 구조:

“서버 안에 여러 개의 워커 스레드를 두고, 전부 같은 채널에서 MsgReceive()로 대기시킨다.”

2.1 구조 그림

  • 서버 프로세스 안에:
    • Thread 1: MsgReceive(chid, ...)에서 block
    • Thread 2: MsgReceive(chid, ...)에서 block
    • Thread 3: MsgReceive(chid, ...)에서 block
  • 클라이언트가 메시지를 보내면:
    • 커널이 대기 중인 서버 스레드 중 하나를 골라서 깨움
    • 그 스레드에게 클라이언트의 priority를 상속 (priority inheritance) 시켜줌
    • 그 스레드가 해당 클라이언트 요청만 처리한 뒤 MsgReply()를 호출하면 끝

여기서 중요한 포인트:

  • 여러 스레드가 있어도 모두 같은 코드 라인에서 MsgReceive()를 호출하고 있음
    → 서로 완전히 동일한 “워커 스레드”라고 보면 됨
  • 어떤 스레드가 선택될지는 커널이 알아서 고름 (문서화된 보장은 없음)

2.2 장점

  1. 응답 지연(latency) 감소
    • Client1의 긴 작업을 Thread3가 처리 중이어도
    • Client3(짧은 deadline, 높은 priority)의 요청이 오면
      → Thread1 같은 다른 스레드가 바로 깨어나서 작업 수행
    • 필요하면 Thread1이 Thread3를 preempt해서 먼저 실행될 수도 있음
  2. 멀티코어 활용
    • 코어가 4개라면, 서로 다른 스레드들이 서로 다른 코어에서 동시에 실행 가능
    • Throughput + deadline 만족률 모두 향상
  3. Priority inheritance 자동 처리
    • 메시지 패싱 자체가 priority inheritance를 해주기 때문에
    • 서버 내부에서 “어떤 클라이언트 priority였는지”를 따로 전달·관리할 필요가 없음

2.3 다른 구조와 비교

  • 다른 설계도 가능은 함:
    • 예: 수신 전담 스레드 하나가 MsgReceive()만 하고,
      내부 queue에 넣어두고, 워커 스레드에게 분배하는 구조
  • 하지만 이 방식은:
    • 서버 내부에서 다시 “메시지 → 스레드” 디스패칭을 해야 해서 오버헤드 증가
    • priority inheritance도 직접 신경 써야 할 수 있음

그래서 “실용적인 QNX 서버”는 대부분 MsgReceive()를 여러 스레드가 같이 들고 있는 구조를 많이 씀.


3. 즉시 Reply하지 않는 서버 (지연 Reply 패턴)

두 번째 중요한 포인트는:

“서버는 항상 MsgReceive 직후에 바로 MsgReply할 필요가 없다.”

즉, 클라이언트 메시지를 받은 뒤, 나중에 조건이 만족되었을 때 Reply 할 수 있음.

 

3.1 큐/대기열 예시

시나리오:

  • Client A: “큐 3에서 데이터 하나 주세요. 기다릴게요” 라는 메시지 전송
  • 서버가 내부 상태를 보니:
    • 큐 3에 아직 데이터가 없음

이때 서버는 두 가지 선택을 할 수 있음:

  1. 바로 에러 리턴: “지금은 데이터 없음, 나중에 다시 시도해”
  2. 클라이언트를 REPLY blocked 상태로 유지하고, 나중에 데이터가 생겼을 때 알려주기

 

2번 선택:

  1. 서버는 해당 요청의 rcvid, 요청한 큐 번호, 바이트 수 등을 내부 client list에 저장
  2. 그리고 MsgReply를 아직 하지 않고 다시 MsgReceive()로 돌아감 → 다른 요청/이벤트 처리
  3. 나중에 다른 클라이언트 or 하드웨어 인터럽트가 큐 3에 데이터를 넣었을 때:
    • “아, 큐 3 기다리던 애 있다!” 라는 걸 client list에서 보고
    • 그때 비로소 해당 rcvid에 대해 MsgReply() 실행 → “여기 네가 기다리던 데이터”

이 구조가 굉장히 많이 쓰임:

  • 메시지 큐 서버
  • 장치 드라이버 (하드웨어에서 데이터 들어올 때까지 기다리는 클라이언트들)
  • 파일 시스템의 read() 구현 등

3.2 /devc-pty + shell 예시

 

  • shell 이 /devc-pty (pseudo-terminal 드라이버)에:
    • “사용자가 키보드로 타이핑한 input 주세요. 기다릴게요” 메시지 전송
  • 아직 사용자가 아무것도 안 쳤다면:
    • 드라이버는 “지금 줄 데이터 없음”
    • 하지만 shell은 “데이터 생길 때까지 기다릴게” 라고 했으므로
      • shell은 REPLY blocked 상태
      • 드라이버 프로세스는 Receive blocked 상태로 입력을 기다릴 수 있음
  • 사용자가 키보드를 치면:
    • 인터럽트 → 드라이버가 입력 버퍼 채우고,
    • 그제서야 MsgReply()로 shell에게 “타이핑된 문자열”을 넘겨줌
  • shell은 reply를 받고 나면, 다시:
    • “또 입력 주세요, 기다릴게요” 메시지를 보냄

이게 매우 전형적인 “입력 주도형 (input-driven)” 구조임.


4. 정리

강의에서 말한 포인트를 한 줄씩 요약하면:

  1. 싱글 스레드 서버
    • 단순하고 안전하지만, 오래 걸리는 작업이 섞이면
      다른 클라이언트가 제때 처리되지 못할 수 있음.
  2. 멀티 스레드 서버 (워커 풀 + 공용 채널)
    • 여러 스레드가 동일 채널에서 MsgReceive()로 대기
    • 클라이언트가 메시지 보내면 커널이 스레드 하나 골라서 깨우고
      해당 클라이언트 priority를 상속해서 실행 → latency, deadline, throughput 향상
  3. 지연 Reply(Delayed Reply) 패턴
    • MsgReceive()로 메시지 받아도 바로 MsgReply() 하지 않고
      rcvid를 저장해뒀다가 이벤트(데이터 도착 등)가 생겼을 때 reply하는 구조
    • 큐 서버, 디바이스 드라이버, 터미널 드라이버 등에서 매우 흔하게 쓰임
    • 클라이언트는 그동안 REPLY blocked 상태로 “데이터를 기다리는 중”