1 điểm bởi GN⁺ 3 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong Linux 7.2, mọi chỗ dùng API strncpy bên trong kernel đã biến mất, và giao diện sao chép chuỗi vốn đã được lên kế hoạch loại bỏ từ lâu nay đã bị gỡ bỏ hoàn toàn
  • strncpy() sao chép theo số byte được chỉ định, nhưng cách hoạt động của kết thúc NUL không trực quan, nên trong nhiều năm đã là nguyên nhân gây lỗi trong kernel
  • Đặc tính tự động điền 0 vào bộ đệm đích một cách không cần thiết còn gây ra vấn đề hiệu năng, và phải mất khoảng 6 năm cùng 362 commit để loại bỏ hoàn toàn
  • Trong đợt merge hôm thứ Sáu, không chỉ phần thân API mà cả triển khai theo từng kiến trúc cho per-CPU cuối cùng cũng bị xóa
  • Mã kernel giờ đây phải chọn các hàm thay thế như strscpy(), strscpy_pad(), strtomem_pad(), memcpy_and_pad(), memcpy() tùy theo mục đích sử dụng

strncpy biến mất trong Linux 7.2

  • Linux 7.2 đã chính thức loại bỏ API strncpy, vốn từ lâu đã được đánh dấu sẽ bị loại bỏ trong kernel
  • Sau 6 năm dọn dẹp, giờ đây không còn đoạn mã nào trong kernel nội bộ sử dụng giao diện strncpy nữa
  • Thay đổi này không chỉ là thay thế một hàm đơn lẻ, mà gần như là quá trình loại bỏ thói quen sao chép chuỗi cũ trên toàn bộ kernel

Quy mô công việc để loại bỏ

  • Việc loại bỏ strncpy cần khoảng 362 commit
  • Công việc được tiến hành theo cách từng bước loại bỏ mã sử dụng strncpy trong kernel
  • Với Linux 7.2, quá trình dọn dẹp này đã đi đến điểm hoàn tất

Vì sao strncpy gây vấn đề trong kernel

  • strncpy từ nhiều năm nay bị xem là nguồn gây lỗi kéo dài trong Linux kernel
  • Cụ thể, có hai hành vi là vấn đề chính
    • Ý nghĩa và cách hoạt động của kết thúc NUL không trực quan, khiến người dùng dễ mắc lỗi
    • Việc lấp đầy bộ đệm đích bằng số 0 một cách dư thừa tạo ra chi phí hiệu năng không cần thiết

Đợt merge loại bỏ thực tế

  • Đợt merge hôm thứ Sáu đã loại bỏ API strncpy
  • Trong cùng đợt merge đó, triển khai strncpy theo từng kiến trúc cho per-CPU cuối cùng cũng biến mất

API thay thế để dùng trong mã kernel

  • Thay vì strncpy, cần chọn hàm phù hợp với đối tượng sao chép và điều kiện kết thúc
    • strscpy(): dùng cho đích có kết thúc NUL
    • strscpy_pad(): dùng khi đích có kết thúc NUL và cần đệm thêm số 0
    • strtomem_pad(): dùng cho trường độ rộng cố định không có kết thúc NUL
    • memcpy_and_pad(): dùng cho sao chép có giới hạn với đệm rõ ràng
    • memcpy(): dùng cho sao chép bộ nhớ khi đã biết độ dài

1 bình luận

 
Ý kiến Hacker News
  • Trước đây người ta hay chọc rằng các lập trình viên kernel Linux ở đẳng cấp C hàng đầu thế giới mà lại không biết tạo kiểu stringbuffer hay stringview, nhưng hồi đó chưa phải thời kỳ có đồng thuận như bây giờ về chủ đề này, nên cũng phần nào dễ hiểu
    Người đã sớm nhìn ra hướng đi đúng là Dennis Ritchie, và ông đã đề xuất kiểu fat pointer cho C vào năm 1990. Nếu nó được đưa vào C99 thì hẳn sẽ là một bổ sung hoàn hảo, và nếu ủy ban chấp nhận thì thế giới có lẽ đã khá khác
    Đến năm 2007 lại có cơ hội thứ hai với bài viết “C's greatest mistake” của Walter Bright; về bản chất nó là cùng một ý tưởng với Ritchie, tức slice/stringview, nhưng được giải thích rõ ràng hơn, vậy mà cuối cùng vẫn không vào được C11. Giờ đã đến C23 mà vẫn chưa có, thay vào đó chúng ta có _Generic và VLA, đúng kiểu cảm giác thôi thì cứ mở tiệc ăn mừng vậy

    • Bài viết năm 2007 của Walter Bright ở đây: https://digitalmars.com/articles/C-biggest-mistake.html
      Khi tìm kiếm tôi còn thấy một bài Reddit về cùng chủ đề, và màn tranh cãi kiểu bike-shedding khá buồn cười: https://www.reddit.com/r/C_Programming/comments/90uq7c/cs_bi...
      Tôi vẫn thắc mắc vì sao hành vi mảng C suy biến thành con trỏ lại được thiết kế như vậy. Có lời giải thích rằng mục tiêu là để mã B có thể biên dịch sang C với thay đổi tối thiểu; trong B thì khai báo mảng thực chất sẽ định nghĩa cả con trỏ lẫn mảng, rồi khởi tạo con trỏ đó trỏ tới phần tử đầu tiên của mảng
    • VLA đã bị hạ xuống thành tính năng tùy chọn trong C11, và tôi thấy đó là điều tốt
      Vấn đề lớn hơn bây giờ là thư viện chuẩn C vẫn bị mắc kẹt ở thời K&R, và ngay cả các tính năng ngôn ngữ được thêm từ C99 như truyền hoặc trả về struct cũng chưa được phản ánh vào API của thư viện chuẩn. Chỉ cần thư viện chuẩn có struct phạm vi dạng cặp con trỏ/kích thước cùng các hàm chuỗi mới hoặc được cập nhật để dùng nó thì tình hình cũng có thể khá hơn nhiều
    • Link đề xuất của Ritchie: https://web.archive.org/web/20150611114358/https://www.bell-...
    • Đây đúng là kiểu mẫu gây khó chịu nhất trong làm việc nhóm. Có các lời giải A, B, C, mỗi cái đều có ưu và nhược điểm, mọi người tranh luận suốt 2 tuần, rồi cuối cùng không chọn gì cả
    • Điều đó chỉ cho thấy ưu tiên của WG14 nằm ở đâu
  • strncpy trong kernel Linux được nói là đã là “nguồn bug dai dẳng” suốt nhiều năm vì ngữ nghĩa phản trực giác, cách xử lý kết thúc NUL, và vấn đề hiệu năng do đổ đầy số 0 vào vùng đích một cách không cần thiết
    Mỗi lần được nhờ review mã C, tôi đều tìm strncpy trước, và lúc nào cũng phát hiện bug ở đó

  • Có những thứ đã làm tôi khó chịu suốt 40 năm. Chuỗi kết thúc bằng NUL, và giờ còn bao gồm cả những chuỗi không phải UTF-8 trong vào/ra nữa
    Cả tập quán xử lý kết thúc dòng bằng LF, CR, CRLF, cũng như cách phân tách trường bằng dấu gạch đứng hay dấu phẩy nữa. Nếu người ta dùng các ký tự ASCII không mơ hồ như GS, FS, RS thì việc mã hóa/giải mã kết thúc dòng sẽ trở thành vấn đề của I/O, còn HT/VT/CR/LF/FF có thể chỉ đơn thuần ở lại trong phần mã liên quan đến xuất ra

    • Tôi từng làm một dự án chuyển đổi dữ liệu được đóng khung bằng các ký tự phân tách trường/bản ghi của ASCII, và nó thật sự rất dễ xử lý
      Những phiền toái bẩn thỉu của xử lý escape vốn hay xuất hiện với dữ liệu phân tách bằng dấu phẩy biến mất, nên mọi thứ đơn giản hơn nhiều
    • Unicode giờ còn có nhiều lựa chọn hơn nữa. Có NL Next line trông như đến từ EBCDIC, rồi LS Line separator và PS Paragraph separator do Unicode tạo ra
      Chuẩn Unicode nói rằng ngoài CR, LF, CRLF và các ký tự trên, vertical tabform feed cũng phải được xử lý như ký tự ngắt dòng
    • UTF-8 hoạt động hoàn toàn ổn trên đầu vào/đầu ra chuẩn. Tất nhiên, ý là nếu không phải Windows, hệ vẫn đang mắc kẹt ở đầu thập niên 90 về mã hóa văn bản quốc tế
      Các kiểu kết thúc dòng như LF, CR, CRLF cũng là tập quán của hệ điều hành, và sẽ tốt hơn nếu ngôn ngữ lập trình đừng cố “đoán” đâu là kết thúc dòng đúng. Việc đó tạo ra nhiều vấn đề hơn là giải quyết, và nói lại lần nữa, đây phần lớn là vấn đề rất riêng của Windows mà Microsoft cần phải đưa Windows vào đúng thế kỷ này
    • LF là lựa chọn hợp lý nhất, nhưng nếu là tệp văn bản thì bên nào cũng được. Vấn đề là CSV không phải văn bản
      Lần gần nhất tôi phải xử lý tệp CSV trong bash, tôi đã chuyển nội bộ nó sang RS và FS để xử lý
    • Tôi nghĩ cứ dùng UTF-8 ở mọi nơi là được
  • Thay vì strncpy, trong mã kernel Linux người ta nói nên dùng strscpy() cho đích kết thúc NUL, strscpy_pad() cho đích kết thúc NUL cần đệm 0, strtomem_pad() cho trường độ rộng cố định không kết thúc NUL, memcpy_and_pad() cho sao chép theo biên có đệm rõ ràng, và memcpy() cho sao chép bộ nhớ khi đã biết độ dài
    Nghe như cơn ác mộng, tôi không hiểu sao mọi thứ lại phải phức tạp đến vậy

    • Lý do là hiệu năng. Một hàm đa năng an toàn xử lý hết phần lớn các trường hợp này tất yếu sẽ chậm hơn vì có nhánh điều kiện bên trong, và việc chọn hàm nào cũng thể hiện chủ đích của lập trình viên
      Khi đọc mã, chỉ nhìn vào lựa chọn hàm mà đã thấy rõ ý đồ của người viết thì theo tôi là tốt hơn
    • Vốn dĩ dùng strncpy cho đúng từ trước đến nay đã luôn phức tạp rồi
    • Ít nhất cũng không thể đặt tên khá hơn một chút sao
  • Chính những công việc lặp đi lặp lại buồn tẻ như thế này mới là nơi công việc thực sự của kỹ nghệ hệ thống diễn ra
    Những dự án hạ tầng lớn kiểu như làm cho kernel Linux đáng tin cậy hơn trong khi vẫn giữ được khả năng dùng thực tế xuyên suốt toàn bộ quá trình không vận hành theo thang vài tháng, mà là hàng chục năm

    • Tôi hiểu vì sao nó lại kéo dài đến quy mô hàng chục năm. Cái đuôi dài của người dùng và các phụ thuộc thật sự rất dài
      Nhưng tôi không chắc ở tốc độ đó liệu có thể tạo ra được tiến bộ dài hạn đủ ý nghĩa hay không. Đây không hẳn là lời phàn nàn, mà gần giống như một nghịch lý của hạ tầng cốt lõi hơn
  • Đây là một công việc vừa vĩ đại vừa khiến người ta phải khiêm tốn. Thật đáng kinh ngạc khi có nhiều người đã đóng góp đến vậy
    Những “tính năng mới ngầu” thì dễ được ghi nhận công lao hơn, nhưng với một nền tảng mang tính căn bản như kernel, việc loại bỏ các tính năng tồi đôi khi còn quan trọng hơn
    Nếu 50 năm nữa con người quên luôn cách đọc mã nguồn, còn đống tàn dư Claude/Codex lặng lẽ chất chồng lên nhau và đốt phần lớn năng lượng của Trái Đất, thì những việc như thế này có lẽ sẽ được truyền lại như huyền thoại của “thời kỳ khai sáng”

    • Làm tôi nhớ đến A Deepness in the Sky của Vernor Vinge. Trong đó có người bảo trì tàu vũ trụ bằng khảo cổ học phần mềm
      Đồng thời cũng là người duy nhất biết Unix epoch là gì
    • Tôi không nghĩ 50 năm nữa mọi người sẽ quên hết cách hiểu mã nguồn đâu. Khao khát muốn biết mọi thứ vận hành ra sao của con người khi đó vẫn sẽ còn
    • Tôi cho rằng mớ mã tạp nham do AI tạo ra sẽ trở nên không thể kiểm soát từ rất lâu trước đó
  • Tôi nghĩ chuỗi kết thúc bằng 0 là sai lầm lớn nhất trong lịch sử điện toán. Chuỗi kiểu Pascal an toàn hơn nhiều

    • Cũng có những phương án trung gian như BSTR mà Visual Basic, rồi sau này là COM, đã chọn
      Nó vẫn là con trỏ tới một mảng ký tự kết thúc bằng 0, nhưng ngay trước byte đầu tiên mà con trỏ trỏ tới có một trường độ dài. Với giả định không có NUL nhúng bên trong, nó vẫn tương thích với chuỗi C, còn các hàm kiểu BSTR thì có thể tận dụng giá trị độ dài
    • Tôi đồng ý ở mức nào đó, nhưng hẳn đã có tranh cãi về kiểu dữ liệu của trường kích thước. Nếu không phải độ dài biến thiên thì còn dễ gây tranh cãi hơn, mà nếu là biến thiên thì lại phát sinh vấn đề khác
      Có thời kỳ ngay cả 16-bit cũng có thể bị xem là quá dư, còn bây giờ 32-bit lại có thể trông quá nhỏ. C là ngôn ngữ “kiểu mạnh”, nhưng ở đúng những chỗ quan trọng thì lại khá lỏng lẻo
    • Chuỗi kết thúc bằng 0 là nền tảng của vô số phần mềm hữu ích. Gọi đó là sai lầm lớn nhất của điện toán thì hơi cường điệu
      Tôi đã hơn 30 năm không viết mã Pascal, nhưng vẫn mang máng nhớ là ngay thời đó tôi cũng thấy hệ thống chuỗi của nó quá khó dùng
    • Chẳng phải 255 ký tự là đủ cho tất cả mọi người rồi sao?
    • Tệ ngang với việc dòng chỉ kết thúc bằng ký tự xuống dòng
  • Chỉ vì không có kiểu dữ liệu chuỗi mà phải chịu quá nhiều đau khổ và công sức vá víu

    • Chính xác hơn thì không phải vì không có kiểu dữ liệu chuỗi, mà là vì nỗi đau và công sức phát sinh khi phải lách quanh thực tế rằng C không có kiểu dữ liệu chuỗi
    • Nếu muốn đưa kiểu mạnh vào đây thì có thể làm theo cách nào? Có lẽ cũng phải tái cấu trúc quy mô lớn để phần mã quanh strncpy dùng kiểu và các hàm đó chăng?
  • Tôi tò mò không biết việc viết lại chỗ dùng strncpy có gì khó đến mức mất 6 năm
    Là vì phạm vi sử dụng quá rộng, hay đây là kiểu công việc dài hạn chỉ thay khi tiện đụng vào cùng file, hoặc còn có khó khăn nào khác nữa?

  • Tôi từng phải xử lý mã dùng chuỗi đệm bằng khoảng trắng trong ứng dụng Win32. Chuỗi đích được đệm bằng khoảng trắng nhưng byte cuối vẫn là ký tự nul
    Khi làm các thao tác như tính độ dài hay sao chép thì phải dùng phiên bản hàm chuỗi riêng. Tôi không rõ vì sao lại như vậy, nhưng codebase đó quá cũ nên cũng có thể bắt nguồn từ cách các cấu trúc Pascal hoạt động

    • Có thể là chuỗi lấy từ trường char trong cơ sở dữ liệu SQL, vì trường char sẽ được đệm bằng khoảng trắng chứ không như varchar
    • Tôi nghĩ gốc rễ của hành vi này không phải Pascal mà là COBOL
    • Cũng có thể để tránh phải cấp phát lại khi kích thước chuỗi thay đổi, hoặc vì căn chỉnh theo dòng cache của CPU