Mạng Linux & Container/Linux network stack — từ NIC interrupt tới application
2/21
Bài 2 / 21~13 phútLinux Network InternalsMiễn phí lượt xem

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

  1. 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.
  2. 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ộ.
  3. 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ỷ.
  4. 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úiNIC + 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úisk_buff (metadata + trỏ payload)
Trạm kiểm soát hành langNetfilter hooks (+ routing)
Hòm thư phòngSocket receive buffer (rcvbuf)
Nhân viên phòng mở thưrecv() / read() của process
💡 Cách nhớ

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ướcNhãnViệc làm
1NIC + RINGFrame nằm trong receive ring sau DMA
2SOFTIRQKernel poll ring, tạo/điền sk_buff
3NETFILTERHook filter/NAT trên path IP (local hoặc forward)
4SOCK_RCVQueue nhận của socket đích
5AppProcess 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:

  1. Hard IRQ / NAPI schedule — báo "có việc", tắt (hoặc hạn chế) interrupt storm, lên lịch poll.
  2. 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 không phải 'thread user'

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đố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_push khi 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ệmVai trò
Metadata (sk_buff)Điều khiển path: device, protocol, hook state, socket gắn kết
PayloadByte thật của frame/segment
Clone / shared dataNhiề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:

  1. L2: validate Ethernet, strip (hoặc giữ) header theo device type.
  2. L3: IPv4/IPv6 input — checksum, defrag nếu cần.
  3. Netfilter PREROUTING — sớm, trước quyết định routing đầy đủ (DNAT cổ điển bám đây).
  4. Routing decision — local? forward? drop (no route)?
  5. Netfilter INPUT (local) — filter "có được vào socket local không?".
  6. L4: demux theo 5-tuple tới listening / established socket.
  7. Đẩ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"]
  1. App send/write → copy (hoặc zero-copy) vào socket send buffer.
  2. Stack build sk_buff, gắn header L4/L3.
  3. Netfilter OUTPUT (local origin) → routing → POSTROUTING (SNAT hay gặp với container).
  4. qdisc (queueing discipline) trên device — có thể delay/shape/drop (TX drop ≠ RX drop).
  5. 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 quaVí dụ rule Docker-ish
PREROUTINGMọi gói vào, trước routingDNAT publish port
INPUTGói tới process trên hostFilter SSH/API host
FORWARDGói đi xuyên host (container↔WAN)DOCKER-USER, policy DROP
OUTPUTGói sinh ra từ process hostÍt đụng container path
POSTROUTINGGói sắp ra deviceMASQUERADE 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 dropTriệu chứng gợi ýQuan sát
NIC / driver ring đầyRX drop tăng, softirq chưa kịp pollip -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/REJECTCounter rule iptables/nft tăng; tcpdump thấy gói vào nhưng app không thấyiptables -L -n -v / nft
Socket rcvbuf đầyRecv-Q cao; TCP zero window; UDP lossss -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
Thử đoán

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

  1. ip -s link RX dropped tăng mạnh → nghi trước socket (ring/driver).
  2. softnet_stat drop / time_squeeze tăng, ring tương đối ổn → nghi softirq/backlog không kịp.
  3. Counter iptables/nft DROP tăng → gói vào stack nhưng bị chặn policy.
  4. Recv-Q lớ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 RSTNetfilter DROP + client retry
"Burst traffic mất request"softnet_stat drop tăng theo CPUSoftirq / backlog
"Chậm dù CPU app 20%"Recv-Q phình trên listenerSocket / 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.

⚠️ Drop im lặng vs REJECT

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

📚 Deep Dive — tài liệu gốc

Spec / reference chính thức:

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

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

Tự kiểm tra
Q1
Vì 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?
Hard IRQ phải ngắn để tránh chặn CPU và interrupt storm. Logic protocol (parse, netfilter, xếp socket) nặng và có thể chạy lâu; softirq/NAPI poll xử lý gói theo budget ngoài hard IRQ context, cho phép batch và giảm chi phí interrupt dưới tải cao.
Q2
sk_buff mang gì khác so với 'dãy byte frame trên wire'? Vì sao kernel cần cấu trúc đó?
Frame trên wire chỉ là payload/byte. 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).
Q3
Mộ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?
Local delivery: sau khi vào stack, gói đi PREROUTING, routing chọn local, rồi INPUT, sau đó vào socket. FORWARD dành cho gói xuyên host (forward), không nằm trên path local INPUT thuần.
Q4
ss 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?
Nghi socket receive buffer / app không drain kịp: gói đã vào kernel và xếp vào queue socket (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.
Q5
Vì sao chỉ nhìn iptables -L -v không đủ để kết luận 'mọi packet drop đều do firewall'?
iptables/nft chỉ đếm gói đi qua netfilter và match rule. Gói có thể đã mất ở receive ring hoặc softnet backlog trước khi (hoặc ngoài) rule bạn đang xem. Cần thêm ip -s link, softnet_stat, và ss để phủ các tầng drop khác.
Q6
Docker '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.
Publish port (DNAT) thường liên quan PREROUTING (đổi địa chỉ đích sớm). MASQUERADE/SNAT container ra ngoài bám POSTROUTING trên path egress/forward. Cả hai là rule netfilter trên path đã vẽ ở §4 — không phải protocol riêng của Docker.
Q7
Khi nào UDP và TCP hành xử khác nhau khi socket receive buffer đầy? Hệ quả quan sát phía peer?
TCP dùng cửa sổ nhận / backpressure: buffer đầy → peer có thể thấy zero window, chậm gửi thay vì mất im lặng ngay. UDP không có cơ chế đó: kernel drop datagram khi queue đầy; peer thường không biết trừ khi tầng app tự ACK/timeout.

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

Đặt 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

Bài tiếp theo

Virtual network devices — veth, tun, tap