QNX RTOS: 5-5. Multi-Part Messages
2025. 12. 2. 17:59ㆍ운영체제/QNX
1. 문제 상황: 여러 큰 버퍼를 한 번에 보내고 싶을 때
클라이언트에 버퍼가 세 개 있다고 해보면:

- 750 KB
- 500 KB
- 1000 KB
→ 총 2.25 MB
지금까지 배운 MsgSend()는 “버퍼 하나 + 길이 하나”만 넘길 수 있었음.
그럼 naive하게는:
- 2.25 MB짜리 큰 버퍼를 malloc()으로 새로 만들고
- memcpy()로 세 버퍼를 거기에 이어붙인 다음
- 그 큰 버퍼를 MsgSend()로 보냄

문제:
- 클라이언트 메모리에 2.25 MB를 두 번 들고 있게 됨 (원본 + 합쳐진 것)
- memcpy() 세 번 + 커널이 client→server 한 번 복사 → 복사 비용 과도
QNX는 원래 client→server 한 번 복사만 해도 “안전성/격리” 때문에 어쩔 수 없이 감수하는 건데,
두 번 복사하는 건 너무 아깝다 → 그래서 나온 게 multi-part message / IOV.
2. 해결책: IOV (Input/Output Vector) + MsgSendv()
2.1 IOV란?

IOV = (포인터, 길이)의 배열
typedef struct {
void *iov_base; // 버퍼의 시작 주소
size_t iov_len; // 이 버퍼에서 몇 바이트를 쓸지
} iov_t;
- 각 엔트리가 “이 버퍼를 이 길이만큼 사용”이라는 디스크립터 역할
- 보통 iov[0], iov[1], iov[2] ... 형태의 배열로 사용
세 개 버퍼를 보낼 때:
iov_t iovs[3];
SETIOV(&iovs[0], buf1, len1); // 750 KB
SETIOV(&iovs[1], buf2, len2); // 500 KB
SETIOV(&iovs[2], buf3, len3); // 1000 KB
여기서 중요 포인트:
- SETIOV()는 데이터를 복사하지 않고,
단지 iov_base, iov_len만 채워서
“이 주소에서 이 길이만큼” 이라는 정보만 만든다. - 실제 복사는 커널이 수행한다.
2.2 MsgSendv()

int MsgSendv(int coid,
const iov_t *siov, int sparts, // 보낼 IOV 배열 & 개수
iov_t *riov, int rparts); // 받을 IOV 배열 & 개수
- siov, sparts : gather (여러 버퍼 → 하나의 논리적 메시지)
- riov, rparts : scatter (하나의 논리적 reply → 여러 버퍼에 나눠 받기)
커널 동작:
- siov[0]의 (base,len) 데이터부터 서버로 복사
- 끝나면 siov[1]로 이어서 복사
- 그 다음 siov[2] …
→ 서버 입장에서는 “연속된 한 메시지”로 보인다.
장점:
- 클라이언트 측에서 큰 버퍼를 새로 만들 필요 없음
- memcpy()도 안 함 → 복사 한 번만 (client→server)
3. 단변형/벡터형 함수들
QNX 메시지 함수들은 거의 다 “벡터 버전”이 있음:


- MsgSend() ↔ MsgSendv()
- MsgReply() ↔ MsgReplyv()
- MsgReceive() ↔ MsgReceivev()
- MsgRead() ↔ MsgReadv()
- MsgWrite() ↔ MsgWritev()
그리고 한쪽만 벡터인 버전도 있음:
- MsgSendsv() : send는 single buffer, reply는 vector
- MsgSendvs() : send는 vector, reply는 single buffer
실제 설계 시:
- “헤더는 struct, payload는 큰 버퍼” → send쪽만 vector 형태가 많음
- reply는 보통 작은 struct 하나라서 단일 버퍼로도 충분
4. 대표 예: POSIX write()가 실제로 하는 일
C 라이브러리의 write()가 내부적으로 하는 일을 들여다보면 딱 이 구조:
ssize_t write(int fd, const void *buf, size_t nbytes)
{
io_write_t hdr; // 메시지 헤더 (타입, 길이 등)
struct iovec siov[2];
hdr.type = _IO_WRITE;
hdr.nbytes = nbytes;
// ... 기타 필드
SETIOV(&siov[0], &hdr, sizeof(hdr)); // 첫 번째 IOV: 헤더
SETIOV(&siov[1], buf, nbytes); // 두 번째 IOV: 실제 데이터
return MsgSendv(fd, siov, 2, NULL, 0);
}

- fd = 사실상 coid
- 서버 입장에서는 “[헤더][데이터]”가 한 덩어리로 온 것처럼 보임.
- 클라이언트 쪽에서는 데이터를 따로 복사하지 않고 그대로 사용.
5. 서버 쪽: MsgReceive vs MsgRead / MsgReadv
이제 서버가 이 데이터를 어떻게 받는지.
5.1 이론적으로는 MsgReceivev()


서버가 처음부터 “헤더 + payload 전체 길이”를 알고 있다면:
struct iovec riov[4]; // 헤더 + 여러 데이터 버퍼 등
SETIOV(&riov[0], header_buf, sizeof(header));
SETIOV(&riov[1], cache_buf1, 4096);
SETIOV(&riov[2], cache_buf2, 4096);
SETIOV(&riov[3], cache_buf3, 4096);
MsgReceivev(chid, riov, 4, &info);
이러면 커널이:
- 클라이언트 헤더 → header_buf
- 클라이언트 데이터 → cache_buf1, cache_buf2, cache_buf3…로 scatter
하지만 현실적으로 이건 잘 안 씀. 이유:
- MsgReceive()를 호출할 때는 아직 payload 길이를 모른다.
- 클라이언트가 write()로 몇 KB를 보낼지 서버가 미리 모를 수 없음.
- 1 byte ~ 수백 MB까지 가능
그래서 실제로는:
5.2 실제 패턴:

- MsgReceive()로 헤더만 먼저 받기
- 헤더를 보고 payload 길이를 확인
- 그 길이에 맞게 버퍼를 준비한 뒤 MsgRead/MsgReadv()로 나머지 읽기
예:
io_write_t hdr;
struct _msg_info info;
rcvid = MsgReceive(chid, &hdr, sizeof(hdr), &info);
// 여기까지 오면 hdr.nbytes를 알게 됨
// 예: 12 KB라면 3 × 4KB 캐시 버퍼 선택
struct iovec riov[3];
SETIOV(&riov[0], cache1, 4096);
SETIOV(&riov[1], cache2, 4096);
SETIOV(&riov[2], cache3, 4096);
// 이제 클라이언트 쪽 send 버퍼에서 “헤더 뒤부터” 읽기
int n = MsgReadv(rcvid, riov, 3, sizeof(hdr)); // offset = sizeof(hdr)

- rcvid : 어떤 클라이언트의 메시지인지 (그 클라이언트는 아직 reply-blocked 상태)
- offset : 클라이언트 send 버퍼에서 어디서부터 복사할지
- 0 ~ sizeof(hdr)-1 : 헤더 (이미 읽었으니 건너뜀)
- sizeof(hdr)부터 payload
중요 포인트
- MsgRead/MsgReadv는 stateless:
- 커널이 “어디까지 읽었다” 를 기억하지 않음
- 매번 offset을 명시해줘야 함
- 따라서 원하는 위치를 임의로 읽을 수 있음
(앞부분, 뒷부분, 중간 건너뛰기 등)

- 클라이언트는 MsgReply()/MsgError()가 호출될 때까지 계속 reply-blocked
→ 그 동안 서버는 MsgRead()/MsgReadv()를 여러 번 호출해서
같은 데이터를 여러 번 읽을 수도 있음.



6. MsgWrite / MsgWritev: 반대 방향
MsgRead/MsgReadv가 “클라이언트 → 서버” 데이터 복사라면,
- MsgWrite/MsgWritev는 “서버 → 클라이언트” 복사
MsgWrite(rcvid, buf, len, offset);
MsgWritev(rcvid, riov, parts, offset);
- offset : 클라이언트의 reply 버퍼에서 어디에 쓸지
- 하지만 실무에서는:
- 대부분 MsgReply() 또는 MsgReplyv() 한 번으로 reply 전체를 보냄
- MsgWrite()는 “조금씩 밀어넣고 나중에 reply로 마무리” 같은 특수 상황에만 사용
7. 복사 규칙 정리
- MsgSend(v) 호출:
- 클라이언트가 버퍼(또는 IOV)를 넘김
- 아직 실제 복사는 안 했다고 생각해도 됨 (개념상)
- MsgReceive(v) 호출 시:
- “전송된 총 길이” vs “서버가 제공한 수신 버퍼(들) 길이” 중 더 작은 만큼 복사
- 나머지는 클라이언트 쪽에 그대로 남아 있음
- 클라이언트는 계속 reply-blocked
- MsgRead/MsgReadv:
- 서버가 필요할 때, 원하는 offset부터 원하는 만큼 가져옴
- min(요청 길이, 남아있는 클라이언트 데이터 양)만큼 복사
- offset이 전체 길이보다 뒤라면 → 0 바이트 리턴 (에러 아님)
- MsgReply / MsgError:
- 호출되는 순간, 클라이언트는 unblock
- 이후에는 MsgRead()/MsgWrite()로 그 메시지에 접근 불가

8. 직관적인 한 줄 요약
- IOV + MsgSendv
→ “큰/여러 개의 버퍼를 복사 없이 묶어서 kernel에게 넘기고”
→ 커널이 복사하면서 알아서 한 덩어리 메시지로 조립해준다. - MsgRead/MsgReadv
→ 서버 입장에서 “클라이언트가 보낸 큰 메시지 중 필요한 부분을 필요할 때, 필요한 만큼만 가져오는 함수”.
'운영체제 > QNX' 카테고리의 다른 글
| QNX RTOS: 5-7. Server Designs (0) | 2025.12.02 |
|---|---|
| QNX RTOS: 5-6. Issues Related to Priorities (0) | 2025.12.02 |
| QNX RTOS: 5-4. How a Client Finds a Server (0) | 2025.12.02 |
| QNX RTOS: 5-3. Client Information Structure (0) | 2025.12.02 |
| QNX RTOS: 5-2. Pulses (0) | 2025.12.02 |