프로세스 간 통신 (IPC)
std/ipc/는 동일 프로세스 내의 다른 스레드가 아니라 다른 프로세스와
통신하는 것을 다룹니다 — 모두 프로세스 내부용인 Channel/MpscQueue/
LFQueue는 동기화를 참고합니다.
#include <std/ipc/pipe.h>
#include <std/ipc/uds.h>
// or via:
#include <std/prelude.h>
파이프 — 익명 파이프
가장 단순한 형태의 호스트 IPC: 읽기 끝과 쓰기 끝을 가진 단방향 바이트 스트림으로, 가장 흔히는 부모/자식 프로세스 간 통신에 쓰입니다 — spawn된 자식 프로세스가 파이프의 fd/핸들을 상속받거나, 자식의 stdin/stdout에 연결됩니다(그 연결 작업 자체는 이 모듈의 범위 밖이며, 이 모듈은 파이프를 생성하고 조작하는 것만 담당합니다).
핸들은 플랫폼 전반에 걸쳐 균일하게 long long입니다: POSIX 파이프 fd는
그대로 들어맞고, Windows 파이프 HANDLE(포인터 크기의 값)은
std/thread.h의 OS 스레드 핸들이 이미 사용하는 것과 동일한
포인터-정수 변환 관례를 통해 저장됩니다. 두 개의 백엔드가 있으며 선택
방식은 std/sched/reactor.h의 것과 동일합니다: pipe_posix.sc(macOS,
iOS, Linux, Android, FreeBSD — 이들 모두에서 pipe()는 동일함)와
pipe_win32.sc(CreatePipe/ReadFile/WriteFile/CloseHandle).
구조체
struct Pipe {
long long read_fd;
long long write_fd;
int init(); // creates the pipe in place; 0 on success, -1 on failure
// Returns bytes read (0 = write end closed and pipe drained: EOF), -1 on error.
long long read([]unsigned char buf);
// Returns bytes written, or -1 on error.
long long write([]const unsigned char buf);
// Non-blocking mode (pairs with std::Reactor — await_fd(fd, SCHED_READ/WRITE)).
int set_read_nonblocking();
int set_write_nonblocking();
// Closing the write end wakes a reader blocked in read() (or awaiting
// SCHED_READ) with EOF once the pipe drains. Safe to call each end's
// close exactly once — closing twice is undefined, the same as any
// double-close(2).
int close_read();
int close_write();
};
Pipe는 Stream(Reader + Writer: read는 읽기
끝에서 가져오고 write는 쓰기 끝에 넣음)이므로 read_exact, write_all,
copy_stream과 BufReader / BufWriter가
그대로 동작합니다.
예제
#include <std/ipc/pipe.h>
#include <std/io.h>
int main() {
struct Pipe p;
if (p.init() != 0) { std::println("pipe init failed"); return 1; }
unsigned char msg[5] = { 'h', 'e', 'l', 'l', 'o' };
p.write(msg[0..5UL]);
unsigned char buf[5];
long long n = p.read(buf[0..5UL]);
std::print("read "); std::print_int(n); std::println(" bytes"); // read 5 bytes
p.close_read();
p.close_write();
return (n == 5LL && buf[4] == 'o') ? 0 : 1;
}
TIP
이 예제는 자체 완결적으로 만들기 위해 단일 프로세스/스레드 내에서
쓰고 읽습니다 — 실제 사용 사례는 fork()를 거쳐 공유되는 파이프로,
부모와 자식이 각자 사용하지 않는 끝을 닫고 나머지 한쪽을 통해
통신합니다.
UDS — 유닉스 도메인 소켓
관련 없는 프로세스 간의 이름 있는, 주소 지정 가능한 IPC입니다(이미 fd를
가지고 있는 것 — 보통은 fork()된 자식 — 사이에서만 동작하는 Pipe의
익명 파이프와 다릅니다): 파일 시스템 경로를 아는 프로세스라면 어느
쪽이든 연결할 수 있으며, 이는 TCP 포트를 아는 프로세스가 네트워크를
통해 연결할 수 있는 것과 마찬가지입니다. std/sched/io_nb.h의 tcp_*_nb
함수와 의도적으로 동일한, 논블로킹이며 std::Reactor와 짝지을 수 있는
형태입니다 — 태스크가 이 함수들 중 하나를 호출하면 프로그램 전체를
블로킹하는 대신 곧바로 EAGAIN/EWOULDBLOCK을 받고,
TaskScheduler::await_fd(fd, SCHED_READ/SCHED_WRITE)를 통해 준비
상태를 기다립니다.
Windows 백엔드 없음
Windows에는 이 프로젝트가 지원하는 버전 전반에 걸쳐 보편적으로
적용되는, 전통적인 BSD 소켓 의미의 AF_UNIX 도메인 소켓에 대응하는
것이 없습니다. 그래서 uds_win32.sc는 존재하지 않습니다 — 네임드 파이프
(CreateNamedPipe/ConnectNamedPipe, 완전히 다른 형태의 API)가
Windows의 네이티브 대응물이며, 이 함수 시그니처들에 끼워 맞추기보다는
별도의 모듈이 필요할 것입니다.
두 개의 백엔드: uds_bsd.sc(macOS, iOS, FreeBSD — struct sockaddr_un에
1바이트 sun_len 필드가 앞에 있음)와 uds_linux.sc(Linux, Android —
sun_len 없이 2바이트 sun_family만 있음).
API
// 'path'에 대해 socket()+bind()+listen()을 수행, 논블로킹. 리스닝 fd를
// 반환하거나, 실패 시 -1 (정리되지 않은 이전 실행에서 남은 소켓 파일로
// 'path'가 이미 존재하면 EADDRINUSE — 그럴 가능성이 있으면 먼저
// uds_unlink를 호출).
int uds_listen_nb(const &char path);
// 논블로킹 accept(): 연결된(그 자체로 논블로킹인) 클라이언트 fd, 또는
// 아직 대기 중인 것이 없으면 -1/EAGAIN — await_fd(listenfd, SCHED_READ) 후
// 재시도.
int uds_accept_nb(int listenfd);
// 'path'에 대해 socket()+connect()를 수행, 논블로킹: 핸드셰이크를
// 기다리지 않고 즉시 EINPROGRESS와 함께 반환 — await_fd(fd,
// SCHED_WRITE)를 사용하고, fd가 쓰기 가능해지는 것을 "연결 완료"로
// 취급한다.
int uds_connect_nb(const &char path);
// 'path'에 있는 오래된 소켓 파일을 제거한다 — 정리되지 않고 종료된
// 이전 실행의 파일 시스템 경로가 여전히 존재하면, TCP 포트는 OS가
// 단순히 반환하는 것과 달리 bind()가 EADDRINUSE로 실패한다. 성공 시
// 0("경로가 존재하지 않았음"도 포함), 실제 실패 시 -1.
int uds_unlink(const &char path);
예제
#include <std/ipc/uds.h>
#include <std/io.h>
extern int write(int fd, const void* buf, unsigned long count);
extern long long read(int fd, void* buf, unsigned long count);
extern int close(int fd);
// Typed wrappers over C's untyped buffers: references in, raw pointers out.
long long send_text(int fd, const &char text, unsigned long n) { return (long long)write(fd, text, n); }
long long recv_text(int fd, &char out, unsigned long n) { return read(fd, out, n); }
int main() {
const &char path = "/tmp/example.sock";
std::uds_unlink(path);
int listenFd = std::uds_listen_nb(path);
int clientFd = std::uds_connect_nb(path);
// Non-blocking accept: retry until the pending connection shows up.
// A real program would await_fd(listenFd, SCHED_READ) via
// TaskScheduler/Reactor instead of busy-polling like this.
int serverFd = -1;
while (serverFd < 0) {
serverFd = std::uds_accept_nb(listenFd);
}
send_text(clientFd, "hi from client", 14UL);
char buf[15];
buf[14] = (char)0;
recv_text(serverFd, &buf[0], 14UL);
std::println(&buf[0]); // hi from client
close(clientFd); close(serverFd); close(listenFd);
std::uds_unlink(path);
return 0;
}