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
- Pipe — anonymous, parent-child
- FIFO — named pipe, processes bất kỳ qua filesystem
- Shared Memory — nhanh nhất, cần đồng bộ thủ công
- Message Queue — kernel queues messages
- Signal — async notification (vd Ctrl+C)
- 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
| Signal | Số | Ý nghĩa | Default action |
|---|---|---|---|
SIGTERM | 15 | Yêu cầu kết thúc lịch sự (terminate) | Terminate |
SIGKILL | 9 | Force kill, KHÔNG bắt được | Terminate |
SIGINT | 2 | Ctrl+C trên terminal | Terminate |
SIGSTOP | 19 | Dừng (Ctrl+Z), KHÔNG bắt được | Stop |
SIGCONT | 18 | Tiếp tục sau SIGSTOP | Continue |
SIGCHLD | 17 | Child kết thúc → parent nhận | Ignore |
SIGSEGV | 11 | Segmentation fault | Terminate + core dump |
SIGPIPE | 13 | Ghi pipe mà reader đã chết | Terminate |
SIGHUP | 1 | Terminal đóng (hangup); thường dùng "reload config" | Terminate |
SIGUSR1/SIGUSR2 | 10/12 | User defined | Terminate |
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 podhoạ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
Giải thích từng bước ps aux | grep node | wc -l hoạt động ra sao (3 process, 2 pipe).
Tạo FIFO bằng mkfifo. Mở 2 terminal: 1 cái write, 1 cái read. Quan sát blocking behavior.
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);
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.
Trên Linux/macOS, file /var/run/docker.sock là gì? Thử curl --unix-socket /var/run/docker.sock http://localhost/version.
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?
Đá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?
Đá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?
Đá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?
Đá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ì?
Đá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?
Đá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?
Đá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?
Đá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