Membangun sistem operasi dari nol terdengar seperti pekerjaan bagi para peretas kernel yang menulis bahasa C. Namun, Anda dapat menjalankan simulasi sederhana dalam Python dalam satu sore, dan Anda akan segera menyadari bahwa logika manajemen proses sama tidak kenal ampunnya dalam bahasa tingkat tinggi. Saya mempelajari hal ini dengan cara yang sulit. Saya duduk untuk menulis simulator OS kecil. Tujuannya sederhana: membuat beberapa proses, menjadwalkannya, dan menandainya selesai ketika pekerjaannya tuntas. Kodenya pendek. Logikanya terasa sangat kuat. Lalu saya menjalankannya, dan tidak ada yang berhenti.
Mengapa Membangun Mini OS di Python?
Sistem operasi sungguhan mengelola paging memori, sistem berkas, interupsi perangkat keras, dan driver perangkat. Sebuah simulasi menanggalkan semua itu dan membiarkan Anda fokus pada ide intinya: status (state). Anda mendefinisikan sebuah proses. Ia memiliki PID, burst time, dan status siklus hidup. Ready. Running. Finished. Sebuah loop penjadwal (scheduler loop) memilih kandidat berikutnya, memajukan statusnya, mensimulasikan time slice, dan mengubahnya menjadi selesai.
Python adalah sarana yang sangat baik untuk eksperimen semacam ini karena memungkinkan Anda mengabaikan aritmetika pointer dan penyelarasan memori (memory alignment). Sebuah list berisi dictionary menjadi tabel proses Anda. Sebuah loop while menjadi penjadwal kernel Anda. Anda dapat mengimplementasikan penjadwalan round-robin atau antrean prioritas (priority queues) hanya dengan menggunakan alat-alat pustaka standar. Ini terasa mudah didekati, yang justru menjadi alasan mengapa bug yang menyusul kemudian terasa sangat menjengkelkan.
Persiapan
Simulasi saya menggunakan sebuah list bernama process_table. Setiap entri adalah dictionary dengan bentuk seperti ini:
{
"pid": 1,
"burst_time": 3,
"status": "ready"
}
Penjadwal menjalankan loop while yang sederhana. Ia memindai tabel untuk mencari proses pertama yang statusnya bukan "finished". Ketika menemukannya, ia memanggil fungsi pembantu, execute_tick(p), untuk menjalankan proses tersebut selama satu siklus simulasi. Di dalam execute_tick, saya mengatur status proses menjadi "running", mengurangi burst time, dan memeriksa apakah sisa pekerjaan mencapai nol. Jika ya, saya memperbarui statusnya menjadi "finished". Loop luar seharusnya berhenti setelah setiap proses mencapai status finished.
Di atas kertas, alurnya bersih. Cari proses yang siap. Jalankan. Periksa penyelesaian. Ulangi sampai selesai. Saya bahkan menambahkan pernyataan print untuk mengamati penjadwal melakukan tugasnya. Saya bisa melihat proses-proses yang dipilih. Loop terus berputar. Namun, proses-proses tersebut seolah-olah memasuki masa kini yang abadi, selamanya berjalan, dan tidak pernah berlanjut.
Gejala
Ini adalah jenis kegagalan terburuk: kegagalan yang senyap. Tidak ada stack trace yang muncul di terminal. Tidak ada IndexError atau KeyError yang memberi saya petunjuk untuk diikuti. Interpreter berjalan dengan sangat baik. Programnya hanya tidak berperilaku sebagaimana mestinya. Proses dimulai, tetapi tidak pernah selesai. Saya menghabiskan waktu berjam-jam menelusuri kembali alurnya.
Apakah kondisi loop-nya salah? Mungkin saya melakukan kesalahan off-by-one dalam perhitungan burst time. Apakah tabel proses terbayangi (shadowed) atau disalin alih-alih diperbarui di tempat? Apakah kondisi terminasi saya memeriksa kunci yang salah? Saya menambahkan lebih banyak print. Saya mengaudit setiap ekspresi boolean. Saya mempertanyakan segalanya kecuali satu baris yang sebenarnya penting.
Pelakunya
Lalu saya melihatnya. Di dalam execute_tick, saya telah menulis:
p["status"] == "running"
Dua tanda sama dengan. Sebuah perbandingan, bukan penugasan (assignment). Perbaikannya hanya terpaut satu karakter:
p["status"] = "running"
Dalam Python, p["status"] == "running" adalah ekspresi yang sangat valid. Ia dievaluasi menjadi True atau False, lalu interpreter membuang hasilnya karena saya tidak pernah menetapkannya ke apa pun. Baris tersebut sama sekali tidak melakukan apa pun yang berguna. Entri dictionary tetap tidak tersentuh, mempertahankan status apa pun yang dimilikinya sebelumnya, dan proses tersebut tidak pernah maju melalui siklus hidupnya.
Saya mengubahnya menjadi satu tanda sama dengan. Saya menjalankan skripnya lagi. Simulasi itu "bernapas". Proses-proses berputar melalui ready, running, dan finished persis seperti yang direncanakan. Satu ketukan tombol ekstra telah merugikan saya selama berjam-jam.
Mengapa Bug Ini Tersembunyi
Alasan mengapa hal ini sangat menyakitkan adalah karena Python tidak menandai pernyataan ekspresi sebagai kesalahan kecuali jika sintaksisnya benar-benar tidak valid. Bug tersebut adalah kesalahan ketik semantik. Program membandingkan status, menghasilkan boolean, lalu membuangnya. Karena perbandingan itu sendiri dapat mengembalikan False, proses tetap terjebak dalam status sebelumnya, dan loop luar tidak memiliki alasan untuk berhenti.
Anda memperparah hal ini dengan bias konfirmasi. Anda merasa telah mengetik sebuah assignment karena memang itulah yang Anda maksudkan. Saat Anda membaca kode tersebut untuk kelima kalinya, otak Anda melakukan koreksi otomatis terhadap simbol tersebut. Inilah alasan mengapa teknik rubber ducking berhasil. Teknik ini memaksa Anda untuk mengartikulasikan setiap baris dengan cukup lambat sehingga celah antara apa yang tertulis dan apa yang Anda maksudkan menjadi terlihat.
Bug kecil seperti ini lebih sulit ditemukan daripada crash yang dramatis. Sebuah segfault atau kesalahan sintaksis akan langsung menunjukkan dirinya. Sebuah no-op yang senyap hanya akan merusak state dan membiarkan program berjalan tersendat-sendat. Kegagalannya terjadi di hilir (downstream), dan insting Anda adalah melakukan debug pada gejalanya, bukan penyebabnya.
Pertahanan yang Lebih Baik
Anda tidak bisa hanya mengandalkan mata Anda sendiri. Setelah kejadian ini, saya mengubah beberapa kebiasaan yang seharusnya bisa mendeteksi kesalahan tersebut lebih awal.
Pertama, jika Anda mengelola state dalam sebuah dictionary, pertimbangkan untuk menggunakan dataclass atau enum.Enum untuk state proses. Definisikan status Anda sebagai konstanta atau anggota enum:
from enum import Enum
class ProcessState(Enum):
READY = "ready"
RUNNING = "running"
FINISHED = "finished"
Dengan tipe data yang eksplisit, alat seperti mypy dapat menandai perbandingan yang mencurigakan selama analisis statis. Perbandingan yang tidak disengaja di tempat yang seharusnya merupakan sebuah assignment akan jauh lebih mudah dideteksi ketika tipe datanya tidak sesuai dengan ekspektasi.
Kedua, tulis unit test untuk transisi state sebelum Anda menulis logika scheduler. Sebuah tes sederhana yang membuat sebuah proses dengan satu tick pekerjaan, menjalankan scheduler, dan memastikan bahwa state akhirnya adalah FINISHED pasti akan langsung gagal. Kegagalan tersebut akan mempersempit pencarian ke logika pembaruan state, alih-alih membiarkan saya berkelana melalui seluruh loop.
