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ầuKernel cung cấpCòn thiếu
Định dạng dữ liệuMả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ácTê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à ghiMộ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ảnKhô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ềnGần như khôngCam 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átQuyền fileAi đượ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.

advanced_ipc_overview-hinh-01

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ế:

advanced_ipc_overview-hinh-02
NhómMục tiêu thiết kếCông nghệĐặc điểm
1. IPC truyền thốngTối ưu và dùng rộng rãipipe, FIFO, message queue, shared memory, semaphore, signal, socketCó 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ánGiao diện có kiểu, gọi hàm từ xa, đăng ký sự kiệnD-Bus, gRPC, ZeroMQ, SOME/IPHoạ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ựcGiảm sao chép, đảm bảo chất lượng truyền (QoS), độ trễ thấp và ổn địnhiceoryx2, DDS, ZenohDà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:

advanced_ipc_overview-hinh-03
Mô hìnhCách hoạt độngHợp vớiCô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/IPBên gọi và bên nhận gắn với nhau hơn
Publish/subscribePublisher 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ậnDữ liệu cảm biến liên tụcDDS, 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 viTransportGhi chú
Cùng máyUNIX domain socket, shared memoryShared memory khi cần tránh sao chép.
Khác máyTCP, 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ư TSNNằ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ếtLà nền cho
PipeIPC PipeGhép lệnh trong shell, log giữa process cha con
FIFOIPC FIFOLuồng byte giữa process không liên quan
Message queueIPC Message QueueLệnh có ưu tiên giữa các module
Shared memoryIPC Shared Memoryiceoryx2, và phần chuyển dữ liệu lớn của DDS
SemaphoreIPC SemaphoreĐồng bộ khi dùng shared memory
SignalIPC SignalBáo sự kiện vòng đời dịch vụ
SocketIPC SocketD-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 busSystem 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ênMỗ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, propertyMethod: gọi và nhận kết quả. Signal: sự kiện phát đi cho ai đăng ký. Property: thuộc tính.
SignatureTham 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.
advanced_ipc_overview-hinh-04

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_PID

4.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-1

Có 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 us

So với UNIX stream socket thuần đo ở bài Selection Guide (cùng máy, hai process):

Cấu hìnhD-Bus (p50)UNIX socket (p50)Chênh lệch
Mọi process cùng một nhânkhoảng 70 us12,0 usgấp khoảng 5,8 lần
Client một nhân, bus daemon và dịch vụ nhân kháckhoảng 154 us81,1 us (hai nhân)gấp khoảng 1,9 lần
Ba process trên ba nhânkhoảng 226 uskhô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 libdbus với dbus-daemon mặ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ínhTransport thường dùngCần daemon hoặc broker?Use case
iceoryx2Shared memory (pub/sub, event, request/response)Shared memoryKhôngCamera, AI
ZenohPub/Sub, QueryTCP, UDP, QUIC, ...Router tùy chọnIoT, Robotics
DDSPub/Sub + QoSRTPS trên UDPKhôngRobotics, ADAS
SOME/IPRPC + EventUDP, TCP trên EthernetKhông (có SOME/IP-SD)Automotive
ZeroMQMessage patternsinproc, ipc, tcpKhôngEmbedded services
gRPCRPCHTTP/2 trên TCPKhôngService/API
D-BusMessage busUNIX socketCó (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ỏiTrả lờiCô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áyD-Bus, iceoryx2, ZeroMQ ipc://
Khác máy trong xeSOME/IP, DDS
Tới đám mâygRPC, 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épiceoryx2
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 AUTOSARSOME/IP, DDS
Linux desktop và dịch vụ hệ thốngD-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:

advanced_ipc_overview-hinh-05
  • 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ạcCông nghệDữ liệuLý do
Capture tới Perceptioniceoryx2Khung hình cameraKhối lớn, cùng máy, cần tránh sao chép.
Perception tới PlannerDDSDanh sách vật thể nhận diệnDò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 ECUSOME/IP (request/reply)Lệnh quỹ đạoGiao tiếp dịch vụ giữa các bộ điều khiển qua Ethernet trong xe.
Service manager và PerceptionD-Bus (request/reply)Lệnh khởi động, dừng, cấu hìnhQuản lý vòng đời và cấu hình dịch vụ hệ thống trên Linux.
Planner và CloudgRPC (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ệ.
Học thử tiếp: Bài 30 – Linux GPIO Legacy: Điều khiển LED bằng ioremap