14.3.3. Vệ sinh hệ thống tệp¶
Bộ nhớ flash và thẻ SD trên một camera đã xuất xưởng sẽ dần đầy với các tệp mà không có nhà vận hành nào dọn dẹp thủ công. Hai quyết định về bộ nhớ này sẽ gắn liền với sản phẩm suốt vòng đời: bề mặt nào lưu loại dữ liệu nào, và cấu trúc thư mục như thế nào để các thao tác với tệp vẫn hoạt động khi ứng dụng tích lũy dữ liệu theo thời gian.
14.3.3.1. Nơi lưu trữ dữ liệu¶
Mã nguồn và tài nguyên được đặt trong các frozen module và ROMFS mà bản dựng cố định tại thời điểm xuất xưởng. Trạng thái của ứng dụng -- bất cứ thứ gì ứng dụng ghi lúc runtime, bất cứ thứ gì tăng dần, bất cứ thứ gì thay đổi giữa các lần khởi động -- phải lưu ở chỗ khác. Camera cung cấp hai bề mặt ghi được cho mục đích này:
Bộ nhớ flash nội bộ tại
/flash: một filesystem ghi được nhỏ, được mount trước khi bất kỳ mã ứng dụng nào chạy. Phù hợp cho các bản ghi kích thước cố định nhỏ tồn tại qua các lần khởi động lại: cấu hình mà ứng dụng cập nhật lúc runtime, hiệu chỉnh gần nhất, bộ đếm cuộn, tệp đánh dấu một dòng ghi "camera này đã được cấp phép." Chu kỳ ghi có hạn -- flash nội bộ hiện đại chịu được hàng nghìn đến hàng chục nghìn lần ghi mỗi sector, không phải hàng triệu lần, vì vậy các lần ghi cần không thường xuyên, không phải mỗi khung hình.Thẻ SD tại
/sdcard: một filesystem ghi được lớn hơn, được mount khi có thẻ. Phù hợp cho các tệp biến động cồng kềnh: ảnh và video chụp được, tệp nhật ký, dữ liệu tinh chỉnh mô hình (ML), bất cứ thứ gì có thể tăng đến megabyte hoặc gigabyte. Dung lượng ghi lớn hơn flash nội bộ nhưng vẫn có giới hạn; có thể tháo rời, thay thế, và đây là bề mặt có khả năng biến mất cao nhất khi ứng dụng đang ghi dở.
Câu trả lời đúng cho nơi ghi dữ liệu hầu như luôn là "flash cho các bản ghi cố định nhỏ, SD cho mọi thứ còn lại." Hai loại này không thể hoán đổi: một ứng dụng ghi tệp nhật ký cuộn của nó vào /flash sẽ làm cạn kiệt độ bền ghi của flash trong một quá trình triển khai mà nếu dùng SD sẽ ổn thỏa.
14.3.3.2. Xem cả hai như có thể lỗi¶
/flash và /sdcard đều có thể bị lỗi. Thẻ SD có thể bị rút ra, flash có thể bị hỏng do mất điện giữa chừng khi đang ghi, bất kỳ cái nào cũng có thể hết dung lượng, và bất kỳ thao tác nào trên cả hai đều có thể ném ra OSError vì những lý do mà ứng dụng sẽ không có cơ hội chẩn đoán ngoài thực địa.
Hai mẫu thiết kế giúp ứng dụng vượt qua điều đó:
Bọc các thao tác mount và thao tác tệp trong khối try. Mọi
open(),os.listdir(),os.rename()trên các đường dẫn dữ liệu người dùng đều có khả năng thất bại. BắtOSError, ghi nhật ký, và chuyển về phương án thay thế đã định sẵn -- ghi vào/flashnếu/sdcardkhông có, bỏ qua thao tác nếu cả hai đều không khả dụng.Ghi nguyên tử cho các tệp phải tồn tại qua mất điện. Ghi vào đường dẫn tạm, đóng handle, rồi dùng
os.rename()để đặt đè lên tên thực. Hoặc là rename thành công và tệp là phiên bản mới, hoặc là không thành công và tệp là phiên bản cũ. Không có trạng thái thứ ba nơi tệp bị ghi dở:import os def write_config_atomic(path, contents): tmp = path + '.tmp' with open(tmp, 'w') as f: f.write(contents) f.flush() os.rename(tmp, path)
Mẫu này hoạt động trên cả flash và SD. Nó không hoạt động cho các tệp đủ lớn mà tệp tạm chiếm hết dung lượng trống của filesystem; hãy giữ nó cho các bản ghi nhỏ.
14.3.3.3. Bẫy thư mục chậm¶
MicroPython VFS không lập chỉ mục nội dung thư mục theo cách một filesystem máy tính để bàn làm. os.listdir() và os.stat() duyệt bảng tệp bên dưới theo tuyến tính. Một thư mục với một trăm tệp vẫn ổn; một thư mục với mười nghìn tệp sẽ chậm không dùng được, với mỗi os.listdir() mất vài giây và mỗi open() phải kiểm tra qua bảng trên đường đi.
Các ứng dụng ghi nhật ký hoặc ảnh chụp vào đĩa sẽ gặp vấn đề này nhanh nhất. Một sơ đồ /sdcard/logs/<timestamp>.log ngây thơ mở một tệp mới mỗi phút sẽ lấp đầy thư mục logs/ với nửa triệu tệp trong một năm triển khai. Trước đó rất lâu, ứng dụng sẽ bắt đầu bỏ lỡ tốc độ khung hình vì mỗi lần mở tệp mất lâu hơn một khoảng thời gian của khung hình.
Mẫu đúng là phân tán các tệp qua một cây các thư mục con theo ngày để không có thư mục đơn lẻ nào chứa quá vài trăm mục:
import os
import time
LOG_ROOT = '/sdcard/logs'
def log_path(now=None):
if now is None:
now = time.localtime()
year, month, day, hour = now[0], now[1], now[2], now[3]
directory = '{}/{:04d}/{:02d}/{:02d}'.format(
LOG_ROOT, year, month, day)
_makedirs(directory)
return '{}/{:02d}.log'.format(directory, hour)
def _makedirs(path):
# os.makedirs equivalent -- create each level if missing
parts = path.split('/')
for i in range(2, len(parts) + 1):
sub = '/'.join(parts[:i])
try:
os.mkdir(sub)
except OSError:
pass
Một năm ghi một tệp mỗi giờ giờ đây được phân tán qua 365 thư mục theo ngày, mỗi thư mục chứa tối đa 24 tệp; os.listdir() trên bất kỳ thư mục nào vẫn nhanh, và vòng lặp khung hình của ứng dụng không bị đình trệ vì các thao tác tệp khi quá trình triển khai lão hóa theo thời gian.
Nguyên tắc tương tự áp dụng cho các ảnh chụp, dấu vết cảm biến, hoặc bất cứ thứ gì khác mà ứng dụng ghi một tệp mỗi sự kiện. Nếu tốc độ sự kiện cao, cây cần sâu hơn (năm/tháng/ngày/giờ, hoặc năm/tháng/ngày/giờ/phút) để mỗi thư mục lá luôn nhỏ. Nếu tốc độ sự kiện thấp, một cây năm/tháng là đủ.
14.3.3.4. Đường dẫn theo thiết bị¶
Trong một đội camera nhiều hơn một chiếc, các tệp nhật ký cần xác định thiết bị vật lý nào tạo ra chúng. machine.unique_id() trả về một định danh phần cứng được ghi vào camera tại nhà máy; nó có cùng giá trị qua các lần khởi động lại, qua các lần cập nhật firmware, và qua các lần đổi thẻ SD. Nhúng nó vào đường dẫn nhật ký hoặc vào bản ghi nhật ký và một nhà vận hành nhìn vào một đống thẻ SD hoặc nhật ký tập trung có thể biết cái nào là của máy nào:
import binascii
import machine
UNIT_ID = binascii.hexlify(machine.unique_id()).decode()
LOG_ROOT = '/sdcard/logs/' + UNIT_ID
Kết hợp với mẫu thư mục con theo ngày, bố cục trở thành /sdcard/logs/<unit-id>/2026/06/09/14.log -- một giờ bản ghi của một thiết bị, trong một thư mục đủ nông để duyệt, trên một đường dẫn đặt tên thiết bị ngay trên hệ thống tệp.
14.3.3.5. Tổng hợp lại¶
Bộ nhớ ghi được của một camera đã xuất xưởng trông đại khái như sau:
/flash-- cấu hình, hiệu chỉnh, một dấu cấp phép. Ghi không thường xuyên, đọc thường xuyên. Mẫu atomic-rename cho bất kỳ tệp nào mà mất đi sẽ làm hỏng lần khởi động tiếp theo./sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log-- nhật ký vận hành. Ghi liên tục, xoay vòng theo đường dẫn, không bao giờ ghi qua một thư mục có hàng nghìn tệp cùng cấp./sdcard/captures/<unit-id>/<year>/<month>/<day>/-- ảnh hoặc video mà ứng dụng chụp. Hình dạng cây tương tự, lý do tương tự.
Bố cục đó tốn của ứng dụng khoảng hai mươi dòng code và cứu nó khỏi các chế độ lỗi khiến camera ngừng hoạt động sau vài tháng triển khai.