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하게는:

  1. 2.25 MB짜리 큰 버퍼를 malloc()으로 새로 만들고
  2. memcpy()로 세 버퍼를 거기에 이어붙인 다음
  3. 그 큰 버퍼를 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 → 여러 버퍼에 나눠 받기)

커널 동작:

  1. siov[0]의 (base,len) 데이터부터 서버로 복사
  2. 끝나면 siov[1]로 이어서 복사
  3. 그 다음 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 실제 패턴:

  1. MsgReceive()로 헤더만 먼저 받기
  2. 헤더를 보고 payload 길이를 확인
  3. 그 길이에 맞게 버퍼를 준비한 뒤 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. 복사 규칙 정리

  1. MsgSend(v) 호출:
    • 클라이언트가 버퍼(또는 IOV)를 넘김
    • 아직 실제 복사는 안 했다고 생각해도 됨 (개념상)
  2. MsgReceive(v) 호출 시:
    • “전송된 총 길이” vs “서버가 제공한 수신 버퍼(들) 길이” 중 더 작은 만큼 복사
    • 나머지는 클라이언트 쪽에 그대로 남아 있음
    • 클라이언트는 계속 reply-blocked
  3. MsgRead/MsgReadv:
    • 서버가 필요할 때, 원하는 offset부터 원하는 만큼 가져옴
    • min(요청 길이, 남아있는 클라이언트 데이터 양)만큼 복사
    • offset이 전체 길이보다 뒤라면 → 0 바이트 리턴 (에러 아님)
  4. 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