Linux loại bỏ API `strncpy` sau 6 năm và hơn 360 bản vá
(phoronix.com/news)- Trong Linux 7.2, mọi chỗ dùng API
strncpybê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
strncpynữ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ỏ
strncpycầ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
strncpytrong 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
strncpytừ 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
strncpytheo 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úcstrscpy(): dùng cho đích có kết thúc NULstrscpy_pad(): dùng khi đích có kết thúc NUL và cần đệm thêm số 0strtomem_pad(): dùng cho trường độ rộng cố định không có kết thúc NULmemcpy_and_pad(): dùng cho sao chép có giới hạn với đệm rõ ràngmemcpy(): 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
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
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
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
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
Chuẩn Unicode nói rằng ngoài CR, LF, CRLF và các ký tự trên, vertical tab và form feed cũng phải được xử lý như ký tự ngắt dòng
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
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ý
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
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
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
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”
Đồng thời cũng là người duy nhất biết Unix epoch là gì
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
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
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
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ỉ 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
strncpydù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
strncpycó gì khó đến mức mất 6 nămLà 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