QNX RTOS: 5-4. How a Client Finds a Server
2025. 12. 2. 17:17ㆍ운영체제/QNX
1. 기본 문제: 클라이언트는 서버를 어떻게 찾나?
QNX 메시지 패싱에서 클라이언트가 MsgSend()를 하려면:
- coid( connection ID )가 필요하고
- coid를 만들려면 ConnectAttach()를 호출해야 하고
- ConnectAttach()를 하려면 서버의 pid(프로세스 ID)와 chid(채널 ID) 를 알아야 함.
실습에서는:
- 서버가 printf("pid=%d, chid=%d\n") 찍고
- 클라이언트가 그걸 커맨드라인 인자로 직접 입력했음.
→ 하지만 이건 “실습용”일 뿐, 실제 시스템에서 쓸 수 있는 구조는 아님.
그래서 QNX는 이름 기반(Name-based) 서버 찾기 방식을 제공함.
2. 두 가지 방식: Resource Manager vs. 단순 MsgReceive 서버
2.1 Resource Manager인 경우
- 서버가 resmgr_attach()로 이름을 등록:
resmgr_attach(..., "/dev/sound", ...);
- 클라이언트는 그냥:
int fd = open("/dev/sound", O_RDWR);
- QNX에서 file descriptor = 사실상 coid
→ read(fd, ...), write(fd, ...)는 내부적으로:- 메시지 포맷 만들고
- MsgSend(fd, ...) 호출하고
- MsgReply() 결과 처리하는, 10줄 내외짜리 래퍼 함수일 뿐.
즉, Resource Manager를 쓰면 open/read/write만으로 IPC가 다 되는 구조.
2.2 단순 MsgReceive 루프 서버인 경우 (우리가 지금 보고 있는 케이스)
Resource Manager처럼 거창하게 안 쓰고, 그냥:
while (1) {
rcvid = MsgReceive(chid, &msg, sizeof(msg), &info);
...
}
이런 서버라면?
- 서버는 name_attach()로 이름을 등록
- 클라이언트는 name_open()으로 그 이름을 찾아서 coid 얻기
3. name_attach() / name_open() 메커니즘
3.1 서버: name_attach()
name_attach_t *attach;
attach = name_attach(NULL, "my_server", 0);
if (attach == NULL) { /* error */ }
int chid = attach->chid; // MsgReceive할 때 사용할 채널 ID
- name_attach()가 내부적으로:
- 채널을 생성 (ChannelCreate() 호출)
- 그 채널에 이름을 pathname space에 등록
- name_attach_t*를 반환 (안에 chid 포함)
- 서버는 이후 MsgReceive()할 때:
MsgReceive(attach->chid, &msg, sizeof(msg), &info);
- 서버 종료 & 이름 제거:
name_detach(attach, 0); // 채널 파괴 + 이름 제거
3.2 클라이언트: name_open()
int coid = name_open("my_server", 0);
if (coid == -1) { /* error */ }
// 이후에는 그냥 MsgSend에 coid 사용
MsgSend(coid, &msg, sizeof(msg), &reply, sizeof(reply));
- name_open()이 내부적으로:
- 서버를 lookup해서
- ConnectAttach()를 호출
- coid(연결 ID)를 반환
- 클라이언트가 더 이상 사용 안 하면:
name_close(coid);
요약:
- 서버: name_attach() → MsgReceive(attach->chid, ...)
- 클라이언트: name_open() → MsgSend(coid, ...)
→ pid/chid를 직접 알 필요가 없음.
4. name_attach()가 내부에서 설정하는 Channel Flags (중요)
단순히 채널만 만드는 게 아니라, ChannelCreate()를 호출할 때 몇 가지 플래그를 자동으로 세팅합니다. 그래서 서버는 이런 Pulse들을 받을 수 있다는 걸 알고 있어야 함.
name_attach()가 설정하는 플래그:
- _NTO_CHF_DISCONNECT
- _NTO_CHF_COID_DISCONNECT
- _NTO_CHF_UNBLOCK
각각 의미는:
4.1 _NTO_CHF_DISCONNECT — “내 클라이언트가 죽으면 알려줘”
- 의미: 서버의 클라이언트가 사라질 때(프로세스 종료, name_close, ConnectDetach 등)
- 커널이 서버에게 pulse를 보냄
- pulse code: _PULSE_CODE_DISCONNECT
- pulse 안의 scoid로 어떤 클라이언트인지 알려줌
서버는 이걸 받아서:
- 클라이언트 리스트에서 해당 클라이언트 제거
- 할당된 버퍼/리소스 해제
- 마지막에 반드시 ConnectDetach(scoid) 호출해야 함 (매우 중요)
4.2 _NTO_CHF_COID_DISCONNECT — “내가 클라이언트로 붙어 있는 서버가 죽으면 알려줘”
- 여기서 이 프로세스는 서버이면서 동시에 다른 서버의 클라이언트일 수 있음.
- 이 플래그는:
- “내가 사용 중인 coid(=내가 다른 서버에 붙은 연결)가 죽을 때 pulse로 알려달라”는 의미
- 커널은 해당 프로세스에 pulse 전송:
- pulse code: _PULSE_CODE_COIDDEATH
- pulse의 value에 어느 coid/FD가 죽었는지 전달
예:
- 내 서버는 클라이언트들로부터도 메시지를 받고,
- 동시에 다른 IPC 서버에 붙어서 I/O를 요청할 수도 있음.
- 그 상대 서버가 죽거나 채널을 닫으면 → _PULSE_CODE_COIDDEATH 로 알려줌 → 정리 필요.
4.3 _NTO_CHF_UNBLOCK — “Reply 기다리는 클라이언트가 타임아웃/시그널로 Unblock 되면 알려줘”
- 상황:
- 클라이언트가 MsgSend() → 서버가 MsgReceive()로 받고 처리 중
- 클라이언트는 REPLY blocked 상태로 서버의 답장을 기다림
- 그 사이에 클라이언트가 타임아웃 / signal / 기타 이유로 unblock을 요청할 수 있음
- 이 플래그 설정 시:
- 그런 일이 발생하면 서버 채널로 pulse 전송
- pulse code: _PULSE_CODE_UNBLOCK
서버는 이걸 보고:
- “아, 이 클라이언트는 더 이상 reply를 기다리지 않겠구나”
- 해당 클라이언트에 대해 진행 중이던 작업을 중지하거나 정리할 수 있음.
5. 서버가 클라이언트를 “어떻게 추적”하는가? (Client Table & scoid)
서버는 보통 클라이언트별 상태를 관리하는 테이블/리스트를 갖습니다.
예시:
- 각 클라이언트가:
- 어떤 요청을 했는지
- 얼마나 많은 데이터를 요청했고, 지금까지 몇 바이트 처리했는지
- 어떤 하드웨어와 연결되어 있는지
- 어떤 이벤트에 대해 notify를 요청했는지
- 이런 정보를 client_list[] / linked list 등으로 관리.
5.1 클라이언트 ID로 scoid 사용
- 최초로 특정 클라이언트로부터 메시지를 받으면:
struct _msg_info info;
rcvid = MsgReceive(chid, &msg, sizeof(msg), &info);
if (is_new_client(info.scoid)) {
add_client_to_list(info.scoid, ...);
}
- _msg_info 안의 scoid는:
- “서버 입장에서 이 클라이언트를 식별하는 ID”
- 클라이언트가 살아 있는 동안에는 동일하게 유지됨.
5.2 클라이언트 종료 시: disconnect pulse 처리
- 클라이언트가 종료되거나 연결을 끊으면:
- 서버는 다음과 같은 pulse를 받게 됨:
rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
if (rcvid == 0) { // pulse
switch (msg.pulse.code) {
case _PULSE_CODE_DISCONNECT:
{
int dead_scoid = msg.pulse.scoid;
client_t *c = find_client(dead_scoid);
cleanup_client(c); // 버퍼, 하드웨어, pending 작업 정리
ConnectDetach(dead_scoid); // ★ 필수!
}
break;
...
}
}
핵심 포인트
- _PULSE_CODE_DISCONNECT 수신
- pulse 안의 scoid로 클라이언트 탐색
- 리소스 정리 + 무조건 ConnectDetach(scoid) 호출
(disconnect pulse를 요청했으면, detach는 서버 책임)
6. 전체 흐름 요약
- 서버 시작
- name_attach(NULL, "my_server", 0)
- 내부적으로 ChannelCreate() + 이름 등록 + _NTO_CHF_* 플래그 설정
- attach->chid로 MsgReceive() 루프 시작
- 클라이언트 시작
- coid = name_open("my_server", 0)
- MsgSend(coid, ...)로 서버와 동기 메시지 통신
- 서버 측 메시지 처리
- MsgReceive(chid, &msg, sizeof(msg), &info)
- rcvid > 0: 정상 메시지 → _msg_info로 scoid 확인 → client list 관리
- rcvid == 0: pulse
- _PULSE_CODE_DISCONNECT: 클라이언트 종료 → client list 정리 + ConnectDetach(scoid)
- _PULSE_CODE_UNBLOCK: REPLY 기다리던 클라이언트가 unblock → 작업 취소/정리
- _PULSE_CODE_COIDDEATH: 이 서버가 의존하던 다른 서버의 coid가 죽음 → 재연결 등 처리
- 종료
- 서버: name_detach(attach, 0)
- 클라이언트: name_close(coid)
'운영체제 > QNX' 카테고리의 다른 글
| QNX RTOS: 5-6. Issues Related to Priorities (0) | 2025.12.02 |
|---|---|
| QNX RTOS: 5-5. Multi-Part Messages (0) | 2025.12.02 |
| QNX RTOS: 5-3. Client Information Structure (0) | 2025.12.02 |
| QNX RTOS: 5-2. Pulses (0) | 2025.12.02 |
| QNX RTOS: 5-1. Message Passing (0) | 2025.11.27 |