4 điểm bởi GN⁺ 2023-09-10 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đâ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ới switch/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_FAILURE thườ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

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:t của kill có nghĩa là L, l, t không nhận đối số, còn n, s cần đối số
    • getopt_long() được dùng trong mọi tiện ích để trả về tùy chọn tiếp theo
    • optind trỏ tới vị trí tham số tiếp theo trong mảng argv[]
    • optarg là con trỏ ký tự trỏ tới giá trị đối số của tùy chọn

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ặc xfts_open() tạo cấu trúc FTS từ 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ạo FTSENT, và quá trình đó chính là quá trình duyệt cây
    • Trường FTSENT->fts_info thườ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ủa dd hay cwrite() của split

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 trong system.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ẩn
    • PROGRAM_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ản
    • AUTHORS định nghĩa tác giả của tiện ích và được dùng khi kiểm tra phiên bản
    • emit_try_help() in gợi ý xem trợ giúp sau khi xuất lỗi
    • emit_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ệnh
    • initialize_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ác
    • atexit(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 boolean
    • do { ... } 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àm exit()error()
  • 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
    • join, sort, uniq từng dễ bị tấn công tràn số cho đến trước bản năm 2016
    • df được thêm tùy chọn --output vào năm 2013
    • yes được cải thiện hiệu năng nhờ buffering tốt hơn
  • 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ới arch, dir, vdir
  • Tiện ích thực thi độc lập ngắn nhất là true vớ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à ls vớ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 LISTF của CTSS khoảng năm 1963, sau này được rút ngắn thành ls
  • Cú pháp đặc biệt của dd gợi nhớ đến job control language của OS/360 đầu thập niên 1960
  • sort là tiện ích duy nhất tận dụng multithreading
  • fmt cho thấy tối ưu hóa dòng và đoạn bằng chi phí chức năng
  • yes triển khai xuất hiệu năng cao bằng bộ đệm bộ nhớ canh theo trang
  • df nhanh hơn vì dùng metadata của thiết bị, còn du phải kiểm tra mọi tệp
  • cksum có hai điểm vào: một cho chạy bình thường và một cho sinh bảng CRC-32
  • echo không có điều kiện thất bại
  • Thiết kế của testexpr khác đáng kể so với cấu trúc tiện ích thông thường
  • su ban đầu được bảo trì trong coreutils/shellutils

Những hiện thực đáng xem

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

 
GN⁺ 2023-09-10
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

    • Vấn đề là chính xác thì vai trò đó là gì. Tôi tự hỏi liệu có giới hạn nào đối với những việc có thể làm bằng các công cụ này không
      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á cho env trong 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
    • Có lẽ nên hiểu ý ở đây là “được tạo ra để được phát triển tích cực trong thời gian dà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
    • Có lẽ ý là “vòng đời phát triển dài”. Tức là gần như tiện ích viết một lần rồi dùng lâu dài, và đó cũng là ưu điểm của cách tiếp cận “làm một việc cho tốt”
    • Kỳ lạ là tôi lại hiểu “long life” là chương trình chạy liên tục. Tôi xem nó như đối lập với các công cụ có đầu vào rõ ràng, thời gian chạy ngắn, đầu ra rõ ràng
    • Emacs?
  • 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ên od của coreutils được cài thành god(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ư yes cũ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ào
    https://maizure.org/projects/decoded-gnu-coreutils/yes.html

    • Ý tôi là ngay cả một tiện ích đơn giản cũng đáng được viết tốt và tối ưu hóa
      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
    • Một bản triển khai bằng Go làm cho vui như sau:
      package main
      
      import "flag"
      
      func main() {  
      yes := flag.String("m", "y", "message")  
      flag.Parse()  
      for {  
      println(*yes)  
      }  
      }  
      
  • 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, factor
    Tự nhiên muốn thử chạy

    sudo find / -type f -exec shred {} \;  
    

    để 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

    • Tôi từng thử thứ tương tự bằng dd, nhưng nó chạy xong đến cuối và khá mất hứng
      Tôi đã hy vọng nó sẽ crash, hoặc ít nhất là mất kết nối, nhưng kernel, sshdbash vẫ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ồn
    • Trên Linux có lẽ khả năng cao là nó sẽ chạy đến cuối. Không thể ghi đè lên file thực thi và thư viện đang chạy hiện tại (ETXTBUSY), nên shred không thể phá hỏng chính nó hay find
  • Tôi thích việc /bin/true trê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

    • Một điểm thú vị khác về lệnh true là nó đã trở nên phức tạp hơn rất nhiều so với mức cần thiết
      Trước hết, một bài tập:
      touch mytrue  
      chmod u+x mytrue  
      ./mytrue  
      echo "error code for mytrue is $?"  
      
      true đã bắt đầu đúng theo nghĩa đen như vậy. Khá giống một công án Thiền
      Thay đổ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 true cuối cùng cũng có mã, và mã đó là exit 0
      Rồ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 GNU true ngà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
    • Không phải chuyện đó chỉ xảy ra khi chạy kèm tùy chọn sao? Nếu chỉ chạy true thì có vẻ không bị ảnh hưởng, trừ khi phần chú thích đang gây hiểu nhầm
  • Nế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?

    • Phần thông minh trong coreutils chủ yếu nằm ở cách tương tác hiệu quả với kernel, hơn là ở cấu trúc dữ liệu và thuật toán
      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ính
    • Có thể tôi hiểu sai, nhưng tôi không rõ cấu trúc dữ liệu và thuật toán liên quan thế nào đến các công cụ dòng lệnh
  • Có 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?

    • Không phải thông tin về cách sử dụng; trang này đi sâu vào việc chương trình hoạt động nội bộ như thế nào