Vì sao kernel Linux đã dành 20 năm để bạn khỏi phải viết Driver
Lệnh /sys/class/gpio mà gần như mọi tài liệu embedded Linux vẫn đang dạy đã bị kernel khai tử từ 2016. Không phải lỗi của ai - đó là dấu vết của một cuộc rút lui có chủ đích: đẩy driver ra khỏi kernel, biến phần cứng từ code thành data.

Tối thứ năm tuần trước, mình ngồi xem lại đống tài liệu Embedded Linux của mình, tới phần device driver. Tới phần demo, mình cắm con Raspberry Pi vào, định chạy lại đúng cái lệnh mà mình đã gõ cả trăm lần để bật một chân GPIO từ user space:
echo 53 > /sys/class/gpio/export
Và terminal trả về một dòng mà mình đã đọc lướt qua không biết bao nhiêu lần nhưng chưa bao giờ thực sự dừng lại:
This ABI is deprecated and will be removed after 2020.
Cái interface mà mình vẫn quen tay gõ ra để minh hoạ "đây là cách điều khiển GPIO từ user space" - cái mà gần như mọi tài liệu Embedded Linux đều dạy - hoá ra đã bị chính kernel Linux cho nghỉ hưu [2]. Deprecated từ kernel 4.8, tức là năm 2016. Tám năm trước.
Phản ứng đầu tiên của mình là tò mò. Phản ứng thứ hai, sau khi pha xong ấm trà và ngồi đào tiếp, còn thú vị hơn nhiều. Vì khi mình lần theo lý do tại sao kernel lại deprecated cái interface đó, mình nhận ra nó không phải một thay đổi lặt vặt. Nó là một mảnh của một câu chuyện lớn hơn nhiều - một câu chuyện mà gần như mọi tutorial "viết Linux device driver" trên internet đang kể ngược.
Đây là luận điểm của bài này, và mình tin nó khá chắc sau một tuần đào: cách điển hình mà ai cũng học chủ đề này là học viết một kernel device driver, nhưng phần lớn lịch sử 20 năm gần đây của kernel Linux lại là một cuộc rút lui có chủ đích khỏi việc đó. Kernel community đã, một cách kiên trì và có hệ thống, làm hai việc: đẩy độ phức tạp của driver ra khỏi kernel, và biến phần "phần cứng nào dùng driver nào" từ code bạn viết thành data kernel đọc.
Cho nên câu hỏi đúng cho một kỹ sư embedded năm 2026 không phải là "viết kernel driver thế nào?". Mà là: "làm sao để mình không phải viết một cái?". Và khi thật sự không tránh được, "làm sao viết càng ít code kernel càng tốt?".
Để thấy được điều đó, mình sẽ đi qua đúng những khái niệm quen thuộc của chủ đề này - chỉ là theo một thứ tự khác, và với câu hỏi "tại sao" đặt lên trước câu hỏi "thế nào". Bắt đầu từ chỗ mọi thứ khởi nguồn: cái lý do mà driver tồn tại.
Driver tồn tại để nói dối bạn một cách tử tế
Định nghĩa quen thuộc nói driver là "người phiên dịch" giữa user space và phần cứng. Đúng, nhưng mình muốn nói thẳng hơn: driver tồn tại để duy trì một lời nói dối. Lời nói dối đó là "phần cứng cũng chỉ là một file".
Khi bạn gõ cat /dev/ttyUSB0, bạn đang đọc một con chip UART - một mạch điện vật lý dịch tín hiệu nối tiếp. Nhưng bạn không thấy mạch điện. Bạn thấy một thứ hành xử y hệt như mở một file ra đọc. Đó là toàn bộ phép màu của Unix: biến mọi loại phần cứng quái dị nhất thành cùng một interface quen thuộc - open, read, write, close.
Và lời nói dối này có một quy tắc sắt: ứng dụng không bao giờ được chạm thẳng vào phần cứng. Mọi thứ phải đi qua một chuỗi:
User Program --(function call)--> C Library
|
(system call)
v
System Call Interface (ranh giới user / kernel)
|
v
Device Driver
|
v
Hardware

Tại sao lại bắt buộc phải vòng vèo như vậy, thay vì để app ghi thẳng vào địa chỉ thanh ghi của thiết bị cho nhanh? Ba lý do, và cả ba đều là lý do tồn tại của cả ngành OS:
- An toàn. Nếu app nào cũng ghi thẳng vào phần cứng, một con trỏ sai trong một app rác có thể làm hỏng đĩa, treo card mạng, hoặc kéo sập cả máy. Kernel đứng giữa làm trọng tài.
- Trừu tượng hoá. Có hàng nghìn loại card mạng. Nếu mỗi app phải tự biết cách nói chuyện với từng loại, không ai viết nổi phần mềm. Driver giấu sự khác biệt đó đi, chỉ chừa lại một interface chung.
- Đa nhiệm. Mười process cùng muốn ghi ra một cái đĩa. Ai đó phải xếp hàng, phải khóa, phải đảm bảo chúng không giẫm lên nhau. Đó là việc của kernel, không phải của app.
Giữ cái khung này trong đầu, vì toàn bộ phần còn lại của bài là câu chuyện về việc kernel community dần dần nhận ra: cái lớp "Device Driver" trong sơ đồ kia càng mỏng càng tốt. Mỗi dòng code nằm trong kernel space là một dòng code có quyền làm sập cả hệ thống. Đó là chỗ đắt nhất, nguy hiểm nhất để đặt một con bug.
Ba loại thiết bị, và cái ngoại lệ tiết lộ tất cả
Phần cứng thường được chia thành ba nhóm driver. Bảng này thì đúng và cần nhớ:
| Loại driver | Thiết bị | Đặc điểm I/O |
|---|---|---|
| Character | sensor, bàn phím, chuột, cổng serial | không đệm, theo từng byte/luồng |
| Block | ổ cứng, flash, SSD | có đệm, theo khối (block) |
| Network | Ethernet, Bluetooth, Wi-Fi | qua socket, không có node trong /dev |
Character device là dạng "luồng": bạn đọc byte ra theo thứ tự, như uống nước qua ống hút. Block device là dạng "kho có ngăn": kernel đệm dữ liệu lại, đọc/ghi theo từng khối, sắp xếp lại thứ tự để tối ưu cho phần cứng quay tròn hoặc flash. Phân biệt này không phải học thuộc cho có - nó quyết định cả cách bạn viết driver lẫn cách app nói chuyện với thiết bị.

Nhưng cái mình muốn bạn để ý là dòng thứ ba. Network device không có node trong /dev. Không có /dev/eth0. Bạn không cat /dev/eth0 được. Thay vào đó bạn nói chuyện với nó qua socket, qua ip, qua ifconfig.
Tại sao? Vì cái lời nói dối "mọi thứ là file" vỡ ở đây. Một file là một thứ bạn đọc tuần tự, từ đầu tới cuối. Một card mạng thì không như vậy: dữ liệu đến theo từng gói tin rời rạc, có địa chỉ, có thứ tự không đảm bảo, có thể mất. Ép network vào mô hình file sẽ là một lời nói dối tệ - nên kernel không ép. Nó dựng một abstraction khác (socket) cho đúng bản chất của thứ nó mô tả.
Đây là bài học ẩn đầu tiên: abstraction tốt nhất là cái khớp với bản chất của thứ nó mô tả, không phải cái thống nhất bằng mọi giá. Giữ ý này lại, vì lát nữa khi nói tới Device Tree, bạn sẽ thấy đúng triết lý đó lặp lại ở một tầng khác.
Module: để bạn khỏi build lại cả kernel chỉ vì một con chip
Trước khi nói tới việc viết driver, phải hiểu driver sống ở đâu. Và đây là chỗ Loadable Kernel Module (LKM) bước vào.
Hình dung kernel như một toà nhà đang chạy 24/7, không được tắt. LKM cho phép bạn lắp thêm một căn phòng vào toà nhà đó trong lúc nó vẫn đang hoạt động, rồi tháo ra khi không cần - không cần đập đi xây lại, không cần reboot. Bạn nạp một driver mới bằng insmod, dùng xong gỡ bằng rmmod, và kernel vẫn chạy liên tục.
Đây không phải tiện lợi vặt. Nó là lý do Linux chạy được trên mọi thứ từ router 32MB RAM tới server 2TB. Một kernel image không thể chứa sẵn driver cho mọi con chip trên đời - nó sẽ phình to vô tận. Thay vào đó kernel giữ phần lõi nhỏ gọn, rồi nạp đúng driver cho đúng phần cứng đang có, lúc cần.
Bạn có thể tắt LKM (build mọi thứ thẳng vào kernel) - nhẹ hơn, an toàn hơn một chút vì không cho phép nạp code lạ lúc runtime. Nhưng cái giá là: mỗi lần đổi driver, build lại toàn bộ kernel. Với embedded thì đôi khi người ta chọn vậy để khoá cứng hệ thống. Với phần lớn trường hợp còn lại, module thắng.
Còn build module thì sao? Cái Makefile huyền thoại mà ai học driver cũng từng copy-paste mà không hiểu:
make -C $KDIR \
ARCH=arm \
CROSS_COMPILE=arm-linux-gnueabihf- \
M=$(PWD) \
modules
Dịch sang tiếng người: -C $KDIR bảo make "chạy với hệ thống build của kernel nằm ở thư mục này", M=$(PWD) bảo "nhưng code module thì ở thư mục hiện tại của tôi". ARCH=arm và CROSS_COMPILE=... là vì bạn đang ngồi trên máy x86 build code cho con ARM - cross-compile. Module có thể nằm in-tree (trong cây mã nguồn kernel) hay out-of-tree (ngoài cây, như ở đây), nhưng dù ở đâu nó cũng cần kernel headers để biết các struct và macro của đúng phiên bản kernel sẽ nạp nó.
insmod vs modprobe: thủ công và thông minh
Nhóm lệnh quản module chia làm hai tầng tư duy, và sự khác nhau giữa chúng tiết lộ một điều về cách kernel nghĩ:
Tầng thủ công: - insmod <đường-dẫn>.ko - nạp đúng một file module. Nếu module này cần module khác mà module kia chưa nạp, insmod báo lỗi và bỏ cuộc. Nó không tự đi tìm. - lsmod - liệt kê module đang nạp và chuỗi phụ thuộc. - rmmod - gỡ module. Khi gỡ, hàm cleanup (đăng ký qua module_exit()) được gọi để trả lại memory, thiết bị, interrupt đã mượn.
Tầng thông minh: - modprobe <tên-module> - nạp module và tự kéo theo mọi phụ thuộc. Bạn chỉ cần tên, nó tự lo phần còn lại. - depmod - sinh ra file modules.dep, tấm bản đồ phụ thuộc mà modprobe đọc. - modinfo - xem metadata của module: tác giả, license, và các tham số nó nhận.

Cặp insmod vs modprobe chính là phiên bản thu nhỏ của toàn bộ luận điểm bài này. insmod bắt bạn tự biết và tự giải quyết mọi phụ thuộc. modprobe đẩy việc đó cho hệ thống - bạn khai báo cái bạn muốn, hệ thống lo cách đạt được. Lát nữa bạn sẽ thấy đúng cú lật này lặp lại ở udev, ở Device Tree, ở alloc_chrdev_region. Linux liên tục dịch chuyển từ "kỹ sư tự lo" sang "khai báo ý định, để hệ thống lo".
Khi bạn viết driver: kernel đã âm thầm gỡ mìn cho bạn nhiều năm
Giờ giả sử bạn buộc phải viết một character driver thật. Driver kết nối yêu cầu từ hệ thống file tới hàm của nó qua struct file_operations:
static struct file_operations fops = {
.open = mydev_open,
.read = mydev_read,
.write = mydev_write,
.release = mydev_release,
};
Khi app gọi read() trên /dev/mydev, kernel lần theo bảng này tới mydev_read. Đẹp và gọn. Nhưng để mình chỉ cho bạn một footgun mà kernel đã gỡ bỏ, vì nó là evidence trực tiếp cho luận điểm.
Có một câu hỏi WHY rất hay: vì sao dùng unlocked_ioctl thay cho ioctl cũ?
Câu trả lời ngắn: ioctl đời cũ tự động giữ một thứ gọi là Big Kernel Lock (BKL) - một cái khóa toàn cục khóa gần như cả kernel lại mỗi khi nó chạy. Trên máy một nhân thì không sao. Nhưng khi CPU có 8, 16, 64 nhân, cái khóa toàn cục đó biến thành nút thắt cổ chai: tất cả các nhân khác phải đứng chờ một nhân làm ioctl xong. unlocked_ioctl bỏ cái khóa tự động đó đi, giao quyền (và trách nhiệm) khóa lại cho chính driver - driver chỉ khóa đúng phần dữ liệu của nó, các nhân khác vẫn chạy.
Và đây là chỗ mình đào thêm. BKL không phải một chi tiết nhỏ - nó là một trong những cuộc dọn dẹp dai dẳng nhất lịch sử kernel. Nó bị xoá hoàn toàn khỏi kernel ở phiên bản 2.6.39, năm 2011, qua một loạt patch của Arnd Bergmann [3] [4]. Cả một thập kỷ để gỡ một cái khóa.
Bằng chứng (độ tin cậy: cao). Big Kernel Lock bị xoá hoàn toàn khỏi kernel Linux ở phiên bản 2.6.39 (2011); ioctl giữ BKL toàn cục là lý do unlocked_ioctl ra đời
LWN, "The real BKL end game": chặng cuối của việc xoá Big Kernel Lock, hoàn tất ở 2.6.39 qua patch của Arnd Bergmann. [3]
Kernel Newbies xác nhận BKL bị xoá ở 2.6.39, phần còn lại thay bằng fine-grained locking. [4]
Tại sao mình kể chuyện này? Vì nó là một ví dụ hoàn hảo cho cách kernel đối xử với code driver: mỗi footgun, mỗi cái khóa thô, mỗi API dễ dùng sai đều bị gỡ dần đi, để driver bạn viết ngày càng nhỏ và càng khó sai. Bạn viết unlocked_ioctl năm 2026 và không bao giờ phải biết tới BKL - đó là vì ai đó đã dành 10 năm đào nó ra khỏi đường đi của bạn.
Cú lật tương tự nằm ở device number. Kernel nhận diện driver qua cặp major (loại thiết bị) và minor (instance cụ thể) number. Để xin một major, bạn có hai lựa chọn:
register_chrdev_region()- xin một major tĩnh, số cố định bạn tự chọn. Vấn đề: số bạn chọn có thể đụng major của driver khác. Bạn phải tự biết số nào còn trống trên mọi hệ thống driver sẽ chạy. Quay lại đúng tư duyinsmod: tự lo.alloc_chrdev_region()- xin major động, để kernel chọn một số còn trống. Đây là cách được khuyến nghị. Bạn không cần biết số nào trống, bạn khai báo "cho tôi một major", kernel lo. Đúng tư duymodprobe.
Lại đúng cái pattern đó: tĩnh vs động, thủ công vs khai báo. Cùng một triết lý, lặp lại ở mọi tầng.
Cái node trong /dev tới từ đâu, và vì sao embedded lại khác
Bạn có major number rồi, nhưng cái file /dev/mydev để app mở thì ai tạo? Ngày xưa người ta tạo tay bằng mknod. Giờ nó được tạo động bởi udev: kernel báo "có thiết bị mới tên X, major Y", udev nghe được và tạo node tương ứng trong /dev. Kernel cũng phơi bày toàn bộ cây thiết bị ra /sys (sysfs) và /proc/devices để cả người lẫn chương trình soi vào được.
Nhưng udev đi kèm systemd và khá nặng. Trên một con thiết bị embedded 64MB RAM, nó là gánh nặng vô lý. Nên thế giới embedded có các lựa chọn nhẹ hơn:
mdev(đi kèm BusyBox) - nhẹ, luật đơn giản, hợp với hệ tối giản.eudev- bản udev tách rời, không cần systemd.- Static
/devnodes - tạo sẵn node lúc build, không cần daemon nào chạy runtime cả.
Đây lại là một lát cắt của cùng câu chuyện: desktop Linux chọn sự tiện lợi tự động (udev + systemd), embedded chọn sự gọn nhẹ và kiểm soát. Không có lựa chọn nào "đúng" tuyệt đối - nó phụ thuộc bạn đang đứng ở đâu trên trục tài nguyên.
Soi xem hệ thống đang chạy driver gì
Trước khi nói chuyện ghép driver, một câu thực dụng: làm sao bạn nhìn thấy driver nào đang chạy trên một máy lạ? Ba cửa sổ chính:
cat /proc/devices- liệt kê mọi character và block driver đang nạp, kèm major number của chúng.ip link(hoặcifconfigđời cũ) - liệt kê thiết bị mạng, vì như đã nói, chúng không hiện trong/dev./sys/devices,/sys/class,/sys/block- đi bộ qua sysfs để thấy đúng góc nhìn của kernel: thiết bị nào thuộc lớp driver nào, gắn vào bus nào, quan hệ cha-con ra sao.
Đây là phản xạ debug đầu tiên khi cắm phần cứng mới vào mà "không thấy gì": soi /proc/devices và /sys/class trước khi nghĩ tới chuyện viết code.
Làm sao kernel biết con chip này dùng driver nào?
Câu hỏi tưởng nhỏ này thực ra là trái tim của cả việc quản driver. Khi bạn cắm một thiết bị USB vào, làm sao hệ thống biết nạp đúng driver?
Câu trả lời là một chuỗi đẹp: mỗi thiết bị có một modalias - một chuỗi mô tả định danh của nó (kiểu "tôi là thiết bị của hãng X, sản phẩm Y"). Mỗi driver, qua macro MODULE_DEVICE_TABLE(), xuất bảng các thiết bị nó hỗ trợ vào metadata của module. udev đọc modalias của thiết bị vừa cắm, tra xem driver nào khai là hỗ trợ nó, rồi gọi modprobe nạp đúng driver đó. Bạn kiểm tra được bằng udevadm info.
Và đây lại là tư duy khai báo. Driver không nói "hãy nạp tôi". Nó nói "đây là danh sách thiết bị tôi biết cách xử lý" - và để hệ thống tự ghép. Giữ ý này thật chặt, vì nó dẫn thẳng tới phần hay nhất của chủ đề: Device Tree.
Cuộc rút lui thứ nhất: đẩy driver ra khỏi kernel
Giờ quay lại cái lệnh đã làm mình khựng lại lúc đầu bài.
Cách dạy phổ biến là với GPIO, LED, I2C, SPI, bạn thường không cần viết driver kernel - bạn điều khiển được trực tiếp từ user space qua các generic driver có sẵn:
- GPIO/LED: qua sysfs.
echo 53 > /sys/class/gpio/exportđể "xuất" một chân ra, rồi đọc/ghi giá trị. LED thìecho timer > triggerđể cho nó tự nhấp nháy. - I2C: mở
/dev/i2c-0, đặt địa chỉ slave bằngioctl(f, I2C_SLAVE, addr), rồi đọc/ghi. - SPI: giao tiếp full-duplex qua node generic như
/dev/spidev1.0.
Ý chính đó đúng, và nó còn quan trọng hơn vẻ ngoài đơn giản của nó: với một lượng lớn phần cứng đơn giản, bạn viết code C bình thường trong user space, không đụng một dòng kernel nào. Bug trong code user space của bạn? App crash, bạn debug lại. Bug trong driver kernel? Kernel panic, cả máy chết. Sự khác biệt về cái giá của sai lầm là khổng lồ. Đẩy được việc gì ra user space là đẩy được rủi ro ra một chỗ mà sai lầm rẻ hơn nhiều.
Nhưng đây là chỗ thú vị nhất, và cũng là cái làm mình khựng lại tối hôm đó - một chi tiết mà phần lớn tài liệu vẫn chưa kịp cập nhật.
Bằng chứng (độ tin cậy: cao). Interface /sys/class/gpio đã bị kernel deprecated từ phiên bản 4.8 (2016) và đánh dấu sẽ xoá; cách 'đúng' hiện nay là GPIO character device /dev/gpiochipN qua libgpiod
Tài liệu chính thức của kernel: "This ABI is deprecated and will be removed after 2020. It is replaced with the GPIO character device." ABI đã bị chuyển sangDocumentation/ABI/obsolete/. [2]
Deprecated từ kernel 4.8 (2016). Char device/dev/gpiochipNđảm bảo giải phóng tài nguyên khi đóng file descriptor, hỗ trợ event polling tin cậy, đọc/ghi nhiều line cùng lúc. [5]

Cái /sys/class/gpio/export mà gần như mọi tài liệu vẫn đang dạy thực ra đã lỗi thời về mặt thiết kế từ 2016 [2]. Nó vẫn chạy được trên nhiều kernel (vì xoá một ABI mất rất nhiều năm để không làm vỡ phần mềm cũ), nhưng kernel community đã thay nó bằng một thứ tốt hơn: GPIO character device /dev/gpiochipN, điều khiển qua thư viện libgpiod [6].
Và đây mới là chỗ thú vị - tại sao lại thay? Cái interface sysfs cũ có một lỗi thiết kế kinh điển: nó dùng global numberspace (chân số 53 là chân nào? Tùy hệ thống, số có thể đổi giữa các lần boot), và nếu process của bạn crash giữa chừng, chân GPIO bạn export ra vẫn nằm đó, không ai dọn. Char device mới sửa cả hai: bạn mở thiết bị, mọi tài nguyên gắn với file descriptor, đóng fd là kernel tự dọn sạch [5]. Nó còn cho phép đọc nhiều chân cùng lúc, bắt sự kiện đáng tin cậy - những thứ sysfs cũ làm rất tệ.
Lại là cái mẫu hình quen thuộc. Ngay cả khi đã đẩy GPIO ra user space, kernel community vẫn tiếp tục tinh chỉnh để cái interface user space đó sạch hơn, an toàn hơn, ít footgun hơn. Cuộc rút lui khỏi kernel không dừng ở việc "ra được user space là xong". Nó là một quá trình liên tục làm cho cái phần bạn-phải-tự-viết ngày càng nhỏ và càng khó sai.
Sẽ có người phản biện, và phản biện này mạnh: "Lý thuyết hay đấy, nhưng phần cứng thật cần driver kernel thật: interrupt, DMA, latency thấp, throughput cao. Không thể bit-bang mọi thứ từ user space được"
Hoàn toàn đúng, và mình không cãi. Một card mạng 10Gbps, một bộ điều khiển DMA, một thiết bị cần xử lý interrupt trong vài microsecond - những thứ đó phải có driver kernel. Bạn không thể đẩy đường dữ liệu nóng (hot path) ra user space rồi mong nó đua được với phần cứng.
Nhưng để ý mình đang nói gì. Mình không nói "đừng bao giờ viết kernel driver". Mình nói cái default đã lật ngược. Mười lăm năm trước, phản xạ mặc định khi gặp một con chip mới là viết driver kernel. Bây giờ, phản xạ mặc định nên là: thử user space trước (libgpiod, spidev, i2c-dev, industrial I/O), và chỉ đi vào kernel khi bạn chứng minh được user space không đủ - vì latency, vì interrupt, vì throughput.
Gánh nặng chứng minh đã đổi chiều. Ngày xưa bạn phải biện minh cho việc không viết kernel driver. Giờ bạn phải biện minh cho việc viết một cái. Đó mới là điểm.
Một chi tiết nữa đáng kéo ra, vì nó lại đúng tinh thần cả bài: khi bạn buộc phải viết driver kernel, kernel cho bạn cả một thực đơn cách phơi giao diện ra user space - sysfs (ưu tiên), rồi tới ioctl, mmap, debugfs, hay netlink. Và cái được ưu tiên, sysfs, lại chính là cái giống data nhất: bạn phơi thiết bị ra thành các file thuộc tính đọc/ghi được, thay vì bắt user space gọi những lệnh ioctl tù mù. Ngay cả lựa chọn giao diện, kernel cũng nghiêng về "mô tả như data" hơn là "ra lệnh như code".
Khi bạn thật sự cần viết, khung tối thiểu trông như thế này - và để ý nó nhỏ tới mức nào:
#include <linux/module.h>
#include <linux/init.h>
static int __init mydrv_init(void)
{
pr_info("mydrv: init\n");
return 0;
}
static void __exit mydrv_exit(void)
{
pr_info("mydrv: exit\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Minimal driver skeleton");
Vài chi tiết đáng dừng lại: __init và __exit đánh dấu code chỉ chạy lúc nạp/gỡ - sau khi init xong, kernel thu hồi lại bộ nhớ của hàm init để khỏi phí. module_init/module_exit đăng ký điểm vào/ra. Và MODULE_LICENSE("GPL") không phải thủ tục cho vui: nếu thiếu hoặc khai license không tương thích, kernel sẽ đánh dấu "tainted" và khoá không cho module bạn dùng nhiều API nội bộ. Kernel theo dõi rất nghiêm chuyện code chạy trong nó tuân theo GPL hay không.
Cuộc rút lui thứ hai: biến phần cứng từ code thành data
Phần này, theo mình, là đỉnh của cả chủ đề - dù thường chỉ được nhắc tới vài dòng ở cuối. Để hiểu Device Tree đẹp tới mức nào, phải biết thế giới trước nó tệ tới mức nào.
Quay về khoảng năm 2011. Linux trên ARM là một mớ hỗn loạn. Mỗi hãng làm một con SoC, mỗi board lại viết một file C riêng (gọi là "board file") mô tả: con chip này nằm ở địa chỉ nào, ngắt số mấy, GPIO ra sao. Hàng nghìn file C như vậy, mỗi file là code phải compile vào kernel, mỗi board mới là thêm code mới. Không ai maintain nổi.
Linus Torvalds nổi đoá. Câu của ông đã thành huyền thoại:
"Gah. Guys, this whole ARM thing is a f*cking pain in the ass."
Và quan trọng hơn câu chửi, là cái ông nói tiếp trên mailing list: "we should not be adding any mindless board drivers for ARM ... They are extra code that actually have negative worth" - những board file đó là code có giá trị âm [7] [8]. Ông từ chối nhận thêm board file mới, ép cả ngành công nghiệp ARM chuyển sang một thứ khác [9].
Thứ khác đó là Device Tree.

Để dễ hình dung, nghĩ tới một công ty giao hàng. Cách cũ: với mỗi khu phố mới, bạn viết hẳn một cuốn sổ tay riêng cho tài xế - "tới ngã tư rẽ trái, nhà thứ ba sơn xanh". Mười nghìn khu phố là mười nghìn cuốn sổ phải in lại, sửa lại, mang theo. Cách mới: tài xế chỉ cần một cái app đọc địa chỉ - dữ liệu thuần tuý - và một logic giao hàng chung cho mọi nơi.
Về mặt kỹ thuật, board file là cuốn sổ tay viết tay cho từng board: code C compile thẳng vào kernel. Device Tree là cái địa chỉ: một file .dts mô tả phần cứng dưới dạng data, kernel đọc lúc boot. Driver giờ là logic chung, không gắn cứng vào board nào.
Hệ quả là thêm một board mới không còn là thêm code vào kernel - nó là viết một file mô tả. Cùng một kernel image chạy được trên hàng trăm board khác nhau, chỉ cần nạp đúng Device Tree. Đó là lý do một bản Linux ARM ngày nay boot được trên vô số thiết bị mà không cần recompile.
Cơ chế ghép nối, ở phía driver, gọn gàng đến bất ngờ. Driver khai báo các thiết bị nó hỗ trợ qua chuỗi compatible:
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
static const struct of_device_id mydrv_of_match[] = {
{ .compatible = "vendor,mydevice" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);
Rồi gắn bảng đó vào driver:
static struct platform_driver mydrv = {
.driver = {
.name = "mydrv",
.of_match_table = OF_MATCH_PTR(mydrv_of_match),
},
.probe = mydrv_probe,
.remove = mydrv_remove,
};
module_platform_driver(mydrv);
Nhìn qua lăng kính cả bài, driver nói "tôi tương thích với phần cứng tên vendor,mydevice". File Device Tree của board nói "ở địa chỉ này có một thiết bị compatible = vendor,mydevice". Kernel đọc cả hai lúc boot, thấy chuỗi khớp, và tự ghép driver với thiết bị, gọi hàm probe [10]. Không ai phải viết code "nạp driver X cho board Y". Sự ghép nối là data, không phải code.
Đây chính xác là cú lật insmod → modprobe, nhưng ở tầng cao nhất: bạn khai báo ý định (tôi hỗ trợ thiết bị này), hệ thống lo cơ chế (tìm và ghép). Và nhớ cái bài học từ phần network device không? Abstraction tốt là cái khớp với bản chất. Phần cứng vốn dĩ là một mô tả tĩnh - con chip ở đâu, ngắt số mấy. Mô tả tĩnh thì nên là data, không nên là code chạy được. Device Tree chỉ đơn giản là nhận ra điều đó.
(Hai chi tiết kỹ thuật cho đủ: OF_MATCH_PTR(x) trả về x nếu kernel bật CONFIG_OF - tức có hỗ trợ Device Tree - và trả về NULL nếu không, để cùng một code biên dịch được trên cả hệ có lẫn không có DT. Khi SoC quá cũ không hỗ trợ DT, có cơ chế dự phòng gọi là Platform Data: vẫn dùng struct C mô tả phần cứng, đúng kiểu cũ. module_platform_driver() chỉ là macro gộp việc đăng ký/hủy đăng ký, thay cho việc tự viết init/exit.)
Vậy rốt cuộc nên đọc chủ đề này thế nào?
Nếu sau tất cả bạn chỉ nhớ được một thứ, mình muốn đó là cái này: độ phức tạp nên nằm ở chỗ mà sai lầm rẻ nhất và thay đổi dễ nhất.

Cả chủ đề device driver, đọc theo trình tự đó, là một câu chuyện về việc kernel Linux liên tục di chuyển độ phức tạp tới đúng chỗ của nó:
- Phần cứng đơn giản (GPIO, I2C, SPI)? Đẩy ra user space - nơi một con bug chỉ làm crash app, không sập máy. Và kể cả ở đó, vẫn tiếp tục mài giũa (sysfs → char device → libgpiod) cho an toàn hơn.
- Mô tả phần cứng (board nào, chip ở đâu)? Biến thành data (Device Tree) - nơi thêm một board là viết một file, không phải compile thêm code có "giá trị âm".
- Phần buộc phải ở trong kernel? Gỡ từng footgun (BKL → unlocked_ioctl, major tĩnh → động) để code bạn viết ngày càng nhỏ và khó sai.
Cái mình thấy đẹp, và cũng là cái mình muốn bạn mang theo, là nó không chỉ đúng với kernel. Nó là một nguyên tắc thiết kế chung - giống hệt cái ý mình từng viết khi mổ xẻ vì sao con switch Ethernet nhanh chính vì nó "ngu": độ phức tạp đặt sai chỗ luôn đắt hơn bạn tưởng. Mỗi khi bạn định nhét logic vào cái lớp nguy hiểm nhất, đắt nhất, khó sửa nhất của hệ thống - dù đó là kernel, là database trigger, là một cái service mà cả công ty phụ thuộc - hãy dừng lại và hỏi: thứ này có thật sự phải ở đây không, hay mình đang đặt nó ở đây chỉ vì đó là phản xạ?
Tối đó mình sửa lại bản demo: bỏ echo > /sys/class/gpio/export, thay bằng gpioset của libgpiod. Một dòng nhỏ. Nhưng mình thấy vui vì cuối cùng cũng hiểu vì sao nó nên đổi, chứ không chỉ là "tài liệu mới bảo thế".
Còn câu hỏi mình vẫn chưa trả lời xong, và sẽ mang theo vài tuần tới: cái default "thử user space trước" này đúng tới đâu thì gãy? Ở ngưỡng latency nào, throughput nào, thì việc rút khỏi kernel không còn trả giá nổi? Mình có vài con số trong đầu nhưng chưa đủ chắc để viết ra đây. Có thể là một bài khác.
Nếu bạn từng viết kernel driver thật cho phần cứng nặng - hoặc ngược lại, từng cố đẩy một thứ ra user space rồi phải rút lui vào kernel vì không kịp - mình rất muốn nghe. Đặc biệt nếu bạn nghĩ mình đang sai ở chỗ nào. Đó là loại phản biện mình học được nhiều nhất.
Bình
[1]
Tài liệu Embedded Linux - Device Driver Basics
[2]
GPIO Sysfs Interface for Userspace (Documentation/gpio/sysfs.txt), kernel.org - Tài liệu chính thức của kernel: 'This ABI is deprecated and will be removed after 2020. It is replaced with the GPIO character device.' ABI đã bị chuyển sang Documentation/ABI/obsolete/sysfs-gpio. Đây chính là interface /sys/class/gpio/export mà gần như mọi tài liệu vẫn đang dạy.
[3]
The real BKL end game, LWN.net, 2011 - Ghi lại chặng cuối của quá trình xoá Big Kernel Lock (BKL). BKL bị xoá hoàn toàn ở kernel 2.6.39 (2011) qua patch của Arnd Bergmann. Đây là lý do unlocked_ioctl ra đời: ioctl cũ giữ BKL toàn cục.
[4]
BigKernelLock, Linux Kernel Newbies - Xác nhận BKL bị xoá ở kernel 2.6.39, phần còn lại được thay bằng khoá mịn hơn (fine-grained locking).
[5]
Stop using /sys/class/gpio - it's deprecated, The Good Penguin - Giải thích sysfs GPIO deprecated từ kernel 4.8 (2016), thay bằng GPIO character device /dev/gpiochipN. Char device đảm bảo giải phóng tài nguyên khi đóng file descriptor, hỗ trợ event polling tin cậy, đọc/ghi nhiều line cùng lúc.
[6]
libgpiod documentation, kernel.org / libgpiod project - Thư viện C low-level cùng binding ngôn ngữ cao và tools (gpioget/gpioset/gpiomon) thay thế legacy GPIO sysfs interface. Là cách 'đúng' để điều khiển GPIO từ user space trên kernel hiện đại.
[7]
Linus Torvalds, Re: [GIT PULL] omap changes for v2.6.39 merge window, Linux Kernel Mailing List, 2011-03-17 - Email gốc của Linus: 'we should not be adding any mindless board drivers for ARM ... They are extra code that actually have negative worth.' Đây là phát súng khởi đầu cho device tree trên ARM.
[8]
Linux on ARM breakthrough to take away Torvalds' arse pain, The Register, 2012-10-10 - Ghi lại câu nói nổi tiếng của Linus Torvalds năm 2011 về ARM: 'this whole ARM thing is a f*cking pain in the ass', và việc ông từ chối nhận thêm board file mới, ép các hãng ARM chuyển sang device tree.
[9]
ARM kernel consolidation, LWN.net, 2011 - Phân tích cuộc dọn dẹp của ARM kernel năm 2011, dẫn tới việc áp dụng flattened device tree thay cho board files viết tay.
[10]
Platform devices and device trees, LWN.net, 2011 - Giải thích cơ chế platform_driver, of_match_table, và cách kernel ghép node Device Tree với driver qua chuỗi compatible.