QNX RTOS: 10-3. Handling read() and write() - read()

2026. 1. 23. 18:00운영체제/QNX

1) 클라이언트 관점: cat /dev/example이 왜 “0 바이트면 종료”인가

cat의 핵심 루프는 사실상 다음입니다.

  1. open("/dev/example")
  2. 반복:
    • n = read(fd, buf, N)
    • n > 0이면 화면에 write(1, buf, n) 후 반복
    • n == 0이면 EOF로 판단하고 반복 종료
  3. close(fd)

따라서 RM이 read에 0바이트를 응답하면(EOF) cat은 즉시 끝납니다. (POSIX 규칙)


2) QNX 내부 동작: read()는 “시스템 콜”이 아니라 “메시지 전송”

QNX는 마이크로커널이므로 read()는 커널 내부에서 파일을 직접 읽는 방식이 아니라:

  • read() 라이브러리 함수가
    • _IO_READ 타입의 메시지(struct _io_read)를 구성하고
    • MsgSend()로 리소스 매니저 프로세스에 보냅니다.
  • 리소스 매니저는 이를 받아 io_read() 핸들러로 디스패치합니다.

메시지에는 최소한 다음이 들어옵니다.

  • 메시지 타입: _IO_READ (첫 2바이트)
  • 확장 타입: xtype (기본은 _IO_XTYPE_NONE)
  • 요청 바이트 수: nbytes (클라이언트가 원하는 읽기 크기)

핸들러에서는 msg->i.nbytes로 접근합니다. (io_read_t가 union이고 i overlay로 _io_read를 본다는 점이 포인트)


3) 핸들러 시그니처: read/write가 공통으로 받는 것

(1) ctp (context pointer, dispatch context pointer)

핸들러는 MsgReceive()를 직접 호출하지 않습니다. 대신 프레임워크가 dispatch_block() 내부에서 수신을 수행하고, 그 결과를 ctp로 전달합니다.

ctp에는 다음이 포함됩니다.

  • rcvid : 누구에게 reply할지 결정하는 수신 ID
  • info : 클라이언트 pid/tid, scoid, 메시지 길이 등
  • msg / msg_max_size : 수신 버퍼 포인터와 크기

즉, reply의 대상(rcvid)과 수신 메시지 자체(msg)가 ctp에 있다가 핵심입니다.

(2) ocb (Open Control Block)

  • open 1회당 1개 생성되는 “per-open 상태” 구조
  • 기본 open 핸들러(iofunc_open_default())가 할당+초기화해 줍니다.
  • 타입은 기본적으로 iofunc_ocb_t이며, 필요하면 이를 감싸서(embedded) 사용자 per-open 데이터를 추가합니다.

대표적인 per-open 데이터 예: 파일 오프셋(file position / offset)

  • 같은 프로세스가 open()을 두 번 하면 OCB도 두 개가 생기고, 각 offset은 0에서 시작합니다.

또한 OCB에는 디바이스 속성(iofunc_attr_t)을 가리키는 포인터가 있어,
핸들러는 ocb->attr를 통해 장치 속성(버퍼, 권한, timestamps 등)에 접근합니다.


4) read() 처리의 핵심 규칙: “얼마를, 어떻게 reply할 것인가”

4.1 성공/EOF/에러의 POSIX 의미

클라이언트 read(fd, buf, wanted) 기준:

  • 에러: -1 반환 + errno 설정
  • EOF: 0 반환
  • 성공: 1..wanted 바이트 반환

특이 케이스:

  • wanted == 0이면, read 권한이 있으면 0 반환(성공) 이 가능
  • read 권한 없으면 -1 + EPERM 등

4.2 QNX에서 “read의 반환값”은 어디서 결정되나

핵심은 다음 한 줄입니다.

  • MsgReply(rcvid, status, data, nbytes)에서 status가 클라이언트의 read() 반환값이 된다.

왜냐하면:

  • 클라이언트의 read()는 내부적으로 MsgSend()를 호출하고,
  • read()는 MsgSend()의 반환값을 그대로 반환하기 때문입니다.

따라서 read 핸들러에서는:

  • 성공 시: status = 실제로 돌려준 바이트 수 (0이면 EOF)
  • reply 데이터 길이: nbytes = 실제로 복사해 줄 데이터 크기

학생들이 “status와 nbytes가 왜 같아 보이냐”에 헷갈리는데,
read에서는 논리적으로 보통 같지만 의미가 다릅니다.

  • status: 유저에게 보이는 read() return value
  • nbytes: 커널이 reply로 복사할 데이터 길이

5) 예제 read 핸들러의 표준 골격

리소스 매니저의 io_read()는 보통 다음 순서가 정석입니다.

  • iofunc_read_verify(ctp, msg, ocb, NULL) 호출
    • open 모드가 write-only인지 등, 기본 권한/상태 체크를 프레임워크에 맡김
    • 실패하면 그 값을 그대로 return (에러 흐름으로 넘어감)
  • xtype 체크
    • 정상 기대값: _IO_XTYPE_NONE
    • 아니면 return ENOSYS (지원하지 않는 확장 타입)
  • 데이터 준비 및 reply
    • 예제 초기 상태: 데이터 없음 → MsgReply(ctp->rcvid, 0, NULL, 0) (EOF)
    • 과제에서는 여기에서 실제 데이터를 넣도록 변경
  • _RESMGR_NOREPLY 반환
    • 이미 MsgReply()로 직접 답했으니 “라이브러리가 추가 reply 하지 말라”는 의미


6) 에러 처리: “핸들러에서 errno를 return하면 된다”

read 핸들러에서 문제가 생기면:

  • return errno_value; (예: EIO, EPERM, EINVAL 등)

그러면 프레임워크가 내부적으로 MsgError()를 호출해

  • 클라이언트 MsgSend()가 -1 리턴
  • 클라이언트 errno가 설정
  • 결국 read()가 -1을 리턴

팁(스크립트): 가능하면 read() 매뉴얼에 문서화된 errno 중에서 선택하고, 없으면 errno.h에서 합당한 것을 고릅니다.


7) Access time(atime) 갱신: 즉시 갱신 vs “나중 갱신” 플래그

read를 처리할 때 “마지막 접근 시간(access time, atime)”을 갱신해야 하는 경우가 있습니다.

  • 매 read마다 커널 호출로 현재 시간을 받아서 즉시 저장하면 정확하지만 성능 비용이 큽니다.
  • 그래서 많은 RM은:
    • ocb->attr->flags |= IOFUNC_ATTR_ATIME; 같은 방식으로
      “atime 업데이트 필요”만 표시하고,
    • 나중에 클라이언트가 stat() 호출하면
      기본 stat 핸들러가 플래그를 보고 그때 시간을 채웁니다.

POSIX 관점에서 “read 시점 정확한 atime”이 아니라 “stat 시점 atime”이 되어도 허용되는 범위가 있어서, 실무적으로 많이 쓰는 패턴이라는 설명입니다.

그리고 스크립트의 조건:

  • msg->i.nbytes > 0일 때만 atime 업데이트 플래그를 세팅
  • read(fd, buf, 0)은 실질적으로 데이터를 읽지 않으므로 atime을 건드리지 않는다는 논리입니다.