Giải mã GNU Coreutils (2018)
(maizure.org)- Đây là một dự án thuyết minh dài hạn đi qua cấu trúc mã nguồn 8.3 của GNU coreutils theo từng tiện ích, phù hợp với lập trình viên mới muốn học cách thiết kế công cụ dòng lệnh
- Đây không phải tài liệu hướng dẫn sử dụng mà là tài liệu hỗ trợ đọc mã nguồn, đồng thời bàn về namespace, luồng thực thi, quyết định thiết kế, thuật toán, cùng với phả hệ UNIX và Coreutils thời kỳ đầu của từng lệnh
- coreutils có nhiều chương trình C nhỏ với mục đích đơn nhất, nên cả những mẫu mã thô theo góc nhìn hiện đại như biến toàn cục, macro,
goto, hay hàm dài cũng cần được đọc trong bối cảnh lịch sử - Luồng chung lặp lại là khởi tạo, phân tích tùy chọn, xử lý đầu vào, thực hiện system call và xử lý lỗi;
getopt_long(),fts, cùng các helper của gnulib xuất hiện nhiều lần - Trước khi đóng góp hay chỉnh sửa, cần kiểm tra trước xem có thể tái hiện bằng công cụ hiện có hay không, liệu có phá vỡ tính tương thích ngược không, và mức độ lệch khỏi POSIX là bao nhiêu
Phạm vi của dự án thuyết minh GNU coreutils
- Đây là một dự án dài hạn giải thích mọi tiện ích trong GNU coreutils 8.3
- Độc giả mục tiêu là lập trình viên mới muốn tìm hiểu cách thiết kế tiện ích dòng lệnh
- Tài liệu này phù hợp nhất khi dùng như phần bối cảnh bổ trợ trong lúc đọc source code của tiện ích mà bạn quan tâm
- Đây không phải tài liệu hướng dẫn cho người dùng; cách sử dụng thực tế nên xem trong man pages
- Dự án gồm nhiều giai đoạn
- Phase 1: hoàn tất trang namespace và tổng quan thực thi cho từng tiện ích
- Phase 2: hoàn tất phần quyết định thiết kế và thuật toán chính, phả hệ UNIX và Coreutils thời kỳ đầu, walkthrough mã nguồn hữu ích hơn, cùng trực quan hóa quá trình tiến hóa của mã nguồn
- Phase indefinite: walkthrough mã theo từng dòng cho từng tiện ích vẫn là hạng mục kéo dài lâu dài
- Có cung cấp GitHub repo để thu thập ghi chú theo từng dòng
Các trang thuyết minh theo từng tiện ích
- Tên của mỗi lệnh liên kết đến trang chi tiết giải mã tiện ích tương ứng
- Mỗi trang bao gồm thảo luận, mã nguồn và walkthrough
- Những tiện ích in đậm là các mục được mở rộng trong Phase 2
- Danh sách bao gồm các lệnh GNU coreutils như
arch,base64,cat,chmod,cp,date,dd,df,ls,rm,sort,tail,tr,wc,yes
Những tiền đề cần biết trước khi đọc mã
- Nhiều tiện ích của coreutils có lịch sử gần 30 năm và là mã được nhiều người chỉnh sửa trong thời gian dài
- Phần lớn là các chương trình nhỏ được tối ưu cho một mục đích đơn nhất
- Thường là chương trình một tệp mã nguồn duy nhất
- Không phải loại mã được thiết kế để sống rất lâu hoặc mở rộng vượt xa vai trò ban đầu
- Có thể thấy các mẫu như biến toàn cục, macro,
goto, hoặc hàm dài vớiswitch/vòng lặp lồng nhau
- Kiến thức về POSIX rất quan trọng để hiểu mã
- Nên bắt đầu từ Utility Syntax Guidelines
- POSIX định nghĩa tính tương vận giữa đầu vào và đầu ra, còn cách thực hiện cụ thể được để cho phần hiện thực
- GNU coreutils có thể không tuân thủ POSIX một cách nghiêm ngặt, nhưng các khái niệm như bit quyền, uid/gid, biến môi trường và trạng thái thoát đều ăn sâu trong đó
- Vì vấn đề tính di động, coreutils phụ thuộc rất nhiều vào gnulib
- Gần như mọi tiện ích đều gồm các hàm gnulib được thiết kế để xử lý các vấn đề chung trên nhiều hệ thống
- coreutils được thực thi với giả định có sự hỗ trợ của shell như
bash,zsh,ksh- Shell chạy tiện ích bằng fork/clone, truyền tham số, thiết lập môi trường, redirect I/O bằng pipe và giữ lại giá trị thoát
- GNU coreutils ban đầu xuất phát từ ba gói: shell, text và file utilities; các tiện ích cùng loại thường chia sẻ nhiều mẫu thiết kế
Các mẫu thiết kế cơ bản
- Hầu hết tiện ích CLI có chung ba giai đoạn
- Giai đoạn thiết lập để chuẩn bị cờ, tùy chọn, bản địa hóa, v.v.
- Giai đoạn phân tích tham số để đọc đầu vào và xác định tham số thực thi
- Giai đoạn xử lý và thực thi để chuẩn bị đầu vào rồi chuyển cho một hay nhiều system call
- Trong quá trình chạy có thể kiểm tra ràng buộc ở nhiều điểm và thất bại
- Các trạng thái thoát khác nhau có thể gợi ý vị trí xảy ra vấn đề
EXIT_FAILUREthường được dùng như trạng thái lỗi chung
- Sau khi lỗi xảy ra, tiện ích sẽ cung cấp phản hồi cho người dùng
- Các biến thể tiện ích được chia thành ba nhóm
- Trivial utilities: định nghĩa vài dòng macro rồi include mã nguồn của tiện ích khác, dùng macro để ép luồng điều khiển cụ thể. Ví dụ:
arch,dir,vdir - Wrapper utilities: sau bước thiết lập và phân tích tùy chọn sẽ chuyển trực tiếp tùy chọn dòng lệnh thành tham số cho system call. Kết quả của system call chính là kết quả của tiện ích, hầu như không có xử lý riêng. Ví dụ:
link,whoami,hostid,logname - Full utilities: có đầy đủ thiết lập, phân tích tùy chọn/tham số, xử lý dữ liệu đầu vào và thực hiện nhiều system call; đa số tiện ích thuộc nhóm này
- Trivial utilities: định nghĩa vài dòng macro rồi include mã nguồn của tiện ích khác, dùng macro để ép luồng điều khiển cụ thể. Ví dụ:
Khởi tạo chung và phân tích tùy chọn
- Mọi tiện ích đều có một thủ tục khởi tạo ngắn ở đầu
main()initialize_main (&argc, &argv);set_program_name (argv[0]);setlocale (LC_ALL, "");bindtextdomain (PACKAGE, LOCALEDIR);textdomain (PACKAGE);atexit (close_stdout);
- Đoạn tiền xử lý này xử lý các việc quản trị như quốc tế hóa và đăng ký hành vi khi thoát, không ảnh hưởng đến hành vi cốt lõi của từng tiện ích
- Bộ công cụ Getopt giữ vai trò then chốt trong việc phân tích tùy chọn dòng lệnh
- Tùy chọn ngắn dùng tiền tố
-, tùy chọn dài dùng tiền tố-- - Tùy chọn ngắn được định nghĩa bằng chuỗi, còn tùy chọn dài dùng struct
- Trong chuỗi tùy chọn ngắn, nếu không có đối số thì chỉ ghi ký tự; đối số bắt buộc thêm
:, đối số tùy chọn thêm:: - Chuỗi tùy chọn ngắn
Lln:s:tcủakillcó nghĩa làL,l,tkhông nhận đối số, cònn,scần đối số getopt_long()được dùng trong mọi tiện ích để trả về tùy chọn tiếp theooptindtrỏ tới vị trí tham số tiếp theo trong mảngargv[]optarglà con trỏ ký tự trỏ tới giá trị đối số của tùy chọn
- Tùy chọn ngắn dùng tiền tố
Duyệt hệ thống tệp và helper cho system call
- Trên các hệ Unix, thư viện fts thường được hỗ trợ để xử lý việc duyệt hệ thống tệp dễ dàng hơn
fts_open()hoặcxfts_open()tạo cấu trúcFTStừ một đường dẫn- Các nút tệp hoặc thư mục trong cây được biểu diễn bằng cấu trúc
FTSENT - Gọi
fts_read()sẽ tạoFTSENT, và quá trình đó chính là quá trình duyệt cây - Trường
FTSENT->fts_infothường được dùng để mô tả mục và quyết định cách xử lý mục đó
- Ngoài những gì libc cung cấp, coreutils còn dùng rất nhiều wrapper system call và helper
- Trong xử lý ghi, ngoài wrapper của system call
write()và các hàm libc nhưfwrite(), còn có các hàm phi chuẩn nhưfull_write()- full_write() tiếp tục thử ghi lại trừ khi gặp lỗi cứng
- safe_write() thử lại system call
write()cả khi bị interrupt - Cũng có các helper ghi chỉ dùng trong một tiện ích như
iwrite()củaddhaycwrite()củasplit
Các hàm chung và thành ngữ mã nguồn
- Mọi tiện ích đều dùng ít nhất ba hàm:
main(),usage(),_() usage()hiển thị trợ giúp gồm tham số đầu vào, ý nghĩa và cú pháp đúng_()là macro được định nghĩa trongsystem.h, nối chuỗi đơn giản với tính năng Native Language Support của GNU gettext.h- Nếu là chuỗi hiển thị cho người dùng thì thường sẽ được bọc bằng macro này
- Trong đa số tiện ích không tầm thường, các dòng mã chung lặp lại nhiều lần
#include "system.h"định nghĩa macro phụ thuộc hệ thống, biến và các hàm tiện ích phi chuẩnPROGRAM_NAMEđịnh nghĩa tên chính thức của tiện ích và được dùng khi kiểm tra phiên bảnAUTHORSđịnh nghĩa tác giả của tiện ích và được dùng khi kiểm tra phiên bảnemit_try_help()in gợi ý xem trợ giúp sau khi xuất lỗiemit_ancillary_info (PROGRAM_NAME)in thông tin trợ giúp bổ sung dùng chung sau phần xuất riêng của từng lệnhinitialize_main(&argc, &argv)xử lý wildcard expansion tích hợp của VMS và bị loại bỏ trên hầu hết hệ điều hành khácatexit(close_stdout)đăng ký hàm flush và đóng stream đệm khi chương trình kết thúc
- Cũng xuất hiện các thành ngữ C có thể lạ với người mới
!!là toán tử NOT đơn ngôi kép, dùng để ép giá trị thành booleando { ... } while (0)là mẫu không-vòng-lặp để bọc macro nhiều câu lệnh, giúp việc token hóa sau thay thế tiền xử lý được an toàn
Cách bảo trì và kiểm tra trước khi đóng góp
- Một dự án đang hoạt động như coreutils thường tiến hóa theo ba luồng bảo trì chính
- Thay đổi toàn dự án là các thay đổi lớn ảnh hưởng đến cấu trúc và phụ thuộc của mọi tiện ích
- Năm 1995, Native Language Support được thêm vào thông qua gettext project của GNU, đưa macro
_()vào hầu hết các dòng xuất văn bản - Năm 1996, hỗ trợ quốc tế hóa được mở rộng và nhiều mã khởi tạo được thêm vào
main() - Năm 1995, phần xuất usage được bổ sung mô tả ngắn về mục đích của tiện ích
- Năm 2003, hỗ trợ wildcard của VMS được thêm vào, thể hiện qua hàm
initialize_main() - Năm 2016, để tránh cảnh báo trình biên dịch trên đường lỗi, macro
die()đã thay thế phần lớn các hàmexit()vàerror()
- Năm 1995, Native Language Support được thêm vào thông qua gettext project của GNU, đưa macro
- Thay đổi theo từng tiện ích được chia thành sửa lỗi, tính năng mới và tối ưu hóa
- Bảo trì hằng năm ít nhất bao gồm cập nhật năm bản quyền trên mọi tiện ích
- Cũng có những thay đổi hành chính như cập nhật địa chỉ FSF và không ảnh hưởng đến việc thực thi
- Người đóng góp nên kiểm tra trước GNU project page, contribution guidelines, rejected features, và mailing list archives
- Có ba câu hỏi nên kiểm tra trước khi viết mã
- Có thể tái hiện cùng chức năng bằng công cụ hiện có hay không
- Đóng góp đó có phá vỡ tính tương thích ngược hay không
- Hành vi được đề xuất có lệch quá xa POSIX hay không
- Nếu chưa chắc chắn, quy trình được khuyến nghị là hỏi cộng đồng qua mailing list
Một số chi tiết thú vị
- Tiện ích ngắn nhất là
false, dài 2 dòng, đồng hạng vớiarch,dir,vdir - Tiện ích thực thi độc lập ngắn nhất là
truevới 80 dòng; phiên bản đầu tiên gần như là một chương trình C tối giản - Tiện ích dài nhất là
lsvới 5308 dòng - Nhiều tiện ích truy ngược về tận Research UNIX của thập niên 1970, một số còn ngược đến Multics
- Tổ tiên tinh thần lâu đời nhất là lệnh
LISTFcủa CTSS khoảng năm 1963, sau này được rút ngắn thànhlsLISTFlà một trong 18 tiện ích gốc của bài báo thiết kế năm 1962
- Cú pháp đặc biệt của
ddgợi nhớ đến job control language của OS/360 đầu thập niên 1960 sortlà tiện ích duy nhất tận dụng multithreadingfmtcho thấy tối ưu hóa dòng và đoạn bằng chi phí chức năngyestriển khai xuất hiệu năng cao bằng bộ đệm bộ nhớ canh theo trangdfnhanh hơn vì dùng metadata của thiết bị, cònduphải kiểm tra mọi tệpcksumcó hai điểm vào: một cho chạy bình thường và một cho sinh bảng CRC-32echokhông có điều kiện thất bại- Thiết kế của
testvàexprkhác đáng kể so với cấu trúc tiện ích thông thường suban đầu được bảo trì trong coreutils/shellutils
Những hiện thực đáng xem
- shuf và shred dùng ISAAC cipher) cho số ngẫu nhiên
- shuf dùng Reservoir sampling
- sum dùng checksum System V và BSD kiểu cũ
- expr dùng đánh giá biểu thức kết hợp trái
- shred dùng Secure overwrite
- cksum tính CRC-32 checksum
- sort dùng biến thể của Merge sort
- factor là một tiện ích đáng xem về mặt hiện thực toán học
Hướng dẫn ủng hộ
- Hiện chưa thiết lập để nhận tài trợ cá nhân
- Nếu muốn chia sẻ thời gian hoặc tiền bạc, bạn có thể đóng góp cho Free Software Foundation
1 bình luận
Các ý kiến trên Hacker News
Vừa nói rằng “nhiều tiện ích trong số này đã gần 30 năm tuổi, và trong thời gian đó đã được nhiều người chỉnh sửa”, lại vừa nói “chúng không được thiết kế để mở rộng vượt quá tuổi thọ dài hay vai trò của mình”, nên muốn xem ví dụ về chương trình được thiết kế với tuổi thọ dài hạn trong đầu mà tác giả nghĩ tới
Tò mò không biết đó là những chương trình đã sống sót 30 năm, hay những chương trình được cho là sẽ tồn tại thêm 30 năm nữa
Tôi đã tạo một framework kiểm thử bằng bash và coreutils để kiểm thử một ngôn ngữ lập trình nhỏ tự viết; ban đầu tôi thấy hơi lấn cấn vì không dùng một ngôn ngữ “đúng nghĩa”, nhưng thực tế nó hoạt động rất tốt và còn chạy song song được
Thứ duy nhất không kiểm thử được là
argv[0]của chương trình; dù kết hợp thế nào cũng không thể tạo chính xác hành vi mong muốn, nên tôi đã gửi yêu cầu tính năng và bản vá choenvtrong coreutils: https://lists.gnu.org/archive/html/coreutils/2023-08/msg0006...Có vẻ nó sẽ được chấp nhận, tức là một tính năng mới được đưa vào một chương trình lâu đời
Những thực hành mà tác giả liệt kê thường bị phê phán là làm giảm khả năng bảo trì khi quy mô tăng lên trong các chương trình lớn
Một số tài liệu đáng tham khảo:
Cách GNU coreutils kiểm thử: https://www.pixelbeat.org/docs/coreutils-testing.html
Khám phá từng lệnh trong coreutils: https://ratfactor.com/slackware/pkgblog/coreutils
Xử lý văn bản dòng lệnh với GNU Coreutils: https://learnbyexample.github.io/cli_text_processing_coreuti... — ebook của tôi bao quát hơn 20 công cụ xử lý văn bản
Các bài liên quan:
Decoded: GNU Coreutils (2018) - https://news.ycombinator.com/item?id=29871037 - tháng 1/2022, 7 bình luận
Decoded: GNU coreutils (2019) - https://news.ycombinator.com/item?id=26411908 - tháng 3/2021, 38 bình luận
Decoded: GNU Coreutils - https://news.ycombinator.com/item?id=20328650 - tháng 7/2019, 55 bình luận
Một sự thật thú vị: nếu cài coreutils bằng Homebrew trên macOS, vì macOS đã có sẵn
od(1)nênodcủa coreutils được cài thànhgod(1)Nếu tác giả đang đọc thì có ít nhất một lỗi. Mô tả ngắn trên trang shred[0] thực ra là mô tả của
csplit[1], và nên là đại loại “ghi đè để che giấu nội dung tệp và tùy chọn xóa tệp”[0]: https://maizure.org/projects/decoded-gnu-coreutils/shred.htm...
[1]: https://maizure.org/projects/decoded-gnu-coreutils/csplit.ht...
Tôi không biết có thứ như thế này, hay thật. Ngay cả thứ đơn giản như
yescũng khá thú vị để xem mã nền tảng của một tiện ích ghi ra stdout được viết như thế nàohttps://maizure.org/projects/decoded-gnu-coreutils/yes.html
Chẳng hạn, thật hay khi có một ví dụ tối giản để học cách ghi đầu ra thật nhanh. Chương trình ngắn đến mức số dòng không quan trọng lắm, và nếu tác giả biết cách thì theo tôi tốt hơn là làm nó nhanh nhất có thể để không trở thành nút cản
Danh sách các tiện ích cơ bản khá thú vị. Dù đã dùng UNIX từ rất lâu, tôi cũng chưa từng nghe đến những thứ như
shred,shuf,factorTự nhiên muốn thử chạy
để xem nó đi được bao xa trước khi tự giết chính mình. Tất nhiên là trên máy ảo hoặc một thiết bị có thể flash lại dễ dàng
dd, nhưng nó chạy xong đến cuối và khá mất hứngTôi đã hy vọng nó sẽ crash, hoặc ít nhất là mất kết nối, nhưng kernel,
sshdvàbashvẫn còn trong bộ nhớ nên tôi quay lại một prompt mà chẳng thể làm gì cho ra hồnETXTBUSY), nênshredkhông thể phá hỏng chính nó hayfindTôi thích việc
/bin/truetrên thực tế có thể thất bại và trả về false. Về mặt kỹ thuật, gọi “Not/bin/false” sẽ vững chắc hơn: https://github.com/coreutils/coreutils/blob/master/src/true....Dĩ nhiên tôi biết đó là trường hợp gần như không xảy ra, nhưng vẫn buồn cười
truelà nó đã trở nên phức tạp hơn rất nhiều so với mức cần thiếtTrước hết, một bài tập:
trueđã bắt đầu đúng theo nghĩa đen như vậy. Khá giống một công án ThiềnThay đổi đầu tiên là vì pháp lý. Mọi đoạn mã đều phải có thông báo bản quyền, và file rỗng cũng cần có. Vì vậy một file không có gì đã trở thành một file chỉ có thông báo bản quyền, khiến người ta đặt câu hỏi: “có thể đặt bản quyền cho sự không có gì không?” AT&T đã thử
Sau đó có người nói rằng chương trình phải được định nghĩa rõ ràng và không nên dựa vào hành vi ngẫu nhiên của Unix, điều này vào thời điểm đó khá hợp lý. Vì vậy
truecuối cùng cũng có mã, và mã đó làexit 0Rồi lại có người nói rằng các tiện ích hệ thống nên được viết bằng C thay vì shell để nhanh hơn, và OpenBSD vẫn còn một ví dụ hay về dạng đó: http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr....
Đến một lúc nào đó, bộ máy quan liêu của GNU can thiệp và nói rằng mọi chương trình phải hỗ trợ cờ
-h, rồi sau đó mọi chương trình phải hỗ trợ locale, nên GNUtruengày nay đáng ngạc nhiên là dài 80 dòng: https://github.com/coreutils/coreutils/blob/master/src/true....Cũng có thể xem là ổn, nhưng với một chương trình mà theo định nghĩa là “không làm gì và thành công” thì như vậy là nhiều mã
http://trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
truethì có vẻ không bị ảnh hưởng, trừ khi phần chú thích đang gây hiểu nhầmNếu là lập trình viên mới muốn rèn cảm giác về cách áp dụng cấu trúc dữ liệu và thuật toán một cách hữu ích, có phần nào ở đây đặc biệt đáng xem không?
Ví dụ như dùng
copy_file_range()thay vìread()/write()để tránh sao chép dữ liệu sang không gian người dùng. Nó gần với kỹ nghệ phần mềm hơn là khoa học máy tínhCó thể tôi đã bỏ lỡ ý chính của trang này, nhưng chẳng phải đã có trang man hoặc trang info cho từng tiện ích rồi sao?