2026. 1. 23. 18:00ㆍ운영체제/QNX
1) 클라이언트 관점: cat /dev/example이 왜 “0 바이트면 종료”인가
cat의 핵심 루프는 사실상 다음입니다.
- open("/dev/example")
- 반복:
- n = read(fd, buf, N)
- n > 0이면 화면에 write(1, buf, n) 후 반복
- n == 0이면 EOF로 판단하고 반복 종료
- 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 핸들러가 플래그를 보고 그때 시간을 채웁니다.
- ocb->attr->flags |= IOFUNC_ATTR_ATIME; 같은 방식으로
POSIX 관점에서 “read 시점 정확한 atime”이 아니라 “stat 시점 atime”이 되어도 허용되는 범위가 있어서, 실무적으로 많이 쓰는 패턴이라는 설명입니다.
그리고 스크립트의 조건:
- msg->i.nbytes > 0일 때만 atime 업데이트 플래그를 세팅
- read(fd, buf, 0)은 실질적으로 데이터를 읽지 않으므로 atime을 건드리지 않는다는 논리입니다.
'운영체제 > QNX' 카테고리의 다른 글
| QNX RTOS: 10-4. Handling read() and write() - write() (0) | 2026.01.23 |
|---|---|
| QNX RTOS: 10-2. A Simple Resource Manager (0) | 2026.01.23 |
| QNX RTOS: 10-1. Overview of Resource Managers (0) | 2026.01.23 |
| QNX RTOS: 9-1. Images & Buildfiles (0) | 2026.01.02 |
| QNX RTOS: 8-6. Kernel Timeouts (0) | 2026.01.02 |