Linux network stack — từ NIC interrupt tới application
Hành trình gói tin qua kernel: NIC interrupt, sk_buff, netfilter hooks, softirq, socket receive buffer — nền để biết gói drop ở đâu khi quá tải.
TL;DR: Mỗi gói tin vào host Linux không "tới thẳng" process của bạn. NIC đưa frame vào receive ring, kernel (thường qua NAPI + softirq) bọc frame trong cấu trúc sk_buff, đẩy qua stack giao thức và các netfilter hooks, rồi mới xếp vào socket receive buffer để recv()/read() lấy. Khi tải cao, gói có thể drop ở nhiều tầng khác nhau — ring đầy, softnet backlog, rule netfilter, hoặc rcvbuf đầy — nên timeout ứng dụng không đồng nghĩa "mạng hỏng".
Production 2 giờ sáng: container nhận request HTTP thỉnh thoảng 504 / Connection reset, CPU không kẹt 100%, curl từ cùng host lúc lẻ vẫn ổn. Team nghi "app chậm". Bạn mở ss thấy Recv-Q phình; ip -s link báo RX drop tăng; /proc/net/softnet_stat cột drop không còn bằng 0. Ba chỗ drop khác tầng — cùng một triệu chứng "request biến mất".
Nhiều tài liệu container dừng ở docker network và iptables. Bài này đi một tầng sâu hơn: gói đi từ dây cáp / vNIC vào kernel ra sao, sk_buff mang gì, softirq làm gì, netfilter "cắm" vào đâu trên đường đi, và buffer socket tạo backpressure thế nào. Đây là xương sống Module 1 — mọi bài veth, bridge, iptables sau này chỉ là rẽ nhánh trên bản đồ này.
Bài này giải thích cơ chế bên dưới path RX (và phác TX), để bạn trace được "gói chết ở tầng nào" trước khi đụng netns hay CNI.
1. Analogy — Bưu cục nhận thư trong toà nhà
Hình dung một toà văn phòng (host Linux) nhận thư từ đường (mạng):
- Cổng bảo vệ / băng chuyền nhận túi thư từ xe tải — không đọc nội dung, chỉ chất vào khay.
- Nhân viên phân loại (không phải bảo vệ đứng chặn xe) xé túi, dán phiếu hành trình, đẩy vào băng chuyền nội bộ.
- Trạm kiểm soát trên hành lang: quét "được vào tầng này không?" — có thể trả về, chuyển tầng, hoặc huỷ.
- Hòm thư từng phòng (process/socket): thư nằm đó cho đến khi nhân viên phòng lấy ra đọc.
Nếu xe tải dồn dập mà khay cổng đầy → thư rơi trước khi vào toà. Nếu phân loại chậm → băng chuyền nội bộ ùn. Nếu hòm thư phòng đầy → thư mới bị từ chối dù toà còn chỗ. Ba cảnh "thư biến mất" trông giống nhau với người gửi, nhưng chỗ hỏng khác nhau.
| Đời thường (bưu cục) | Linux network stack |
|---|---|
| Cổng / băng chuyền nhận túi | NIC + receive ring (DMA) |
| Nhân viên phân loại (batch, không đứng chặn xe) | softirq / NAPI poll |
| Phiếu hành trình dán lên túi | sk_buff (metadata + trỏ payload) |
| Trạm kiểm soát hành lang | Netfilter hooks (+ routing) |
| Hòm thư phòng | Socket receive buffer (rcvbuf) |
| Nhân viên phòng mở thư | recv() / read() của process |
Gói vào máy = nhận (ring) → xử lý nền (softirq) → dán phiếu (sk_buff) → kiểm soát (netfilter) → hòm thư (socket) → app đọc. Timeout app chỉ nói "hòm thư không ra thư đúng hạn" — chưa nói khay cổng đã đầy từ lâu.
2. Gói tin đi từ NIC interrupt tới softirq thế nào?
NIC (Network Interface Card) — card mạng vật lý hoặc vNIC ảo hoá — nhận frame Ethernet (hoặc tương đương) từ dây / hypervisor. Phần cứng DMA (Direct Memory Access) chép frame vào vùng nhớ kernel đã map sẵn, thường là receive ring (vòng đệm descriptor + buffer). Driver đã “đăng ký” sẵn các buffer với card; khi frame tới, card ghi dữ liệu và đánh dấu descriptor — kernel chưa kịp chạy protocol stack.
flowchart TB
NIC["NIC DMA to RING"] --> IRQ["Hard IRQ / NAPI schedule"]
IRQ --> SOFT["SOFTIRQ NET_RX"]
SOFT --> SKB["Build / fill sk_buff"]
SKB --> PROTO["L2/L3/L4 demux"]
PROTO --> NF["NETFILTER hooks"]
NF --> SOCK["SOCK_RCV buffer"]
SOCK --> APP["Application recv"]Thứ tự đọc sơ đồ RX (ASCII labels cố ý khớp debug mental model):
| Bước | Nhãn | Việc làm |
|---|---|---|
| 1 | NIC + RING | Frame nằm trong receive ring sau DMA |
| 2 | SOFTIRQ | Kernel poll ring, tạo/điền sk_buff |
| 3 | NETFILTER | Hook filter/NAT trên path IP (local hoặc forward) |
| 4 | SOCK_RCV | Queue nhận của socket đích |
| 5 | App | Process recv / epoll wake |
Vì sao không xử lý gói ngay trong hard IRQ?
Hardware interrupt (hard IRQ) phải ngắn: mask IRQ khác, không được chạy logic nặng. Nếu mỗi gói đều "xử lý TCP đầy đủ" trong hard IRQ, máy sẽ nghẽn interrupt storm dưới tải. Linux tách:
- Hard IRQ / NAPI schedule — báo "có việc", tắt (hoặc hạn chế) interrupt storm, lên lịch poll.
- softirq
NET_RX_SOFTIRQ— chạy ngoài hard IRQ context, poll ring, đẩy gói vào stack.
NAPI (New API — tên lịch sử, vẫn dùng) là mô hình hybrid: lúc traffic thấp dùng interrupt; lúc traffic cao chuyển sang polling theo budget (số gói / trọng số thời gian mỗi lần softirq) để amortize chi phí interrupt. Driver NIC hiện đại gần như luôn NAPI-aware.
Khi softirq không kịp drain ring:
- Ring/driver có thể drop frame mới (RX dropped trên interface).
- Hoặc gói được xếp tạm vào input backlog per-CPU; nếu backlog đầy (
netdev_max_backlog) → drop ghi nhận trong/proc/net/softnet_stat.
Đó là lý do “CPU còn rảnh theo top” vẫn có thể mất gói RX: work nằm trên softirq path và ring, không phải process app.
Softirq chạy trong kernel — có thể trên cùng CPU vừa nhận IRQ, hoặc được đẩy sang ksoftirqd khi quá tải. Bạn không ps ra "process netfilter" cho mỗi gói. Quan sát gián tiếp: /proc/net/softnet_stat, latency, drop counter — không phải PID app.
Multi-queue và scaling (preview)
NIC nhiều queue (RSS) phân tán interrupt/softirq lên nhiều CPU. Kernel còn RPS/RFS/XPS (xem Deep Dive scaling) để steer softirq/app affinity. Khi softnet_stat chỉ “nóng” trên một vài CPU, vấn đề thường là không scale RX chứ không phải “băng thông wire hết”. Chi tiết tuning để khi bạn đã thấy counter sống trên máy load.
3. sk_buff là gì — metadata và payload?
sk_buff (socket buffer) là cấu trúc C trung tâm của networking subsystem Linux. Gần như mọi gói đi qua stack đều được bọc trong một (hoặc chuỗi) sk_buff. Đây không phải buffer của SO_RCVBUF (giới hạn bộ nhớ socket user thấy gián tiếp) — sk_buff là đối tượng kernel per-packet trên đường đi.
Vì sao cần "phiếu hành trình" riêng?
Frame trên wire chỉ là dãy byte. Kernel cần điều hướng mà không copy lung tung, và vẫn biết:
- Frame thuộc device nào (
skb->dev) - Protocol nào (
skb->protocol) — ethertype, rồi IP/TCP… - Con trỏ tới đâu là L2/L3/L4 header (kéo
skb_pull/skb_pushkhi strip/add header) - Độ dài, checksum offload flags, network namespace, mark cho netfilter…
Payload thường nằm trên trang bộ nhớ (fragment list, page frags); sk_buff trỏ tới dữ liệu hơn là luôn nhét cả frame vào một struct liền. Stack có thể forward hoặc NAT mà không "chép cả gói" mỗi tầng một lần (clone / shared data path khi tối ưu).
+------------------+ +---------------------------+
| sk_buff (meta) |---->| linear data / page frags |
| dev, protocol | | [Eth][IP][TCP][payload] |
| head/data/tail | +---------------------------+
| len, csum, ... |
+------------------+
| Thành phần khái niệm | Vai trò |
|---|---|
Metadata (sk_buff) | Điều khiển path: device, protocol, hook state, socket gắn kết |
| Payload | Byte thật của frame/segment |
| Clone / shared data | Nhiều sk_buff có thể share payload (reference count) khi fan-out |
Headroom / tailroom: vùng đệm trước/sau payload cho phép push header (ví dụ thêm encapsulation) hoặc pull header mà không realloc toàn bộ. Đó là chi tiết implementation — đủ biết: “đụng header” trên path thường rẻ hơn copy full frame.
Bạn không malloc sk_buff từ user space. Application chỉ thấy socket API; kernel cấp phát/giải phóng sk_buff trên RX/TX path. Hiểu sk_buff tồn tại giúp bạn đọc kernel docs, eBPF trace điểm skb, và giải thích vì sao "một gói" trong tcpdump tương ứng nhiều bước xử lý trong kernel trước khi app recv.
4. RX/TX path và netfilter hooks
RX — gói vào máy (local delivery)
Sau softirq và sk_buff, path local receive (gói dành cho host này, không forward) gần đúng:
- L2: validate Ethernet, strip (hoặc giữ) header theo device type.
- L3: IPv4/IPv6 input — checksum, defrag nếu cần.
- Netfilter PREROUTING — sớm, trước quyết định routing đầy đủ (DNAT cổ điển bám đây).
- Routing decision — local? forward? drop (no route)?
- Netfilter INPUT (local) — filter "có được vào socket local không?".
- L4: demux theo 5-tuple tới listening / established socket.
- Đẩy data vào receive queue của socket; wake process đang block/
epoll.
Gói forward (router, bridge L3, Docker bridge NAT path phức tạp hơn) rẽ FORWARD thay vì INPUT, rồi POSTROUTING trước khi ra device khác. Module này chỉ cần bạn nhớ tên hook và vị trí tương đối — bài 04 — iptables và netfilter đào tables/chains/Docker map.
flowchart LR
RING["RING"] --> SOFTIRQ["SOFTIRQ"]
SOFTIRQ --> SKB["sk_buff"]
SKB --> PRE["NF PREROUTING"]
PRE --> RT{"Local or Forward?"}
RT -->|"local"| IN["NF INPUT"]
RT -->|"forward"| FW["NF FORWARD"]
IN --> SOCK["SOCK_RCV"]
FW --> POST["NF POSTROUTING"]
POST --> TX["Egress device"]TX — gói ra máy (phác thảo)
Chiều ngược, đủ để đóng vòng:
flowchart LR
APP["App send"] --> SND["Socket send buf"]
SND --> SKB["sk_buff + headers"]
SKB --> OUT["NF OUTPUT"]
OUT --> POST["NF POSTROUTING"]
POST --> QDISC["qdisc"]
QDISC --> TXRING["TX RING"]
TXRING --> NIC["NIC wire"]- App
send/write→ copy (hoặc zero-copy) vào socket send buffer. - Stack build
sk_buff, gắn header L4/L3. - Netfilter OUTPUT (local origin) → routing → POSTROUTING (SNAT hay gặp với container).
- qdisc (queueing discipline) trên device — có thể delay/shape/drop (TX drop ≠ RX drop).
- Driver đẩy vào TX ring → NIC truyền.
Container "không ra Internet" thường chết ở FORWARD + POSTROUTING NAT, không phải "NIC hỏng" — nhưng muốn chắc, bạn cần biết gói đã từng vào softirq/stack hay chưa (counter RX trên path vào) trước khi đổ lỗi iptables.
Netfilter — preview, chưa phải iptables CLI
Netfilter là framework hook trong kernel. iptables / nftables là frontend user-space cắm rule vào các hook đó. Docker inject rule vào filter/nat — vẫn là netfilter. Khi debug "DROP", hỏi: hook nào, chain nào, counter rule nào tăng — không hỏi mơ hồ "firewall có bật không?".
| Hook (khái niệm) | Gói nào đi qua | Ví dụ rule Docker-ish |
|---|---|---|
| PREROUTING | Mọi gói vào, trước routing | DNAT publish port |
| INPUT | Gói tới process trên host | Filter SSH/API host |
| FORWARD | Gói đi xuyên host (container↔WAN) | DOCKER-USER, policy DROP |
| OUTPUT | Gói sinh ra từ process host | Ít đụng container path |
| POSTROUTING | Gói sắp ra device | MASQUERADE SNAT |
5. Gói drop ở đâu khi quá tải?
Socket receive buffer là giới hạn bộ nhớ kernel gắn với mỗi socket (tunable net.core.rmem_*, SO_RCVBUF). TCP còn cửa sổ nhận quảng bá cho peer: khi buffer đầy, peer bị backpressure (zero window) — "đừng gửi thêm". UDP không có backpressure đẹp: kernel drop datagram khi queue đầy.
Bốn điểm drop hay nhầm lẫn
| Điểm drop | Triệu chứng gợi ý | Quan sát |
|---|---|---|
| NIC / driver ring đầy | RX drop tăng, softirq chưa kịp poll | ip -s link, ethtool -S (tên counter theo driver) |
softnet backlog (netdev_max_backlog) | Cột drop trong softnet_stat tăng | /proc/net/softnet_stat |
| Netfilter DROP/REJECT | Counter rule iptables/nft tăng; tcpdump thấy gói vào nhưng app không thấy | iptables -L -n -v / nft |
| Socket rcvbuf đầy | Recv-Q cao; TCP zero window; UDP loss | ss -ltnp / ss -m |
Worked example — đọc counter trên máy Linux
Chạy trên Linux (VM/WSL2/host). Không cần root cho hầu hết lệnh đọc. Mục tiêu: gắn mỗi lệnh với một tầng trên bản đồ §2–§4.
# 1) Interface counters: RX/TX bytes, errors, dropped
ip -s link show
# Look at "RX: ... dropped" on the interface under load
# 2) Per-CPU softirq receive stats (one line per CPU)
# Common layout (hex): total, dropped, time_squeeze, ...
# Exact column docs vary by kernel — treat non-zero "dropped"
# fields under load as a softirq/backlog smell, then cross-check.
cat /proc/net/softnet_stat
# Optional: backlog limit (packets queued to CPU backlog)
sysctl net.core.netdev_max_backlog
# Default often 1000; raising it is not a substitute for fixing
# a single-CPU RX bottleneck — it only delays the cliff.
# 3) Socket queues: Recv-Q = bytes waiting for the app to read
ss -ltnp
# If Recv-Q stays high while clients timeout, app may not drain
# 4) Memory accounting for sockets (where supported)
ss -m
# 5) Netfilter counters (needs privileges for full tables)
# sudo iptables -L -n -v
# Watch the "pkts" column on DROP/REJECT rules while reproducing
Trước khi chạy ss trên một service đang "chậm", bạn đoán Recv-Q sẽ: (a) gần 0 vì app khoẻ, (b) phình vì app không đọc kịp, hay (c) gần 0 vì gói đã drop ở ring/netfilter trước khi vào socket? Viết lựa chọn ra — rồi đối chiếu với ip -s link + softnet_stat + iptables counters. Không mở đáp án trong đầu bằng cách chỉ nhìn log app.
Cách đọc kết hợp (worked mental checklist):
ip -s linkRX dropped tăng mạnh → nghi trước socket (ring/driver).- softnet_stat drop / time_squeeze tăng, ring tương đối ổn → nghi softirq/backlog không kịp.
- Counter iptables/nft DROP tăng → gói vào stack nhưng bị chặn policy.
Recv-Qlớn, RX drop thấp → gói đã vào socket, app/slow consumer.
Cùng một curl timeout — bốn kết luận dẫn tới bốn hướng fix khác nhau: tuning ring/RPS, scale softirq/CPU affinity, sửa rule FORWARD/NAT, scale worker/app. Đừng bắt đầu bằng docker restart khi chưa biết tầng nào.
Mini scenario — ba ticket, một triệu chứng
| Ticket (user nói) | Counter gợi ý | Tầng |
|---|---|---|
| "API treo 30s rồi 504" | Rule DROP pkts tăng; peer không RST | Netfilter DROP + client retry |
| "Burst traffic mất request" | softnet_stat drop tăng theo CPU | Softirq / backlog |
| "Chậm dù CPU app 20%" | Recv-Q phình trên listener | Socket / app drain |
Ba ticket đều có thể mô tả giống nhau trên Slack. Bản đồ stack khiến bạn hỏi counter nào trước khi hỏi framework nào.
6. Pitfall tổng hợp
❌ Nhầm 1: "Timeout app = mạng/kernel hỏng."
✅ Timeout chỉ nói deadline phía client/app. Gói có thể đã drop ở ring, netfilter, hoặc nằm đầy Recv-Q vì thread event loop block. Luôn tách: wire → kernel → socket → app.
❌ Nhầm 2: "Chỉ cần xem iptables là biết mọi drop."
✅ iptables/nft chỉ thấy path đi qua netfilter và rule match. Ring full và softnet backlog không hiện trong iptables -L -v. Bỏ sót ip -s link / softnet_stat = debug mù nửa dưới.
❌ Nhầm 3: "Container timeout chắc chắn do Docker network magic."
✅ Docker/K8s script stack Linux (veth, bridge, rule). Path gói vẫn là softirq → sk_buff → hooks → socket — chỉ thêm netns và device ảo. Bài sau (veth / tun / tap) và bridge gắn thêm "cáp ảo"; iptables gắn rule. Không có stack map → mỗi lần chỉ restart daemon.
❌ Nhầm 4: "Một tcpdump trên host chứng minh app đã nhận."
✅ tcpdump bám gói ở điểm capture (thường AF_PACKET trên interface) — trước hoặc quanh stack, không đồng nghĩa process đã recv xong. Gói có thể vẫn drop giữa capture point và socket queue.
Netfilter DROP thường im: peer có thể retransmit rồi timeout. REJECT gửi ICMP/TCP RST — client fail nhanh hơn. Production "treo 30s" đôi khi là DROP + client retry, không phải NIC chết. Phân biệt bằng counter rule + tcpdump hai đầu, không chỉ log app.
📚 Deep Dive — kernel & netfilter
Spec / reference chính thức:
- Linux networking documentation (index) — mục lục subsystem mạng kernel: scaling, device, protocol.
- sk_buff — kernel docs — cấu trúc socket buffer, layout metadata/payload.
- Netfilter documentation — hook model, iptables/nftables phía user-space.
- Scaling in the Linux Networking Stack — RPS/RFS/XPS, multi-queue — mở rộng khi softnet_stat báo quá tải.
Ghi chú: Đọc kernel docs theo câu hỏi debug ("backlog drop nghĩa là gì?") hơn là đọc tuần tự từ A–Z. Bài 04 sẽ bám netfilter sâu hơn; scaling để dành khi bạn đã thấy softnet_stat sống trên máy load.
Liên hệ các bài khác
- 00 — Tổng quan module — bản đồ bốn mảnh plumbing; bài này là mảnh 1 (stack).
- 02 — Virtual network devices — sau khi gói rời/ra device,
veth/tun/taplà "cáp và cửa" ảo mà container ghép vào cùng stack. - 03 — Linux bridge — L2 switch ảo (
docker0): gói có thể bridge forward trước khi (hoặc thay vì) local socket path. - 04 — iptables và netfilter — tables/chains và rule Docker map đúng các hook đã preview ở §4.
- Networking Foundations — từ URL tới response — path end-to-end 5 chặng (DNS→TCP→TLS→HTTP); bài này là bên trong một hop Linux.
Tóm tắt
- RX path: NIC ring → softirq/NAPI →
sk_buff→ protocol + netfilter → socket rcv buffer → app. sk_buff= metadata điều khiển path + trỏ payload; không phải "chỉ byte trên wire".- Netfilter hooks chia path local (INPUT) vs forward (FORWARD); NAT hay bám PREROUTING/POSTROUTING.
- Drop khi quá tải: ring, softnet backlog, netfilter, rcvbuf — bốn chỗ, bốn counter.
- Lệnh nền:
ip -s link,/proc/net/softnet_stat,ss(Recv-Q), iptables/nft counters. - Docker không thay stack — nó tạo device/rule trên cùng cơ chế kernel này.
- Timeout app ≠ đủ chẩn đoán; luôn hỏi gói đã qua tầng nào.
7. Tự kiểm tra
Q1Vì sao Linux tách hard IRQ (hoặc NAPI schedule) khỏi softirq NET_RX thay vì xử lý hết TCP trong interrupt handler?▸
Q2sk_buff mang gì khác so với 'dãy byte frame trên wire'? Vì sao kernel cần cấu trúc đó?▸
sk_buff thêm metadata: device, protocol, con trỏ header, độ dài, cờ offload, ngữ cảnh netfilter/socket… Kernel điều hướng gói qua stack và tránh copy mù quáng nhờ tách metadata khỏi dữ liệu (và share/clone khi cần).Q3Một gói IPv4 tới process đang listen trên host đi qua hook netfilter nào theo thứ tự khái niệm (local delivery)? FORWARD có nằm trên path đó không?▸
Q4ss báo Recv-Q rất cao và client timeout, trong khi ip -s link RX dropped gần như không tăng. Bạn nghi drop/điểm nghẽn ở đâu trước — và vì sao không phải ring?▸
Recv-Q cao). Ring/driver drop thường làm RX dropped trên interface tăng; ở đây counter đó yên → nghẽn phía consumer (app) hoặc rcvbuf, không phải NIC ring.Q5Vì sao chỉ nhìn iptables -L -v không đủ để kết luận 'mọi packet drop đều do firewall'?▸
ip -s link, softnet_stat, và ss để phủ các tầng drop khác.Q6Docker 'publish port' và MASQUERADE container ra Internet bám loosely vào hook netfilter nào (preview)? Liên hệ với path RX/TX vừa học.▸
Q7Khi nào UDP và TCP hành xử khác nhau khi socket receive buffer đầy? Hệ quả quan sát phía peer?▸
Bài tiếp theo: Virtual network devices — veth, tun, tap
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên