CHƯƠNG 05 · IPC · ~110 phút

Inter-Process
Communication

Process tách biệt nhau về memory — vậy chúng "nói chuyện" thế nào? Đây là chương về 6 cơ chế IPC: pipe, FIFO, shared memory, message queue, signal, socket. Hiểu chúng, bạn nắm cách ls | grep hoạt động, vì sao Ctrl+C kill được process, và Docker container giao tiếp ra sao.

5.1 Tại sao cần IPC?

Process A có biến x = 42. Process B có muốn đọc x không? Không thể truy cập trực tiếp — virtual memory cô lập, mỗi process có address space riêng. Address 0x7fff... trong A và B trỏ vào page khác nhau.

Vậy cần cơ chế kernel-mediated để 2 process trao đổi data. Đó là IPC.

6 cơ chế IPC chính

  1. Pipe — anonymous, parent-child
  2. FIFO — named pipe, processes bất kỳ qua filesystem
  3. Shared Memory — nhanh nhất, cần đồng bộ thủ công
  4. Message Queue — kernel queues messages
  5. Signal — async notification (vd Ctrl+C)
  6. Socket — Unix socket hoặc TCP/UDP — phổ biến nhất hiện nay

5.2 Pipe (anonymous) — | của shell

Pipe là kênh đơn hướng (one-way): 1 process ghi, 1 process đọc. Tạo bằng syscall pipe() trả về 2 file descriptor. Chỉ dùng được giữa parent-child (vì cần kế thừa fd qua fork).

Hoạt động trong shell

ls -l | grep .txt
# 1. Shell tạo pipe → fds: [read_end, write_end]
# 2. Shell fork ra ls process — child kế thừa pipe fds
#    + redirect stdout (fd=1) → write_end của pipe
#    + đóng read_end (không cần)
#    + exec ls
# 3. Shell fork ra grep process — child kế thừa pipe fds
#    + redirect stdin (fd=0) → read_end của pipe
#    + đóng write_end
#    + exec grep
# 4. Output của ls đi qua kernel buffer của pipe vào input của grep

Code C — pipe đơn giản

#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
    int fd[2];
    pipe(fd);  // fd[0] = read, fd[1] = write

    pid_t pid = fork();
    if (pid == 0) {
        // Child: ghi vào pipe
        close(fd[0]);  // không đọc
        const char *msg = "Hello from child\n";
        write(fd[1], msg, strlen(msg));
        close(fd[1]);
    } else {
        // Parent: đọc từ pipe
        close(fd[1]);  // không ghi
        char buf[100];
        ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("Parent received: %s", buf);
        close(fd[0]);
    }
    return 0;
}

Đặc điểm

  • One-way: muốn 2-way phải tạo 2 pipe
  • Parent-child only: 2 process không liên quan không dùng được
  • FIFO ordering: data đọc theo đúng thứ tự ghi
  • Buffer kernel: pipe có buffer (~64KB Linux). Buffer đầy → write block. Buffer rỗng → read block.
  • Atomicity: write < PIPE_BUF (4KB) là atomic — không bị xen kẽ

5.3 Named Pipe (FIFO)

Giống pipe nhưng có "tên" trong filesystem → process bất kỳ có thể mở.

mkfifo /tmp/mypipe       # tạo named pipe
ls -l /tmp/mypipe        # prw-r--r-- → 'p' = pipe

# Terminal 1: writer
echo "Hello from writer" > /tmp/mypipe

# Terminal 2: reader (block đến khi có ai write)
cat /tmp/mypipe
# Output: Hello from writer

FIFO vẫn đơn hướng. Để giao tiếp 2 chiều cần 2 FIFO.

5.4 Shared Memory — nhanh nhất

Kernel cấp 1 vùng memory mà nhiều process có thể map vào address space của mình. Sau đó đọc/ghi như memory bình thường — không qua syscall mỗi lần.

POSIX shm_open + mmap

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
    int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
    ftruncate(fd, 4096);

    char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, 0);
    // ptr giờ là 1 vùng 4KB shared với mọi process mở "/myshm"

    sprintf(ptr, "Hello shared!");
    // ... process khác đọc cùng vùng đó
}

Đặc điểm

  • Cực nhanh: không có overhead syscall mỗi lần đọc/ghi (sau khi map)
  • 2-way, multi-process
  • Phải tự đồng bộ: 2 process cùng ghi → race condition. Cần mutex/semaphore (chương 6).
  • Use case: video streaming, audio buffer, database (PostgreSQL shared_buffers)

5.5 Message Queue

Kernel quản lý 1 queue chứa messages có structure (type, payload). Process gửi message vào queue, process khác lấy theo FIFO hoặc theo type filter.

#include <mqueue.h>
#include <fcntl.h>

// Sender
mqd_t mq = mq_open("/myqueue", O_CREAT | O_WRONLY, 0644, NULL);
mq_send(mq, "Hello", 5, 0);
mq_close(mq);

// Receiver
mqd_t mq = mq_open("/myqueue", O_RDONLY);
char buf[256];
mq_receive(mq, buf, sizeof(buf), NULL);

So với Pipe

  • Pipe = stream byte không cấu trúc. Message queue = unit có ranh giới.
  • Message queue có priority.
  • Cấu trúc hơn nhưng có overhead hơn pipe.

Trong production, ít ai dùng SysV/POSIX message queue trực tiếp. Thay vào đó dùng RabbitMQ, Kafka, Redis Streams — same concept nhưng phân tán qua network.

5.6 Signal — async notification

Signal là cách "nhanh" để gửi notification tới process — không có data đi kèm, chỉ là 1 con số (signal number). Process có thể: ignore, default action, hoặc cài handler tự xử lý.

Common signals

SignalSốÝ nghĩaDefault action
SIGTERM15Yêu cầu kết thúc lịch sự (terminate)Terminate
SIGKILL9Force kill, KHÔNG bắt đượcTerminate
SIGINT2Ctrl+C trên terminalTerminate
SIGSTOP19Dừng (Ctrl+Z), KHÔNG bắt đượcStop
SIGCONT18Tiếp tục sau SIGSTOPContinue
SIGCHLD17Child kết thúc → parent nhậnIgnore
SIGSEGV11Segmentation faultTerminate + core dump
SIGPIPE13Ghi pipe mà reader đã chếtTerminate
SIGHUP1Terminal đóng (hangup); thường dùng "reload config"Terminate
SIGUSR1/SIGUSR210/12User definedTerminate

Gửi và bắt signal

kill -TERM 1234     # gửi SIGTERM
kill -9 1234        # SIGKILL (force)
kill -USR1 1234     # custom signal app handle
#include <signal.h>
#include <stdio.h>

void handler(int sig) {
    printf("Received signal %d\n", sig);
    // Cleanup, save state, ...
}

int main() {
    signal(SIGTERM, handler);
    signal(SIGINT, handler);
    while (1) pause();  // đợi signal
}

SIGKILL vs SIGTERM — phải biết

  • SIGTERM: app có thể bắt + cleanup (đóng file, flush buffer...). Phải dùng cái này trước.
  • SIGKILL: kernel kill ngay, app không kịp làm gì. Mất data có thể.
  • Best practice: gửi SIGTERM, đợi vài giây, nếu chưa chết thì SIGKILL. Đây là cách systemctl stop, kubectl delete pod hoạt động.

Ctrl+C kill được vì sao?

Khi bạn gõ Ctrl+C trên terminal, terminal driver gửi SIGINT đến foreground process group. App nhận, chạy default action (terminate). Nếu app cài handler, có thể intercept.

5.7 Socket — IPC phổ biến nhất hiện nay

Socket ban đầu thiết kế cho network (TCP/UDP) nhưng cũng dùng được local. Đây là cơ chế IPC phổ biến nhất vì cùng API hoạt động cho cả local lẫn remote.

Unix Domain Socket vs TCP Socket

Unix Domain Socket (AF_UNIX)

  • Local only — qua filesystem path (vd /var/run/docker.sock)
  • Nhanh hơn TCP loopback (không qua network stack)
  • Có thể truyền file descriptor giữa các process!
  • Có permission như file thường (chmod, chown)

TCP Socket (AF_INET)

  • Local hoặc remote
  • Cần host:port
  • Có cả những network feature: TCP retransmit, ordering...
  • Dùng cho microservices, web

Ví dụ Unix domain socket trong Node.js

// Server
const net = require('net');
const server = net.createServer(socket => {
  socket.on('data', d => socket.write('Echo: ' + d));
});
server.listen('/tmp/myapp.sock');

// Client
const client = net.connect('/tmp/myapp.sock');
client.write('Hi');
client.on('data', d => console.log(d.toString()));

Ai dùng Unix domain socket?

  • Docker: /var/run/docker.sock để CLI nói chuyện với daemon
  • X11: /tmp/.X11-unix/X0
  • PostgreSQL: /var/run/postgresql/.s.PGSQL.5432
  • nginx ↔ PHP-FPM: thường qua Unix socket cho nhanh

5.8 IPC trong Node.js

Khi fork Node child, có sẵn IPC channel (thực ra là pipe + protocol JSON).

// parent.js
const { fork } = require('child_process');
const child = fork('./worker.js');

child.send({ task: 'compute', n: 100 });

child.on('message', (msg) => {
  console.log('From worker:', msg);
});

child.on('exit', (code) => {
  console.log('Worker exited:', code);
});
// worker.js
process.on('message', (msg) => {
  if (msg.task === 'compute') {
    let sum = 0;
    for (let i = 1; i <= msg.n; i++) sum += i;
    process.send({ result: sum });
  }
});

Đây là cách cluster module hoạt động — master process talk với worker qua IPC.

5.9 So sánh các cơ chế IPC

Cơ chế Tốc độ 2-way Phạm vi Đặc điểm
Pipe Nhanh 1-way Parent-child Stream bytes; shell `|` dùng nó
FIFO Nhanh 1-way Bất kỳ process Như pipe nhưng có tên
Shared Memory Nhanh nhất 2-way Bất kỳ Phải tự sync; data lớn
Message Queue Trung bình 2-way Bất kỳ Có cấu trúc message + priority
Signal Nhanh 1-way Bất kỳ Notification, không data
Unix Socket Nhanh 2-way Local API giống TCP, có thể truyền fd
TCP Socket Chậm hơn local 2-way Local + remote Phổ biến nhất; dùng cho microservices

Khi nào dùng cái nào?

  • Shell pipeline → Pipe (built-in)
  • 2 process unrelated nói chuyện đơn giản → FIFO
  • Cần truyền data lớn nhanh (vd buffer video) → Shared Memory
  • Notification "process A xong rồi" → Signal
  • Local app như Docker daemon ↔ CLI → Unix Socket
  • Microservices giao tiếp → TCP Socket (HTTP, gRPC)
  • Async messaging giữa nhiều service → Message broker (RabbitMQ, Kafka)

Bài tập

Bài 1 — Pipe trong shell

Giải thích từng bước ps aux | grep node | wc -l hoạt động ra sao (3 process, 2 pipe).

Bài 2 — Named pipe demo

Tạo FIFO bằng mkfifo. Mở 2 terminal: 1 cái write, 1 cái read. Quan sát blocking behavior.

Bài 3 — Bắt SIGINT

Viết script Node bắt SIGINT (Ctrl+C). Khi nhận, in "Cleaning up..." rồi exit. Test bằng cách chạy + Ctrl+C.

process.on('SIGINT', () => {
  console.log('Cleaning up...');
  process.exit(0);
});
setInterval(() => console.log('working'), 1000);
Bài 4 — Graceful shutdown

Khi K8s/Docker stop container, gửi SIGTERM. Sau 30s nếu chưa chết → SIGKILL. Viết Node app handle SIGTERM: đóng DB connection, finish HTTP requests đang xử lý, rồi exit.

Bài 5 — Docker socket

Trên Linux/macOS, file /var/run/docker.sock là gì? Thử curl --unix-socket /var/run/docker.sock http://localhost/version.

Bài 6 — Compare Unix socket vs TCP loopback

Benchmark: gửi 1 triệu message qua Unix domain socket vs TCP localhost. So sánh throughput & latency.

🧪 Quiz cuối chương

Câu 1. Tại sao 2 process không thể đọc trực tiếp biến của nhau?

  • Vì memory không đủ
  • Vì process chạy single-threaded
  • Vì virtual memory cô lập — mỗi process có address space riêng
  • Vì C không support

Đáp án: virtual memory cô lập. Cần IPC qua kernel để chia sẻ data.

Câu 2. Pipe (anonymous) chỉ dùng được giữa?

  • 2 process bất kỳ
  • Parent và child (vì cần kế thừa fd qua fork)
  • Chỉ trên cùng máy
  • 2 thread

Đáp án: parent-child. Anonymous pipe không có tên trong filesystem; cần kế thừa fd qua fork.

Câu 3. SIGKILL khác SIGTERM ở đâu?

  • SIGKILL không thể bắt/ignore — kernel kill ngay; SIGTERM có thể bắt để cleanup
  • SIGKILL chậm hơn
  • SIGTERM cần root
  • Hai cái giống nhau

Đáp án: SIGKILL không bắt được. Best practice: SIGTERM trước (cho app cleanup), nếu không chết → SIGKILL.

Câu 4. Cơ chế IPC nào nhanh nhất?

  • Pipe
  • Shared Memory — không qua syscall mỗi lần đọc/ghi
  • Message Queue
  • Signal

Đáp án: Shared Memory. Nhược: phải tự sync (mutex/semaphore — chương 6).

Câu 5. Khi gõ Ctrl+C, terminal làm gì?

  • Gọi syscall exit
  • Gửi SIGKILL
  • Gửi SIGINT đến foreground process group
  • Reboot máy

Đáp án: SIGINT. App có thể bắt SIGINT để cleanup. Default action: terminate.

Câu 6. Unix Domain Socket khác TCP loopback như nào?

  • Cùng tốc độ
  • Unix socket cần network card
  • Unix socket không qua network stack → nhanh hơn TCP loopback; có thể truyền file descriptor
  • TCP loopback nhanh hơn

Đáp án: Unix socket nhanh hơn + truyền fd được. Đó là lý do Docker, PostgreSQL, X11 dùng Unix socket cho local.

Câu 7. Pipe có buffer kernel ~64KB. Khi buffer đầy mà process tiếp tục write?

  • write() block đến khi reader đọc bớt
  • Lỗi EOF
  • Data bị drop
  • Kernel panic

Đáp án: write block. Đây là backpressure tự nhiên — fast producer, slow consumer.

Câu 8. Khi container nhận SIGTERM (vd K8s scale-down), app nên?

  • Ignore SIGTERM
  • Bắt SIGTERM, finish in-flight requests, đóng DB connection, exit gracefully
  • exit ngay lập tức
  • Restart container

Đáp án: graceful shutdown. K8s đợi terminationGracePeriodSeconds (default 30s) trước khi SIGKILL.

Tổng kết chương 5

  • ✅ IPC = cách process giao tiếp khi memory bị cô lập
  • Pipe: 1-way, parent-child, dùng cho shell |
  • FIFO: pipe có tên trong filesystem
  • Shared Memory: nhanh nhất, cần đồng bộ thủ công
  • Message Queue: messages có cấu trúc + priority
  • Signal: notification không data; SIGTERM (cleanup được) ≠ SIGKILL (force)
  • Unix Socket: local, nhanh, truyền fd được — Docker, PostgreSQL dùng
  • TCP Socket: phổ biến nhất, dùng cho microservices
  • ✅ Graceful shutdown: bắt SIGTERM, cleanup, exit — quan trọng cho production
← Chương trước Chương 04: CPU Scheduling Chương kế tiếp Chương 06: Synchronization →