Tổng kết module — Đường đi I/O & GPU
Cheat sheet đường đi I/O từ thiết bị tới ứng dụng, glossary, pitfall tổng hợp và self-assessment đối chiếu bảy learning outcome của module.
TL;DR: Module này lần theo một tuyến đường duy nhất, từ chỗ CPU chạm vào thanh ghi thiết bị cho tới chỗ dữ liệu nằm trong biến của chương trình bạn. Trên tuyến ấy có bốn câu hỏi lặp đi lặp lại ở mọi tầng: ai chủ động, ai tốn chu kỳ CPU, dữ liệu bị chép mấy lần, và cái gì đang chờ cái gì. Trả lời được bốn câu đó ở một tầng bất kỳ thì bạn chẩn đoán được tầng ấy. Đây là một trang để đánh dấu và quay lại.
Đã đi qua những gì
Bạn bắt đầu ở chỗ thấp nhất mà phần mềm còn với tới: vài thanh ghi nằm trong không gian địa chỉ vật lý, và một cái cây PCIe nối chúng về CPU. Từ đó lên một nấc, câu hỏi đổi thành ai trả tiền cho việc chờ và việc chép, và câu trả lời hoá ra không cố định — polling thắng interrupt ở tốc độ cao, đúng chỗ trực giác nói ngược lại.
Bài 03 ráp mọi thứ thành một tuyến đường liền mạch, và từ đó hai bài kế tiếp tấn công hai trục tối ưu khác nhau: cách chờ, và số bản sao. Hai bài cuối lấy GPU làm ví dụ cực đoan — một cỗ máy tính toán mạnh tới mức chính đoạn dây nối nó với CPU trở thành nút thắt.

🗺️ Cheat sheet
| Khái niệm | Cốt lõi | Pitfall hay gặp |
|---|---|---|
| Thanh ghi thiết bị | Ô nhớ có side effect, không phải RAM | Dereference con trỏ trần, trình biên dịch tối ưu mất lần đọc |
| MMIO so với port I/O | Thiết bị chiếm dải địa chỉ vật lý, dùng lệnh load/store thường | Tưởng cần lệnh CPU đặc biệt mới điều khiển được phần cứng |
| PCIe | Cây điểm-nối-điểm, băng thông theo lane từng khe | Tưởng là bus dùng chung nên cắm thêm card là chậm cả máy |
| Posted write | Ghi ra PCI được xác nhận từ lúc rời CPU | Coi hàm ghi trả về là bằng chứng thiết bị đã nhận |
| Polling | Đốt lõi CPU, đổi lại độ trễ thấp nhất | Coi polling luôn là dấu hiệu code kém |
| Interrupt | Trả CPU cho việc khác, mỗi lần tốn một chuyển ngữ cảnh | Bỏ qua livelock khi tần suất lên cao |
| DMA | Thiết bị tự chuyển dữ liệu vào RAM sau khi đọc descriptor | Chạm buffer đang thuộc quyền thiết bị trong streaming DMA |
| NAPI | Tắt ngắt rồi poll theo lô có budget | Nghĩ polling và interrupt loại trừ nhau |
| Page cache | Cổng rẽ sớm nhất trên đường read() | Benchmark I/O mà quên xoá cache |
| blk-mq | Hai tầng hàng đợi, tag cho hoàn tất ngoài thứ tự | Đổi I/O scheduler như một mẹo tăng thông lượng |
| Blocking | Dễ viết đúng, đắt theo số kết nối | Dùng cho hàng vạn kết nối |
select/poll | Chi phí theo tổng số fd mỗi lượt | Tưởng khác epoll chỉ ở cú pháp |
epoll | Tập fd sống trong kernel, trả về fd sẵn sàng | Edge-triggered mà không đọc tới EAGAIN |
io_uring | Mô hình hoàn tất, hàng đợi chia sẻ với user space | Tưởng epoll async được file thường |
| Zero-copy | sendfile bỏ hai bản sao do CPU | Kỳ vọng zero-copy trong khi vẫn bật TLS |
| SIMT / warp | 32 thread dùng chung một luồng lệnh | Nghĩ GPU chạy song song cả hai nhánh if |
| Warp divergence | Hai nhánh bị tuần tự hoá | So GPU với CPU bằng số nhân |
| Host-device copy | PCIe hẹp hơn hẳn đường GPU tới bộ nhớ của nó | Đo tốc độ kernel rồi kết luận về tốc độ pipeline |
📖 Glossary module
| Thuật ngữ | Định nghĩa một câu |
|---|---|
| Thanh ghi thiết bị | Ô nhớ do phần cứng bày ra, chạm vào là gây ra hành động chứ không chỉ đọc ghi dữ liệu. |
| MMIO | Cách gán cho thanh ghi thiết bị một dải trong không gian địa chỉ vật lý chung với RAM. |
ioremap | Hàm kernel tạo ánh xạ từ dải địa chỉ vật lý của thiết bị vào không gian ảo của kernel. |
| BAR | Thanh ghi qua đó endpoint PCIe khai báo nó cần bao nhiêu byte không gian địa chỉ. |
| Posted write | Lệnh ghi ra bus PCI được xác nhận ngay khi rời CPU, chưa chắc đã tới thiết bị. |
| Polling | Đọc lặp lại thanh ghi trạng thái cho tới khi thiết bị báo xong. |
| IRQ | Đường tín hiệu thiết bị dùng để yêu cầu CPU ngưng việc đang làm và chạy trình xử lý ngắt. |
| DMA | Cơ chế thiết bị tự chuyển dữ liệu giữa nó và RAM mà không qua CPU. |
| Descriptor | Cấu trúc trong RAM mô tả cần chuyển dữ liệu gì, đi đâu, để thiết bị tự đọc. |
| IOMMU | Tầng ánh xạ địa chỉ cho thiết bị, vừa cách ly vừa gom trang rời rạc thành vùng liền mạch. |
| Bounce buffer | Vùng trung gian kernel phải chép qua khi thiết bị không địa chỉ hoá được buffer gốc. |
| NAPI | Cơ chế mạng của Linux tắt ngắt rồi chuyển sang vòng poll có budget khi tải cao. |
| Livelock | Trạng thái máy bận 100% xử lý thông báo mà thông lượng hữu ích về gần không. |
| VFS | Lớp trừu tượng khiến read() chạy như nhau trên mọi loại filesystem. |
bio | Đơn vị mô tả một yêu cầu I/O ở block layer: đọc hay ghi, sector nào, vào trang nhớ nào. |
| blk-mq | Kiến trúc hàng đợi hai tầng của block layer, staging ở phần mềm và dispatch ở phần cứng. |
| Tag | Định danh gắn kèm mỗi yêu cầu để thiết bị báo lại đúng yêu cầu nào đã hoàn tất. |
O_NONBLOCK | Cờ khiến thao tác trả về EAGAIN ngay thay vì nằm chờ dữ liệu. |
| Level-triggered | Chế độ epoll báo chừng nào còn dữ liệu chưa đọc hết. |
| Edge-triggered | Chế độ epoll chỉ báo một lần mỗi khi trạng thái đổi sang sẵn sàng. |
io_uring | Giao diện I/O dựa trên hai hàng đợi chia sẻ giữa kernel và user space, làm việc theo hoàn tất. |
| Zero-copy | Nhóm kỹ thuật giữ dữ liệu trong kernel để bỏ các bản sao do CPU thực hiện. |
| Scatter-gather DMA | Khả năng thiết bị tự gom dữ liệu từ nhiều vùng nhớ rời rạc, bỏ nốt bản sao gom sẵn. |
| Warp | Nhóm 32 thread GPU cùng chạy một lệnh tại mỗi thời điểm. |
| SIMT | Mô hình thực thi cho phép mỗi thread có luồng điều khiển riêng trong khi vẫn chạy như một phần của warp. |
| Warp divergence | Hiện tượng hai nhánh rẽ trong một warp bị chạy tuần tự, mỗi lượt một phần warp ngồi không. |
| Coalescing | Việc phần cứng gộp các truy cập bộ nhớ của một warp thành ít giao dịch nhất có thể. |
| Pinned memory | Bộ nhớ host bị ghim không cho page out, để DMA nhắm thẳng vào mà không cần trung chuyển. |
⚠️ Pitfall tổng hợp
❌ Coi thanh ghi thiết bị như biến thường → ✅ Dùng readl/writel với con trỏ __iomem; con trỏ trần thì trình biên dịch bỏ mất lần đọc thứ hai và vòng lặp chờ quay vô tận.
❌ Tin rằng ghi ra PCI xong là thiết bị đã nhận → ✅ Ghi là posted; đọc ngược lại một thanh ghi của chính thiết bị đó mới có bảo đảm.
❌ Coi polling luôn là lãng phí → ✅ Ở tần suất cao, interrupt gây livelock; NAPI cố ý chuyển sang polling, và driver độ trễ thấp chấp nhận đốt một lõi.
❌ Chạm buffer đang thuộc quyền thiết bị trong streaming DMA → ✅ Gỡ ánh xạ hoặc đồng bộ theo DMA API trước; lỗi loại này hỏng dữ liệu rời rạc chứ không crash, nên cực khó tái hiện.
❌ Benchmark read() mà không xoá page cache → ✅ Lần thứ hai gần như luôn trúng cache, và bạn đang đo RAM chứ không đo thiết bị.
❌ Dùng epoll edge-triggered mà đọc một lần rồi quay lại epoll_wait → ✅ Lặp read() tới khi gặp EAGAIN; đọc được ít hơn sức chứa không phải dấu hiệu đã cạn.
❌ Đăng ký file thường vào epoll để "cho khỏi chặn" → ✅ File thường luôn được coi là sẵn sàng; dùng thread riêng hoặc io_uring.
❌ Kỳ vọng zero-copy trong khi bật TLS → ✅ Mã hoá buộc dữ liệu ra user space, đường đi quay về đủ bốn bản sao. Zero-copy mất hẳn chứ không mất một phần.
❌ Nghĩ GPU chạy song song cả hai nhánh if → ✅ Thread cùng warp rẽ hai hướng thì hai nhánh chạy tuần tự; song song thật nằm giữa các warp.
❌ Đo tốc độ kernel GPU rồi kết luận về tốc độ pipeline → ✅ Đo trọn vòng gồm cả hai đầu chép; kernel 4 ms trong pipeline 40 ms nghĩa là 90% thời gian ở chỗ khác.
❌ Thêm thread hoặc thêm GPU khi nút thắt nằm ở tầng dưới → ✅ Bus và thiết bị có trần cứng; thêm worker chỉ tăng tranh chấp. Xác định tầng đang nghẽn trước khi thêm bất cứ thứ gì.
✅ Self-assessment
Bạn đã đạt module này nếu trả lời được:
- Giải thích được CPU điều khiển thiết bị qua thanh ghi thiết bị, memory-mapped I/O và bus PCIe — nếu chưa: bài 01 mục 2, 3 và 4.
- So sánh được polling, interrupt và DMA về việc ai tốn chu kỳ CPU khi dữ liệu di chuyển — nếu chưa: bài 02 mục 2 và 3.
- Lần theo được một lời gọi
read()qua syscall, VFS, page cache, block layer và driver tới thiết bị rồi quay về — nếu chưa: bài 03. - Chọn được mô hình chờ I/O cho server nhiều kết nối dựa trên số fd và chi phí syscall — nếu chưa: bài 04 mục 2 và 3.
- Giải thích được zero-copy loại bỏ những bản sao nào và giới hạn của nó — nếu chưa: bài 05 mục 2, 3 và 4.
- Giải thích được GPU đạt thông lượng cao nhờ đâu — SIMT, băng thông bộ nhớ, đánh đổi độ trễ — nếu chưa: bài 06.
- Chẩn đoán được vì sao một pipeline GPU nghẽn ở khâu chuyển dữ liệu thay vì ở phép tính — nếu chưa: bài 07 mục 4.
Chưa chắc mục nào thì cách kiểm tra thật nhất là lấy một service bạn đang chạy, chạy strace -T lên nó, và tự hỏi mỗi lời gọi đang dừng ở tầng nào trên tuyến đường của bài 03.
🚀 Tiếp theo
Hai module đầu trả lời dữ liệu nằm ở đâu và dữ liệu đi đường nào. Module cuối cùng của khoá đổi sang câu hỏi thực dụng nhất: làm sao biết máy đang nghẽn ở đâu. Bạn sẽ đọc top và htop cho đúng, phân biệt RSS với VSZ, đo I/O bằng iostat, hiểu cgroups chặn tài nguyên container thế nào, và đọc được một lần OOM kill từ dmesg.
Đó cũng là chỗ mọi thứ của hai module trước được đem ra dùng: khi iostat báo độ sâu hàng đợi cao, bạn sẽ biết nó ứng với chặng nào; khi một container bị giết, bạn sẽ biết trang nào đáng lẽ được thu hồi trước.
📚 Tài liệu mở rộng
- Bus-Independent Device Accesses — docs.kernel.org — chuẩn mực cho mọi truy cập MMIO trong kernel.
- Dynamic DMA mapping Guide — docs.kernel.org — coherent so với streaming DMA, IOMMU, bounce buffer.
- NAPI — docs.kernel.org — vì sao mạng tốc độ cao chuyển sang polling.
- Multi-Queue Block IO — docs.kernel.org — hai tầng hàng đợi và cơ chế tag.
- epoll(7) và io_uring_setup(2) — hai man page nên đọc nguyên văn.
- Ringing in a new asynchronous I/O API — LWN — bối cảnh lịch sử vì sao AIO cũ thất bại.
- CUDA C++ Best Practices Guide — chuyển dữ liệu host-device, pinned memory, gộp truy cập bộ nhớ.
Bài tiếp theo: Tổng quan module — Tài nguyên & đo lường
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