Xây dựng một hệ điều hành từ con số không nghe có vẻ là công việc của những hacker chuyên viết kernel bằng C. Nhưng bạn có thể khởi chạy một bản mô phỏng đơn giản hóa bằng Python chỉ trong một buổi chiều, và bạn sẽ nhanh chóng nhận ra rằng logic của việc quản lý tiến trình cũng khắc nghiệt không kém trong một ngôn ngữ bậc cao. Tôi đã học được điều này một cách cay đắng. Tôi đã ngồi xuống để viết một trình mô phỏng OS nhỏ. Mục tiêu khá khiêm tốn: tạo ra một vài tiến trình, lập lịch cho chúng, và đánh dấu chúng là đã hoàn thành khi công việc kết thúc. Mã nguồn thì ngắn. Logic thì cảm giác như không thể sai được. Thế rồi tôi chạy nó, và chẳng có gì chịu kết thúc cả.

Tại sao lại xây dựng một OS mini bằng Python?

Một hệ điều hành thực thụ phải xoay xở với phân trang bộ nhớ (memory paging), hệ thống tệp, ngắt phần cứng và trình điều khiển thiết bị. Một bản mô phỏng loại bỏ tất cả những thứ đó và cho phép bạn tập trung vào ý tưởng cốt lõi: trạng thái (state). Bạn định nghĩa một tiến trình. Nó có một PID, một thời gian thực thi (burst time) và một trạng thái vòng đời. Sẵn sàng (Ready). Đang chạy (Running). Đã hoàn thành (Finished). Một vòng lặp bộ lập lịch (scheduler loop) sẽ chọn ứng viên tiếp theo, tiến triển trạng thái của nó, mô phỏng một lát cắt thời gian (time slice) và chuyển nó sang trạng thái xong.

Python là một công cụ tuyệt vời cho loại thử nghiệm này vì nó cho phép bạn bỏ qua việc tính toán con trỏ (pointer arithmetic) và căn chỉnh bộ nhớ (memory alignment). Một danh sách các dictionary sẽ trở thành bảng tiến trình của bạn. Một vòng lặp while sẽ trở thành bộ lập lịch nhân (kernel scheduler). Bạn có thể triển khai lập lịch xoay vòng (round-robin scheduling) hoặc hàng đợi ưu tiên (priority queues) mà không cần gì hơn ngoài các công cụ thư viện tiêu chuẩn. Nó tạo cảm giác dễ tiếp cận, đó chính xác là lý do tại sao lỗi xảy ra sau đó lại gây ức chế đến vậy.

Thiết lập

Bản mô phỏng của tôi sử dụng một danh sách gọi là process_table. Mỗi mục là một dictionary có cấu trúc như sau:

{
    "pid": 1,
    "burst_time": 3,
    "status": "ready"
}

Bộ lập lịch chạy một vòng lặp while đơn giản. Nó quét bảng để tìm tiến trình đầu tiên có trạng thái không phải là "finished". Khi tìm thấy, nó gọi một hàm bổ trợ, execute_tick(p), để chạy tiến trình đó trong một chu kỳ mô phỏng. Bên trong execute_tick, tôi đặt trạng thái tiến trình thành "running", giảm thời gian thực thi, và kiểm tra xem công việc còn lại có bằng không hay không. Nếu có, tôi cập nhật trạng thái thành "finished". Vòng lặp bên ngoài được cho là sẽ kết thúc khi mọi tiến trình đều đạt đến trạng thái hoàn thành.

Trên lý thuyết, luồng xử lý rất gọn gàng. Tìm một tiến trình sẵn sàng. Chạy nó. Kiểm tra hoàn thành. Lặp lại cho đến khi xong. Tôi thậm chí còn thêm các câu lệnh print để theo dõi bộ lập lịch làm việc. Tôi có thể thấy các tiến trình được chọn. Vòng lặp vẫn tiếp tục chạy. Tuy nhiên, các tiến trình dường như rơi vào một trạng thái hiện tại vĩnh cửu, cứ chạy mãi mà không bao giờ kết thúc.

Triệu chứng

Đây là loại thất bại tồi tệ nhất: loại thất bại âm thầm. Không có stack trace nào hiện ra trong terminal. Không có IndexError hay KeyError nào đưa ra manh mối để tôi theo dõi. Trình thông dịch (interpreter) vẫn hoạt động hoàn hảo. Chương trình chỉ đơn giản là không hoạt động đúng. Các tiến trình bắt đầu, nhưng chúng không bao giờ kết thúc. Tôi đã dành hàng giờ để lần theo luồng xử lý.

Có phải điều kiện vòng lặp bị sai không? Có lẽ tôi đã mắc lỗi "off-by-one" trong việc tính toán thời gian thực thi. Có phải bảng tiến trình đang bị che khuất (shadowed) hoặc bị sao chép thay vì được cập nhật tại chỗ (in place)? Có phải điều kiện kết thúc của tôi đang kiểm tra sai khóa (key) không? Tôi đã thêm nhiều lệnh print hơn. Tôi đã kiểm tra kỹ mọi biểu thức boolean. Tôi đã nghi ngờ mọi thứ ngoại trừ dòng mã thực sự quan trọng.

Thủ phạm

Rồi tôi đã thấy nó. Bên trong execute_tick, tôi đã viết:

p["status"] == "running"

Hai dấu bằng. Đó là một phép so sánh, không phải phép gán. Cách khắc phục chỉ cách đúng một ký tự:

p["status"] = "running"

Trong Python, p["status"] == "running" là một biểu thức hoàn toàn hợp lệ. Nó trả về True hoặc False, và sau đó trình thông dịch sẽ bỏ qua kết quả vì tôi không bao giờ gán nó cho bất cứ thứ gì. Dòng mã đó hoàn toàn không làm được việc gì hữu ích. Mục trong dictionary vẫn không hề được chạm tới, giữ nguyên trạng thái trước đó, và tiến trình không bao giờ tiến triển qua vòng đời của nó.

Tôi đã đổi nó thành một dấu bằng duy nhất. Tôi chạy lại script. Bản mô phỏng đã hoạt động. Các tiến trình xoay vòng qua các trạng thái sẵn sàng, đang chạy và đã hoàn thành đúng như kế hoạch. Một phím bấm thừa đã khiến tôi mất hàng giờ đồng hồ.

Tại sao những lỗi này lại ẩn mình

Lý do khiến điều này gây khó chịu đến vậy là vì Python không đánh dấu một câu lệnh biểu thức (expression statement) là lỗi trừ khi nó sai cú pháp hoàn toàn. Lỗi này là một lỗi đánh máy về mặt ngữ nghĩa (semantic typo). Chương trình đã so sánh trạng thái, tạo ra một giá trị boolean, rồi vứt nó đi. Vì bản thân phép so sánh có thể trả về False, tiến trình vẫn bị kẹt ở trạng thái trước đó, và vòng lặp bên ngoài không có lý do gì để thoát ra.

Bạn làm trầm trọng thêm điều này bằng thiên kiến xác nhận. Bạn biết mình đã gõ một phép gán vì bạn thực sự định gõ một phép gán. Khi bạn đọc mã nguồn đến lần thứ năm, não bộ sẽ tự động "sửa lỗi" ký hiệu đó. Đây là lý do tại sao phương pháp rubber ducking lại hiệu quả. Nó buộc bạn phải diễn đạt từng dòng mã đủ chậm để khoảng cách giữa những gì được viết và những gì bạn định viết trở nên rõ ràng.

Những lỗi nhỏ như thế này khó tìm hơn nhiều so với các lỗi crash nghiêm trọng. Một lỗi segfault hay lỗi cú pháp sẽ tự thông báo ngay lập tức. Một lệnh no-op thầm lặng chỉ đơn giản là làm sai lệch trạng thái và để chương trình tiếp tục chạy một cách chập chờn. Sự cố xảy ra ở các bước sau đó, và bản năng của bạn là đi tìm lỗi ở triệu chứng thay vì tìm nguyên nhân gốc rễ.

Một phương pháp phòng vệ tốt hơn

Bạn không thể chỉ tin vào mắt mình. Sau sự cố này, tôi đã thay đổi một vài thói quen giúp phát hiện lỗi sớm hơn.

Đầu tiên, nếu bạn đang duy trì trạng thái trong một dictionary, hãy cân nhắc sử dụng dataclass hoặc enum.Enum cho các trạng thái của tiến trình. Hãy định nghĩa các trạng thái của bạn dưới dạng hằng số hoặc các thành viên enum:

from enum import Enum

class ProcessState(Enum):
    READY = "ready"
    RUNNING = "running"
    FINISHED = "finished"

Với các kiểu dữ liệu tường minh, các công cụ như mypy có thể cảnh báo các phép so sánh đáng ngờ trong quá trình phân tích tĩnh. Một phép so sánh vô tình xảy ra tại nơi đáng lẽ phải là một phép gán sẽ trở nên dễ dàng phát hiện hơn nhiều khi các kiểu dữ liệu không khớp với kỳ vọng.

Thứ hai, hãy viết unit test cho các bước chuyển đổi trạng thái trước khi bạn viết logic của bộ lập lịch. Một bài kiểm tra đơn giản tạo ra một tiến trình với một tick công việc, chạy bộ lập lịch và kiểm tra xem trạng thái cuối cùng có phải là FINISHED hay không sẽ thất bại ngay lập tức. Sự thất bại đó sẽ giúp thu hẹp phạm vi tìm kiếm vào logic cập nhật trạng thái, thay vì để tôi phải loay hoay tìm kiếm trong toàn bộ vòng lặp.