4.2 Unix domain socket 與共享記憶體#
本機 IPC 的兩個極端#
程序間通訊(IPC)的候選方案很多:管線(2.3 節)、訊號(2.2 節)、訊息佇列(System V, 1983 / POSIX mq)、共享記憶體(shm)、Unix domain socket(UDS,BSD 4.2, 1983)。性能與易用性的兩端是:
- UDS:語法幾乎與 TCP 相同,但走核心內部路徑,不走網路協定棧(無 TCP/IP header、無 checksum、無擁塞控制),本機延遲顯著更低。
- 共享記憶體:零拷貝的極致——兩個程序的虛擬位址映射到同一塊物理頁,讀寫就是記憶體存取,快到沒有「系統呼叫開銷」可言;代價是同步完全自理。
D-Bus(2003,Red Hat 為桌面程序通訊設計)底層就是 UDS;Docker daemon 與 containerd(2016 起)之間也是 UDS;PostgreSQL、Redis、Chrome 多程序架構全都用 UDS——因為它還有一個殺手級功能:可以傳遞 fd 本身(後述)。
Unix domain socket#
use std::io::{Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};
fn main() -> std::io::Result<()> {
let _ = std::fs::remove_file("/tmp/echo.sock"); // bind 前要清掉舊檔(檔案系統即命名空間)
let listener = UnixListener::bind("/tmp/echo.sock")?;
for stream in listener.incoming() {
let mut stream = stream?;
std::thread::spawn(move || {
let mut buf = [0u8; 1024];
loop {
match stream.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => { stream.write_all(&buf[..n]).ok(); }
}
}
});
}
Ok(())
}
UDS 位址就是檔案系統路徑(另一種是「抽象命名空間」,Linux 特有,名稱不落盤)。這帶來 Unix 權限模型的免費紅利:誰能連,由檔案權限決定——chmod 660 /tmp/echo.sock 就完成了 TCP 需要應用層認證才能做到的事。Docker 把 /var/run/docker.sock 設為 root 群組可寫,就是用檔案權限當存取控制。
傳遞 fd:SCM_RIGHTS#
UDS 獨有的魔法:透過輔助訊息(ancillary data)把一個 fd 從程序 A「傳」給程序 B——核心把 A 的開檔描述複製進 B 的 fd 表。這是 fd-passing 認證架構(1980 年代 acc 分散式系統思想)的基礎,也是 systemd socket activation、無 root 特權代理監聽埠的原理。
// 用 libc 傳遞 fd(核心機制 SCM_RIGHTS,Linux 2.4 起)
unsafe fn send_fd(sock_fd: i32, fd_to_send: i32) -> i32 {
let mut iov = libc::iovec {
iov_base: b"x".as_ptr() as *mut libc::c_void,
iov_len: 1,
};
let mut cmsg_buf = [0u8; 64];
let hdr_size = libc::CMSG_SPACE(sizeof_cmsghdr()) as usize;
let msg = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: cmsg_buf.as_mut_ptr() as *mut libc::c_void,
msg_controllen: hdr_size,
msg_flags: 0,
};
let cmsg = libc::CMSG_FIRSTHDR(&msg);
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(sizeof_cmsghdr()) as usize;
std::ptr::copy_nonoverlapping(
&fd_to_send as *const i32 as *const u8,
libc::CMSG_DATA(cmsg), std::mem::size_of::<i32>());
libc::sendmsg(sock_fd, &msg, 0)
}
(sizeof_cmsghdr 為 std::mem::size_of::<libc::cmsghdr>();接收端用 recvmsg 對稱取回。)這段 unsafe 很長,恰好示範 Rust 生態的分工:nix crate 提供 sendmsg/RecvMsg 的安全包裝,實務上直接用它,讓這類程式碼只出現在庫的底層。
共享記憶體#
POSIX shm(shm_open + mmap,POSIX.1b, 1993)與 System V shm(1970 年代 System III 遺產)並存;Linux 也有匿名 MAP_SHARED|MAP_ANONYMOUS。流程:
shm_open("/demo", O_CREAT|O_RDWR, 0666)→ 在/dev/shm/demo建立物件,得到 fdftruncate(fd, size)設定大小mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)→ 各自程序拿到指標
use std::sync::atomic::{AtomicU64, Ordering};
const SIZE: usize = 4096;
fn main() -> std::io::Result<()> {
let name = b"/rust_demo_shm\0";
let fd = unsafe {
libc::shm_open(name.as_ptr() as *const _, libc::O_CREAT | libc::O_RDWR, 0o666)
};
assert!(fd >= 0, "{}", std::io::Error::last_os_error());
unsafe { libc::ftruncate(fd, SIZE as i64) };
let ptr = unsafe {
libc::mmap(std::ptr::null_mut(), SIZE,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED, fd, 0)
};
assert_ne!(ptr, libc::MAP_FAILED);
// 把共享頁開頭當成 AtomicU64 計數器
let counter = unsafe { &*(ptr as *const AtomicU64) };
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("writer") => loop {
let v = counter.fetch_add(1, Ordering::SeqCst);
println!("wrote {v}");
std::thread::sleep(std::time::Duration::from_secs(1));
},
_ => loop {
println!("read = {}", counter.load(Ordering::SeqCst));
std::thread::sleep(std::time::Duration::from_secs(1));
},
}
}
執行:
$ cargo run -- reader # 終端 1
$ cargo run -- writer # 終端 2,reader 立即看到數字
共享記憶體的正確性:Rust 型別系統的角色#
共享記憶體最恐怖之處是兩個程序、同一塊記憶體、快取一致性靠你自己的同步。經典錯誤:
struct shared { int ready; char data[100]; };
// writer: 把 data 寫好 → ready = 1
// reader: 看到 ready == 1 → 讀 data
在多核 CPU 上這段是未定義的:compiler 與 CPU 都可能重排(store buffer、寫合併),reader 可能看到 ready==1 但 data 還是舊的。需要 memory barrier。Rust 的答案仍是顯式原子:把 ready 定義為 AtomicU64,用 Release(writer)/Acquire(reader)配對建立 happens-before 邊緣——與 3.1 節執行緒間的規則完全一致,只是這裡跨的是程序。而 &*(ptr as *const AtomicU64) 這個 unsafe 轉換的安全性依賴「對齊、初始化、無 aliasing 可變引用」三個不變量,正是應該集中註解的 unsafe 邊界(1.3 節)。
共享頁裡放 Mutex(跨程序)則需要 pthread_mutexattr_setpshared(PTHREAD_PROCESS_SHARED)——Linux futex 機制(2003 年核心 2.6 引入)在程序間同樣可用。
何時用哪個#
| 需求 | 選擇 |
|---|---|
| 程序間請求/回應、權限控制 | UDS(簡單、安全) |
| 大量資料高速傳輸(影音幀、行情) | shm + 原子/鎖同步 |
| 傳遞開啟的資源 | UDS + SCM_RIGHTS |
| 單向資料流 | 管線即可 |
習題#
- 給 UDS 伺服器加上
chmod權限控制,實驗不同使用者連線行為。 - 用
nixcrate 重寫 fd-passing,比較 unsafe 行數。 - 在 shm 範例中把
Ordering::SeqCst降級為Relaxed,並說明(或用計數器實驗)會看到什麼錯誤現象。