Mục lục khóa họcMở
Chương 5 · Bài 18Học thử miễn phí
Advanced IPC Overview
Các cơ chế IPC của kernel (pipe, socket, message queue, shared memory...) chỉ chuyển byte từ nơi này sang nơi khác. Khi hệ thống lớn lên thành hàng chục process, nhiều ngôn ngữ lập trình, nhiều máy, thì người ta cần thêm những thứ mà kernel không cung cấp: định dạng dữ liệu chung, cách tìm thấy nhau, các mẫu giao tiếp như gọi hàm từ xa hay đăng ký nhận tin, và cam kết về chất lượng truyền. Bài này giới thiệu tầng middleware đó và bảy công nghệ phổ biến: D-Bus, ZeroMQ, gRPC, SOME/IP, DDS, Zenoh và iceoryx2. Bài là phần tổng quan nên chỉ nêu khái niệm, mô hình và chỗ dùng của từng công nghệ; ví dụ chạy thật trong bài dùng D-Bus vì đó là công nghệ có sẵn trên máy này.
Xem lại: các cơ chế IPC của kernel ở bài Linux IPC Overview, mục 3, cách chọn giữa chúng ở bài IPC Selection Guide, mục 2 (số đo độ trễ dùng để so sánh trong bài này), và UNIX domain socket ở bài IPC Socket.
Mục tiêu bài học
Sau bài học này, bạn sẽ có thể:
- Giải thích vì sao IPC của kernel chưa đủ cho hệ thống lớn và middleware bổ sung những gì.
- Mô tả các khái niệm nền: serialization, discovery, request/reply, publish/subscribe, QoS.
- Phân biệt ba nhóm IPC và nêu mô hình chính, use case của bảy công nghệ.
- Quan sát và đo chi phí của D-Bus so với UNIX socket thuần.
1. Vì sao cần IPC nâng cao
1.1. Những điều kernel không làm giúp
Giả sử hai process cần trao đổi một thông tin "vị trí của vật thể": tọa độ x, y, độ tin cậy, mốc thời gian. Với các cơ chế ở bài trước, kernel đã lo việc chuyển byte. Mọi thứ còn lại do lập trình viên tự giải quyết:
| Nhu cầu | Kernel cung cấp | Còn thiếu |
|---|---|---|
| Định dạng dữ liệu | Mảng byte thô | Ai quy định struct có bao nhiêu byte, thứ tự byte, padding? Thêm một trường mới thì bên cũ đọc ra sao? |
| Tìm đối tác | Tên file, PID, địa chỉ | Làm sao biết dịch vụ nào đang chạy, ở đâu, khi nó khởi động lại thì kết nối lại thế nào? |
| Mẫu giao tiếp | Đọc và ghi | Một bên gửi, nhiều bên nhận (publish/subscribe) hay gọi hàm và chờ kết quả (RPC) đều phải tự cài. |
| Hợp đồng và phiên bản | Không có | Mô tả giao diện bằng ngôn ngữ trung lập để C, Python, Rust cùng dùng, sinh mã tự động. |
| Chất lượng truyền | Gần như không | Cam kết "không mất tin", "tin cũ hơn 10 ms thì bỏ", "giữ 5 tin gần nhất". |
| Bảo mật, quan sát | Quyền file | Ai được gọi hàm nào, và công cụ để xem hệ thống đang nói chuyện gì. |
Middleware là phần mềm đứng giữa ứng dụng và kernel để lấp những chỗ trống đó. Nó vẫn chạy trên các cơ chế truyền của kernel (UNIX socket, TCP/UDP, shared memory), nhưng ứng dụng không còn phải làm việc trực tiếp với chúng.
1.2. Ba nhóm IPC
Có thể chia các cơ chế IPC thành ba nhóm theo mục tiêu thiết kế:
| Nhóm | Mục tiêu thiết kế | Công nghệ | Đặc điểm |
|---|---|---|---|
| 1. IPC truyền thống | Tối ưu và dùng rộng rãi | pipe, FIFO, message queue, shared memory, semaphore, signal, socket | Có sẵn trong kernel, không cài thêm gì, nhưng chỉ chuyển byte. |
| 2. IPC hiện đại cho kiến trúc hướng dịch vụ (service-oriented) và hệ thống phân tán | Giao diện có kiểu, gọi hàm từ xa, đăng ký sự kiện | D-Bus, gRPC, ZeroMQ, SOME/IP | Hoạt động giữa nhiều ngôn ngữ và nhiều máy. |
| 3. IPC hiệu năng cao cho multimedia, robotics và thời gian thực | Giảm sao chép, đảm bảo chất lượng truyền (QoS), độ trễ thấp và ổn định | iceoryx2, DDS, Zenoh | Dành cho dữ liệu liên tục như khung hình, dữ liệu cảm biến. |
Ranh giới giữa nhóm 2 và 3 không tuyệt đối: DDS và Zenoh cũng chạy được trên nhiều máy như nhóm 2, còn iceoryx2 chỉ phục vụ giao tiếp cùng máy.
2. Khái niệm nền tảng
2.1. Serialization và IDL
Serialization là chuyển dữ liệu trong bộ nhớ (struct, đối tượng) thành chuỗi byte để gửi đi, và deserialization là chiều ngược lại. Chép nguyên struct ra byte như ở các bài trước chạy tốt khi hai bên cùng một chương trình, nhưng sẽ hỏng khi hai bên biên dịch khác nhau (padding, endian, kích thước long) hoặc khác ngôn ngữ.
Các middleware giải quyết bằng một IDL (Interface Definition Language): file mô tả kiểu dữ liệu và giao diện bằng ngôn ngữ trung lập, từ đó công cụ sinh mã cho từng ngôn ngữ. Ví dụ gRPC dùng file .proto của Protocol Buffers, DDS dùng IDL của OMG, D-Bus mô tả kiểu bằng chuỗi signature (như s là string, a{sv} là từ điển). Chi phí là thời gian mã hóa và giải mã, nên với dữ liệu lớn như ảnh người ta thường để dữ liệu thô trong shared memory và chỉ serialize phần thông tin nhỏ đi kèm.
2.2. Discovery
Discovery là cách các bên tìm thấy nhau mà không cần biết trước địa chỉ cụ thể: D-Bus dùng tên dịch vụ do bus daemon quản lý (như vn.devlinux.Echo), DDS dùng giao thức tự quảng bá trên mạng, SOME/IP có SOME/IP-SD (Service Discovery). Nhờ vậy dịch vụ khởi động lại hay chuyển sang máy khác thì client không phải sửa cấu hình.
2.3. Mô hình giao tiếp
Hai mô hình chính của middleware:
| Mô hình | Cách hoạt động | Hợp với | Công nghệ | Mức gắn kết |
|---|---|---|---|---|
| Request/reply (RPC) | Client gọi một hàm của server như gọi hàm cục bộ và chờ kết quả | Thao tác có câu trả lời (đọc cấu hình, bật thiết bị) | gRPC, D-Bus, SOME/IP | Bên gọi và bên nhận gắn với nhau hơn |
| Publish/subscribe | Publisher phát dữ liệu lên một topic (chủ đề) mà không cần biết ai nghe; mọi subscriber đã đăng ký đều nhận | Dữ liệu cảm biến liên tục | DDS, Zenoh, iceoryx2, ZeroMQ (kiểu PUB/SUB) | Các bên độc lập, dễ thêm bớt người nghe |
Ngoài ra còn có streaming (một lời gọi nhưng dữ liệu chảy liên tục, như gRPC streaming) và query (hỏi dữ liệu đã lưu, như trong Zenoh).
2.4. QoS
QoS (Quality of Service) là các cam kết về cách truyền. Những thiết lập thường gặp:
| Thiết lập QoS | Ý nghĩa |
|---|---|
| Độ tin cậy (reliable hay best effort) | Reliable: mọi tin phải đến, chấp nhận chậm. Best effort: có thể mất để giữ độ trễ thấp. |
| Độ bền (durability) | Bên đến sau còn nhận được tin cũ không. |
| Lịch sử (history) | Giữ bao nhiêu tin gần nhất. |
| Thời hạn (deadline) | Dữ liệu phải đến trong bao lâu. |
DDS có hệ thống QoS đầy đủ nhất. Kernel không có khái niệm này nên ứng dụng dùng IPC truyền thống phải tự cài.
2.5. Transport
Cùng một middleware thường chạy được trên nhiều transport, tùy bên nhận ở đâu:
| Phạm vi | Transport | Ghi chú |
|---|---|---|
| Cùng máy | UNIX domain socket, shared memory | Shared memory khi cần tránh sao chép. |
| Khác máy | TCP, UDP (có thể multicast để một gói đến nhiều nơi), QUIC | |
| Mạng ô tô | Các cơ chế đảm bảo thời gian như TSN | Nằm ngoài phạm vi bài này. |
3. Nhóm 1: IPC truyền thống
Nhóm này đã được trình bày riêng từng cơ chế. Chúng là nền cho các nhóm sau, và với hai process đơn giản trên một máy thì dùng thẳng chúng thường là đủ và nhẹ nhất (xem bài IPC Selection Guide):
| Cơ chế | Bài chi tiết | Là nền cho |
|---|---|---|
| Pipe | IPC Pipe | Ghép lệnh trong shell, log giữa process cha con |
| FIFO | IPC FIFO | Luồng byte giữa process không liên quan |
| Message queue | IPC Message Queue | Lệnh có ưu tiên giữa các module |
| Shared memory | IPC Shared Memory | iceoryx2, và phần chuyển dữ liệu lớn của DDS |
| Semaphore | IPC Semaphore | Đồng bộ khi dùng shared memory |
| Signal | IPC Signal | Báo sự kiện vòng đời dịch vụ |
| Socket | IPC Socket | D-Bus (UNIX socket), ZeroMQ, gRPC, SOME/IP, DDS (qua mạng) |
4. Nhóm 2: hướng dịch vụ và phân tán
4.1. D-Bus
D-Bus là hệ thống bus thông điệp (message bus) phổ biến nhất cho các dịch vụ hệ thống trên Linux: systemd, NetworkManager, BlueZ (Bluetooth), các môi trường desktop đều dùng nó. Mô hình có ba khái niệm:
| Khái niệm | Ý nghĩa |
|---|---|
Bus daemon (dbus-daemon) | Process trung tâm. Mọi ứng dụng kết nối tới nó qua UNIX socket, và nó chuyển thông điệp tới đúng nơi. |
| System bus và session bus | System bus dành cho dịch vụ hệ thống, dùng chung. Session bus của từng người dùng đăng nhập. |
| Tên | Mỗi dịch vụ giữ một tên dạng vn.devlinux.Echo. |
| Đối tượng và interface | Đối tượng có đường dẫn như /vn/devlinux/Echo, mỗi đối tượng có interface gồm method, signal và property. |
| Method, signal, property | Method: gọi và nhận kết quả. Signal: sự kiện phát đi cho ai đăng ký. Property: thuộc tính. |
| Signature | Tham số và kết quả có kiểu rõ ràng nên bus kiểm tra được và công cụ như busctl in ra được. |
Hình trên vẽ mọi thành phần trong một máy Linux (D-Bus không đi qua mạng). Client không nói chuyện trực tiếp với dịch vụ mà gửi lời gọi tới bus daemon (request, nét đậm) và nhận trả lời (reply, nét nhạt); bus daemon chuyển lời gọi tới dịch vụ có đúng tên rồi chuyển câu trả lời về. Khi dịch vụ phát một signal (đường xanh liền), bus daemon chuyển tới mọi ứng dụng đã đăng ký, ở đây là Listener app.
Ưu điểm: có sẵn trên hầu hết bản phân phối, có công cụ quan sát (busctl, dbus-monitor), có kiểm soát truy cập, tên dịch vụ cho phép tự động khởi động dịch vụ khi có người gọi. Nhược điểm: mỗi thông điệp đi qua bus daemon nên chậm hơn socket thuần (đo ở Ví dụ 2), và không dành cho dữ liệu lớn hay tần suất cao. Vì vậy D-Bus là kênh điều khiển và cấu hình, không phải kênh dữ liệu.
4.1.1. Ví dụ 1
Để không đụng vào bus của phiên đăng nhập, ta tạo một bus riêng bằng dbus-daemon, chạy một dịch vụ mẫu (viết ở Ví dụ 2) rồi quan sát bằng các công cụ có sẵn. Ba câu lệnh đầu tạo bus và lưu địa chỉ vào biến môi trường mà thư viện D-Bus đọc:
$ OUT=$(dbus-daemon --session --fork --print-address=1 --print-pid=1 --nopidfile)
$ export DBUS_SESSION_BUS_ADDRESS=$(echo "$OUT" | sed -n 1p)
$ DBUS_PID=$(echo "$OUT" | sed -n 2p)
$ echo $DBUS_SESSION_BUS_ADDRESS
unix:abstract=/tmp/dbus-VnYWvknXUF,guid=145be20e1dc6f20027ad601d6aef1ec8Địa chỉ cho thấy bus dùng một UNIX socket (loại abstract). Tiếp theo chạy dịch vụ mẫu vn.devlinux.Echo (code ở Ví dụ 2) và liệt kê các tên trên bus:
$ ./build/dbus_echo_server &
ready
$ busctl --user list | grep -E "^(NAME|:1|vn.devlinux|org.freedesktop.DBus )"
NAME PID PROCESS USER CONNECTION UNIT SESSION DESCRIPTION
:1.0 697999 dbus_echo_serve aosp :1.0 session-354.scope 354 -
:1.1 698003 busctl aosp :1.1 session-354.scope 354 -
org.freedesktop.DBus - - - - - - -
vn.devlinux.Echo 697999 dbus_echo_serve aosp :1.0 session-354.scope 354 - Mỗi kết nối vào bus nhận một tên duy nhất dạng :1.N (:1.0 là dịch vụ của ta, :1.1 là chính busctl), và dịch vụ còn xin thêm tên dễ nhớ vn.devlinux.Echo. Bản thân bus daemon cũng là một dịch vụ tên org.freedesktop.DBus. Danh sách đầy đủ trên bus riêng còn dài hơn vì bus liệt kê cả các dịch vụ "có thể tự khởi động" (activatable) khai báo trong hệ thống, nên câu lệnh ở trên lọc bớt. Giờ gọi method Echo bằng dbus-send và xem chính bus daemon công bố giao diện nào:
$ dbus-send --session --dest=vn.devlinux.Echo --print-reply /vn/devlinux/Echo vn.devlinux.Echo.Echo string:"hello"
method return time=1794055881.006237 sender=:1.0 -> destination=:1.2 serial=5 reply_serial=2
string "hello"
$ busctl --user introspect org.freedesktop.DBus /org/freedesktop/DBus | head -6
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus interface - - -
.AddMatch method s - -
.GetAdtAuditSessionData method s ay -
.GetConnectionAppArmorSecurityContext method s s -
.GetConnectionCredentials method s a{sv} -Dòng method return là câu trả lời của dịch vụ, string "hello" là giá trị trả về có kiểu string. Lệnh introspect cho thấy tính tự mô tả của D-Bus: bus liệt kê method, kiểu tham số (cột SIGNATURE: s là string, ay là mảng byte, a{sv} là từ điển string tới variant) mà không cần tài liệu rời. Cuối cùng yêu cầu dịch vụ thoát (lưu ý dbus-send mặc định gửi signal, nên phải thêm --type=method_call để gửi lời gọi method) và tắt bus riêng:
$ dbus-send --session --type=method_call --dest=vn.devlinux.Echo /vn/devlinux/Echo vn.devlinux.Echo.Quit
$ kill $DBUS_PID4.1.2. Ví dụ 2
Dịch vụ vn.devlinux.Echo dùng thư viện C libdbus ở mức thấp nhất. Nó xin tên trên bus, rồi lặp nhận thông điệp: method Echo trả lại chuỗi nhận được, method Quit dừng dịch vụ. File examples/dbus_echo_server.c:
/**
* @file dbus_echo_server.c
* @brief D-Bus service vn.devlinux.Echo: method Echo returns its string, method Quit stops the service.
*/
#include <dbus/dbus.h>
#include <stdio.h>
/**
* @brief Claim the bus name and serve method calls until Quit arrives.
* @return 0 on success, 1 when the bus or the name is not available.
*/
int main(void)
{
DBusError err; /**< Error filled by libdbus. */
DBusConnection *c; /**< Connection to the session bus. */
DBusMessage *msg, *rep; /**< Incoming call and our reply. */
const char *s; /**< String argument of Echo. */
int run = 1; /**< Cleared by Quit. */
dbus_error_init(&err);
c = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (!c || dbus_bus_request_name(c, "vn.devlinux.Echo", 0, &err) != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
fprintf(stderr, "bus: %s\n", err.message ? err.message : "name taken");
return 1;
}
printf("ready\n");
fflush(stdout);
while (run && dbus_connection_read_write(c, -1))
while ((msg = dbus_connection_pop_message(c))) {
if (dbus_message_is_method_call(msg, "vn.devlinux.Echo", "Quit"))
run = 0;
else if (dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &s, DBUS_TYPE_INVALID)) {
rep = dbus_message_new_method_return(msg);
dbus_message_append_args(rep, DBUS_TYPE_STRING, &s, DBUS_TYPE_INVALID);
dbus_connection_send(c, rep, NULL);
dbus_message_unref(rep);
}
dbus_message_unref(msg);
}
return 0;
}Client gọi Echo 21000 lần (bỏ 1000 lần đầu), đo độ trễ khứ hồi từng lần và in p50, p99, giống cách đo ở bài IPC Selection Guide, rồi gọi Quit. File examples/dbus_echo_client.c:
/**
* @file dbus_echo_client.c
* @brief Call vn.devlinux.Echo.Echo many times and report the round-trip latency.
*/
#include <dbus/dbus.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 20000 /**< Measured calls. */
#define WARM 1000 /**< Calls discarded before measuring. */
/**
* @brief Compare two doubles for qsort().
* @param x First value.
* @param y Second value.
* @return Negative, zero or positive.
*/
static int cmp(const void *x, const void *y)
{
double a = *(const double *)x; /**< First value. */
double b = *(const double *)y; /**< Second value. */
return (a > b) - (a < b);
}
/**
* @brief Send one method call to the service and wait for the reply.
* @param c Bus connection.
* @param method Method name: Echo or Quit.
* @return 0 on success, -1 on error.
*/
static int call(DBusConnection *c, const char *method)
{
DBusMessage *m = dbus_message_new_method_call("vn.devlinux.Echo", "/vn/devlinux/Echo",
"vn.devlinux.Echo", method); /**< The call. */
DBusMessage *r; /**< The reply. */
const char *s = "x";/**< Argument of Echo. */
DBusError err; /**< Error filled by libdbus. */
dbus_error_init(&err);
if (method[0] == 'E')
dbus_message_append_args(m, DBUS_TYPE_STRING, &s, DBUS_TYPE_INVALID);
r = dbus_connection_send_with_reply_and_block(c, m, 5000, &err);
dbus_message_unref(m);
if (!r)
return -1;
dbus_message_unref(r);
return 0;
}
/**
* @brief Measure N round trips to the Echo service, then ask it to quit.
* @return 0 on success, 1 when the bus or the service is not available.
*/
int main(void)
{
static double us[N + WARM]; /**< Latency of each call in microseconds. */
struct timespec a, b; /**< Start and end of one call. */
DBusError err; /**< Error filled by libdbus. */
DBusConnection *c; /**< Connection to the session bus. */
dbus_error_init(&err);
c = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (!c)
return 1;
for (int i = 0; i < N + WARM; i++) {
clock_gettime(CLOCK_MONOTONIC, &a);
if (call(c, "Echo")) {
fprintf(stderr, "Echo failed\n");
return 1;
}
clock_gettime(CLOCK_MONOTONIC, &b);
us[i] = (b.tv_sec - a.tv_sec) * 1e6 + (b.tv_nsec - a.tv_nsec) / 1e3;
}
qsort(us + WARM, N, sizeof(double), cmp);
printf("dbus lat rtt_p50=%.1f us rtt_p99=%.1f us\n", us[WARM + N / 2], us[WARM + N * 99 / 100]);
call(c, "Quit");
return 0;
}Chạy make trong thư mục của bài. Makefile lấy cờ biên dịch của thư viện bằng pkg-config (cần cài gói libdbus-1-dev):
$ make
mkdir -p build
gcc -Wall -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include examples/dbus_echo_client.c -o build/dbus_echo_client -ldbus-1
gcc -Wall -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include examples/dbus_echo_server.c -o build/dbus_echo_server -ldbus-1Có ba process tham gia (client, bus daemon, dịch vụ), nên ta ghim chúng vào các nhân khác nhau để tách ảnh hưởng của việc đánh thức. Hàm rtt nhận nhân của bus daemon, của dịch vụ và của client, mỗi cấu hình chạy 3 lần:
$ rtt() {
OUT=$(taskset -c $1 dbus-daemon --session --fork --print-address=1 --print-pid=1 --nopidfile)
export DBUS_SESSION_BUS_ADDRESS=$(echo "$OUT" | sed -n 1p)
taskset -c $2 ./build/dbus_echo_server > /dev/null &
sleep 0.4
taskset -c $3 ./build/dbus_echo_client
wait
kill $(echo "$OUT" | sed -n 2p)
}
$ for cfg in "2 2 2" "3 3 2" "3 4 2"; do echo "rtt $cfg"; for i in 1 2 3; do rtt $cfg; done; done
rtt 2 2 2
dbus lat rtt_p50=69.5 us rtt_p99=269.6 us
dbus lat rtt_p50=70.1 us rtt_p99=313.6 us
dbus lat rtt_p50=70.2 us rtt_p99=320.3 us
rtt 3 3 2
dbus lat rtt_p50=153.4 us rtt_p99=617.6 us
dbus lat rtt_p50=153.8 us rtt_p99=725.9 us
dbus lat rtt_p50=154.1 us rtt_p99=699.4 us
rtt 3 4 2
dbus lat rtt_p50=226.0 us rtt_p99=964.6 us
dbus lat rtt_p50=225.8 us rtt_p99=927.2 us
dbus lat rtt_p50=225.8 us rtt_p99=870.4 usSo với UNIX stream socket thuần đo ở bài Selection Guide (cùng máy, hai process):
| Cấu hình | D-Bus (p50) | UNIX socket (p50) | Chênh lệch |
|---|---|---|---|
| Mọi process cùng một nhân | khoảng 70 us | 12,0 us | gấp khoảng 5,8 lần |
| Client một nhân, bus daemon và dịch vụ nhân khác | khoảng 154 us | 81,1 us (hai nhân) | gấp khoảng 1,9 lần |
| Ba process trên ba nhân | khoảng 226 us | không có số đo tương ứng |
- Mỗi lời gọi D-Bus đi bốn chặng: client tới bus daemon, bus daemon tới dịch vụ, rồi dịch vụ về bus daemon và về client, cộng thêm việc đóng gói thông điệp có kiểu và so khớp luật chuyển tiếp ở bus daemon. Khi các process nằm trên nhân khác nhau thì mỗi chặng thêm một lần đánh thức, nên độ trễ tăng dần theo số nhân (70, 154, 226 us).
- p99 là 270 đến 960 us, vẫn nhỏ hơn 1 ms. Với lệnh điều khiển (bật tắt thiết bị, đổi cấu hình, vài lần mỗi giây) đó là cái giá rất rẻ cho việc có tên dịch vụ, kiểm tra kiểu, kiểm soát truy cập và công cụ quan sát. Với dữ liệu cảm biến 1000 mẫu mỗi giây hay khung hình camera thì không nên đi qua D-Bus.
- Đây là phép đo cho
libdbusvớidbus-daemonmặc định trên máy này; các thư viện cấp cao hơn (GLib/GDBus, sd-bus) và bus daemon khác có thể cho con số khác, và con số tuyệt đối trên board embedded sẽ khác.
Bài chi tiết: IPC D-Bus.
4.2. ZeroMQ
ZeroMQ (ØMQ) là thư viện nhúng (embeddable) cung cấp socket "thông minh" mang sẵn các mẫu giao tiếp, không cần broker hay daemon. Mỗi socket ZeroMQ có một kiểu quyết định mẫu: REQ/REP (hỏi đáp), PUB/SUB (phát và đăng ký), PUSH/PULL (chia việc theo đường ống), DEALER/ROUTER (hỏi đáp bất đồng bộ nhiều bên). Thông điệp được gửi nguyên vẹn (không phải luồng byte như TCP) và thư viện tự kết nối lại khi mất kết nối.
Cùng một đoạn mã đổi được transport chỉ bằng chuỗi địa chỉ: inproc:// (giữa các thread trong process), ipc:// (UNIX socket), tcp://. ZeroMQ không quy định định dạng dữ liệu (bạn tự chọn Protobuf, JSON...) và không có discovery. Hợp với dịch vụ embedded cần nhẹ và linh hoạt về mẫu giao tiếp mà không muốn chạy thêm daemon.
Bài chi tiết: IPC ZeroMQ.
4.3. gRPC
gRPC là khung RPC của Google chạy trên HTTP/2, dùng Protocol Buffers làm IDL và cách mã hóa. Quy trình: viết giao diện trong file .proto (các service và message), công cụ protoc sinh stub cho client và skeleton cho server bằng C++, Python, Go, Java và nhiều ngôn ngữ khác, rồi lập trình viên chỉ gọi hàm như gọi hàm cục bộ. Hỗ trợ bốn kiểu lời gọi: một yêu cầu một trả lời (unary), server streaming, client streaming và streaming hai chiều; có deadline, hủy lời gọi, mã hóa TLS và xác thực.
Điểm mạnh là hệ sinh thái và tương thích đa ngôn ngữ, nên là lựa chọn mặc định cho API giữa các dịch vụ (service/API), đặc biệt giữa thiết bị và đám mây. Cái giá là phụ thuộc thư viện lớn (HTTP/2, Protobuf), tiêu tốn bộ nhớ và CPU hơn các lựa chọn trên, nên cần cân nhắc với MCU hay SoC rất nhỏ.
Bài chi tiết: IPC gRPC.
4.4. SOME/IP
SOME/IP (Scalable service-Oriented MiddlewarE over IP) là middleware hướng dịch vụ của ngành ô tô, được chuẩn hóa trong AUTOSAR, chạy trên Ethernet bằng UDP hoặc TCP. Một dịch vụ cung cấp ba loại thành phần: method (gọi và nhận kết quả, hoặc gọi không cần kết quả), event (phát khi có thay đổi, bên đăng ký nhận) và field (một giá trị có thể đọc, ghi và đăng ký theo dõi). Việc dịch vụ tìm thấy nhau do SOME/IP-SD (Service Discovery) đảm nhiệm: một ECU thông báo "tôi cung cấp dịch vụ này ở địa chỉ kia", ECU khác đăng ký sử dụng. Có triển khai mã nguồn mở là vsomeip.
Dùng khi các ECU trong xe cần giao tiếp kiểu dịch vụ trên mạng Ethernet với giao diện được chuẩn hóa giữa các nhà cung cấp.
Bài chi tiết: IPC SOME/IP.
5. Nhóm 3: hiệu năng cao
5.1. DDS
DDS (Data Distribution Service) là chuẩn của OMG cho giao tiếp publish/subscribe lấy dữ liệu làm trung tâm (data-centric). Ứng dụng khai báo topic có kiểu dữ liệu (mô tả bằng IDL), bên phát dùng DataWriter, bên nhận dùng DataReader, và hệ thống lo phần còn lại. Điểm nổi bật là bộ chính sách QoS rất đầy đủ (reliability, durability, history, deadline, lifespan...) và discovery tự động, không cần broker. Giao thức mạng bên dưới là RTPS (chạy trên UDP, thường dùng multicast để các bên tìm nhau).
Có nhiều triển khai (Fast DDS, Cyclone DDS, RTI Connext...). DDS là middleware mặc định của ROS 2 trong robotics, và được dùng trong các hệ thống ADAS và tự lái, nơi cần dữ liệu cảm biến liên tục với cam kết chất lượng rõ ràng.
Bài chi tiết: IPC DDS.
5.2. Zenoh
Zenoh là giao thức và middleware của Eclipse kết hợp publish/subscribe, query và lưu trữ trong một mô hình, thiết kế để chạy từ thiết bị nhỏ ở biên (edge) tới đám mây với chi phí giao thức thấp. Ứng dụng đặt dữ liệu vào các key (như sensor/room1/temp) và các bên đăng ký theo mẫu key hoặc truy vấn (query) giá trị. Chạy trên nhiều transport (TCP, UDP, QUIC...), có thể dùng thêm router để nối các mạng với nhau nhưng không bắt buộc.
Hợp với IoT và robotics nơi cùng một hệ thống trải từ cảm biến tới cloud và mạng không ổn định; cũng có lớp tích hợp cho ROS 2.
Bài chi tiết: IPC Zenoh.
5.3. iceoryx2
iceoryx2 là thư viện IPC zero-copy dựa trên shared memory, thuộc dự án Eclipse iceoryx và viết bằng Rust (có thể dùng từ C và C++). Bên phát ghi dữ liệu thẳng vào một vùng nhớ chia sẻ, bên nhận chỉ nhận con trỏ (hay chỉ số) tới vùng đó chứ không nhận bản sao. Vì vậy độ trễ gần như không đổi dù dữ liệu 1 KB hay 10 MB, đó là điều mà kênh truyền qua kernel không làm được (Ví dụ 2 ở bài IPC Selection Guide đã cho thấy thông lượng tăng vọt khi dùng shared memory cho khối lớn). Hỗ trợ mô hình publish/subscribe, và các mẫu khác như sự kiện (event) và request/response.
Cái giá của zero-copy: kiểu dữ liệu chia sẻ phải tự chứa (không có con trỏ trỏ ra ngoài vùng, vì địa chỉ ảo ở hai process khác nhau; xem bài IPC Shared Memory), và chỉ dùng giữa các process trên cùng một máy. Hợp với camera và AI: truyền khung hình lớn giữa tiến trình thu hình, tiền xử lý và suy luận.
Bài chi tiết: IPC iceoryx2.
5.4. Mailbox và RPMsg
Ba nhóm ở trên đều giả định các process chạy dưới cùng một kernel. Trên SoC có Cortex-A chạy Linux và Cortex-M chạy RTOS, hai bên không chung kernel và không chung scheduler, nên chỉ còn phần cứng làm cầu nối: mailbox (chuông báo bằng ngắt), hardware mutex và shared memory, và trên đó là RPMsg hoặc OpenAMP để có thông điệp có địa chỉ.
Bài chi tiết: IPC Mailbox.
6. So sánh và lựa chọn
6.1. Bảng so sánh
| Công nghệ | Mô hình chính | Transport thường dùng | Cần daemon hoặc broker? | Use case |
|---|---|---|---|---|
| iceoryx2 | Shared memory (pub/sub, event, request/response) | Shared memory | Không | Camera, AI |
| Zenoh | Pub/Sub, Query | TCP, UDP, QUIC, ... | Router tùy chọn | IoT, Robotics |
| DDS | Pub/Sub + QoS | RTPS trên UDP | Không | Robotics, ADAS |
| SOME/IP | RPC + Event | UDP, TCP trên Ethernet | Không (có SOME/IP-SD) | Automotive |
| ZeroMQ | Message patterns | inproc, ipc, tcp | Không | Embedded services |
| gRPC | RPC | HTTP/2 trên TCP | Không | Service/API |
| D-Bus | Message bus | UNIX socket | Có (bus daemon) | Linux system services |
6.2. Cách chọn
Bốn câu hỏi giúp thu hẹp lựa chọn:
| Câu hỏi | Trả lời | Công nghệ nên xét |
|---|---|---|
| Dữ liệu là gì? | Lệnh và cấu hình (ít, cần câu trả lời) | D-Bus, gRPC, SOME/IP |
| Dòng dữ liệu liên tục (cảm biến, khung hình) | DDS, Zenoh, iceoryx2 | |
| Bên kia ở đâu? | Cùng máy | D-Bus, iceoryx2, ZeroMQ ipc:// |
| Khác máy trong xe | SOME/IP, DDS | |
| Tới đám mây | gRPC, Zenoh | |
| Cần cam kết gì? | Yêu cầu QoS rõ ràng (độ tin cậy, thời hạn) | DDS |
| Chỉ cần độ trễ thấp và không sao chép | iceoryx2 | |
| Ràng buộc nền tảng? | MCU hoặc SoC nhỏ | ZeroMQ, Zenoh, hoặc chính các cơ chế kernel |
| Ô tô chuẩn AUTOSAR | SOME/IP, DDS | |
| Linux desktop và dịch vụ hệ thống | D-Bus |
Một hệ thống thật thường dùng nhiều lớp cùng lúc, mỗi đường liên lạc một công nghệ. Hình dưới đây là ví dụ hệ thống hỗ trợ lái xe (ADAS). Hình vẽ rõ ranh giới thiết bị (nét đứt) và phần cứng, hệ điều hành của từng vùng, để thấy process nào ở cùng một SoC và process nào ở thiết bị khác:
- Camera module là phần cứng, nối vào SoC bằng đường CSI (nét đứt xám). Đây là đường nối phần cứng, không phải IPC.
- Linux SoC chạy các process Capture, Perception, Planner và dịch vụ hệ thống Service manager, nên mới dùng được các cơ chế trong một máy.
- Vehicle ECU là thiết bị khác (MCU chạy RTOS), Cloud cũng là thiết bị khác. Hai đường qua ranh giới thiết bị dùng giao thức mạng.
- Đường một chiều (chấm xanh) là dòng dữ liệu đẩy đi; hai mũi tên ngược nhau là request (đậm) và reply (nhạt).
Từng đường liên lạc:
| Đường liên lạc | Công nghệ | Dữ liệu | Lý do |
|---|---|---|---|
| Capture tới Perception | iceoryx2 | Khung hình camera | Khối lớn, cùng máy, cần tránh sao chép. |
| Perception tới Planner | DDS | Danh sách vật thể nhận diện | Dòng dữ liệu liên tục, cần QoS rõ ràng và dễ thêm bên nhận. |
| Planner và Vehicle ECU | SOME/IP (request/reply) | Lệnh quỹ đạo | Giao tiếp dịch vụ giữa các bộ điều khiển qua Ethernet trong xe. |
| Service manager và Perception | D-Bus (request/reply) | Lệnh khởi động, dừng, cấu hình | Quản lý vòng đời và cấu hình dịch vụ hệ thống trên Linux. |
| Planner và Cloud | gRPC (request/reply) | Bản cập nhật bản đồ | API giữa xe và đám mây, đa ngôn ngữ, có TLS. |
Lưu ý quan trọng: bài này chỉ chạy thật với D-Bus, vì các thư viện còn lại (ZeroMQ, gRPC, DDS, Zenoh, vsomeip, iceoryx2) chưa được cài trên máy dùng để viết bài. Phần mô tả của chúng dựa trên tài liệu chính thức của từng dự án; trước khi chọn cho dự án thật, cần đọc tài liệu phiên bản đang dùng và tự đo trên phần cứng đích như cách đã làm ở bài IPC Selection Guide.
7. Kết luận
- IPC của kernel chỉ chuyển byte; middleware bổ sung định dạng dữ liệu (serialization, IDL), discovery, các mẫu giao tiếp (RPC, pub/sub) và QoS.
- Có ba nhóm: IPC truyền thống của kernel, IPC hướng dịch vụ và phân tán (D-Bus, gRPC, ZeroMQ, SOME/IP), IPC hiệu năng cao (iceoryx2, DDS, Zenoh).
- D-Bus dành cho điều khiển và cấu hình dịch vụ Linux; mỗi lời gọi chậm hơn UNIX socket thuần khoảng 2 đến 6 lần vì đi qua bus daemon, nên không dùng cho dữ liệu lớn hay tần suất cao.
- Chọn theo loại dữ liệu, vị trí của bên nhận, yêu cầu QoS và giới hạn tài nguyên của nền tảng; hệ thống thật thường kết hợp nhiều công nghệ.
