Melampaui fork() + exec()
(lwn.net)- spawn templates adalah usulan pembuatan proses untuk kernel Linux yang memungkinkan kernel menyimpan cache informasi file eksekusi agar peluncuran proses berikutnya lebih cepat pada aplikasi yang berulang kali menjalankan file eksekusi yang sama
- fork() harus menyalin seluruh status proses termasuk memori untuk proses anak, dan dalam banyak kasus
exec()yang langsung menyusul akan membuang memori itu, sehingga menimbulkan inefisiensi pada pola yang ada - spawn_template_create() mengembalikan file descriptor templat dengan menentukan file eksekusi melalui
execfdataufilenamejalur absolut, dan kernel membuka file tersebut lalu menyimpan cache informasi yang diperlukan untuk eksekusi cepat - spawn_template_spawn() bekerja dengan cara yang mirip jalur
fork()/exec()biasa, tetap mempertahankan pemeriksaan yang diterapkan saat mengeksekusi file baru, dan benchmark pada cover letter mencatat peningkatan sekitar 2% {p:2} - Pembuatan proses kosong berbasis pidfd dan konfigurasi
pidfd_config()dinilai sebagai pendekatan yang lebih baik, dengan tujuan mendukung implementasiposix_spawn()di ruang pengguna
Keterbatasan model pembuatan proses Unix
- Sejak awal Unix,
fork()adalah system call inti yang membuat proses anak sebagai salinan induknya, sementaraexec()menjalankan program baru menggantikan proses saat ini - Di kernel Linux, fungsi inti yang sama lebih dikenal sebagai clone() dan execve()
- Model pembuatan proses ini memiliki keanggunan sekaligus kekurangan, dan usulan spawn templates dari Li Chen kemungkinan tidak akan diterima ke kernel Linux dalam bentuk saat ini, tetapi bisa mengarah pada primitive pembuatan proses baru di masa depan
fork()adalah system call yang relatif mahal karena harus menyalin seluruh status proses termasuk memori untuk membuat proses anak- Selama bertahun-tahun ada berbagai optimasi, tetapi
fork()pada dasarnya tetap merupakan operasi berbiaya tinggi - Dalam banyak kasus, pemanggilan
fork()langsung diikutiexec(), danexec()membuang seluruh memori yang sudah disalin untuk anak - Sudah ada upaya optimasi seperti vfork(), tetapi pola
fork()laluexec()masih lebih mahal daripada yang seharusnya
Spawn templates
- Set patch Li Chen berfokus pada aplikasi yang berulang kali menjalankan file eksekusi yang sama untuk mengoptimalkan pola
fork()danexec() - Contohnya adalah program yang harus berulang kali menjalankan Git untuk mengambil informasi tentang konten repositori
- Dalam kasus seperti ini, program dapat membuat templat untuk menyebarkan biaya penyiapan ke banyak eksekusi, lalu mempercepat pemanggilan menggunakan templat tersebut
- Pembuatan templat menggunakan system call
spawn_template_create()- dengan signature berbentuk
int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
- dengan signature berbentuk
- Pemanggilan ini mengembalikan file descriptor yang merepresentasikan templat file eksekusi
- File eksekusi harus ditentukan lewat file descriptor
execfdatau jalur absolutfilename, dan keduanya tidak bisa digunakan bersamaan - Kernel membuka file yang ditentukan, lalu menyimpan cache berbagai informasi yang diperlukan untuk mengeksekusi file itu lebih cepat nanti
- Setiap eksekusi dapat memiliki argumen, environment, perubahan file descriptor, dan perubahan penanganan sinyal yang berbeda
- Informasi eksekusi yang spesifik ditempatkan dalam struktur
spawn_template_spawn_argsargvadalah pointer ke daftar argumen yang diteruskan ke programenvpadalah pointer ke environment programactionsadalah pointer ke arrayspawn_template_actionyang meneruskan perubahan file descriptor dan penanganan sinyal
spawn_template_actionterdiri dari fieldtype,flags,fd,newfd, danarg- Jika file descriptor 4 harus ditutup di proses anak,
typedisetel keSPAWN_TEMPLATE_ACTION_CLOSEdanfddisetel ke 4 - Aksi lain mendukung duplikasi file descriptor, membuka file, mengubah direktori kerja, dan mengubah penanganan sinyal
- Jika file descriptor 4 harus ditutup di proses anak,
- Setelah informasi eksekusi diisi, proses baru dijalankan dengan
spawn_template_spawn()- dengan signature berbentuk
int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
- dengan signature berbentuk
- Cara kerja internalnya mirip dengan jalur
fork()/exec()biasa - Semua pemeriksaan normal yang diterapkan saat mengeksekusi file baru tetap dipertahankan
- Informasi yang di-cache dalam templat mempercepat keseluruhan alur pembuatan
- Hasil benchmark pada cover letter menunjukkan peningkatan sekitar 2%, angka yang bisa berarti bagi aplikasi yang sesuai dengan pola yang diharapkan {p:2}
Menuju posix_spawn()
- Mateusz Guzik menilai bahwa “seluruh idiom fork + exec itu mengerikan dan harus disingkirkan”
- Titik yang terasa aneh dari set patch ini adalah bagian
fork()tetap dipertahankan, padahal sebagian besar biayanya dianggap ada di sana - Optimasi seharusnya menghilangkan penyalinan proses saat ini dan membuat “proses yang bersih (pristine)”
- Christian Brauner berpendapat bahwa gagasan API builder untuk
exec“tidak terlalu aneh” - Namun ia lebih memilih pendekatan membangun API baru di atas abstraksi pidfd yang sudah ada
- Belum ada rincian spesifik, tetapi pendekatan yang tepat adalah menambahkan opsi ke pidfd_open() untuk membuat proses kosong
- Setelah itu, system call baru
pidfd_config()dapat dipanggil beberapa kali untuk menerapkan environment, image yang akan dieksekusi, dan pengaturan lain yang diinginkan ke proses baru pidfd_config()berperan serupa dengan fsconfig()- Tujuan penting dari antarmuka baru ini adalah mendukung implementasi posix_spawn() di ruang pengguna
posix_spawn()cocok sebagai pengganti polafork()/exec()- Implementasi saat ini menyembunyikan
fork()danexec()di dalamnya, sementara implementasi native akan memiliki struktur yang berbeda - Li Chen setuju bahwa API yang digambarkan secara luas oleh Brauner tampak lebih baik, dan berencana mengarahkan pekerjaan selanjutnya ke sana
- Spawn templates tidak akan masuk ke kernel Linux, tetapi jika pekerjaan lanjutan itu membuahkan hasil, Linux pada akhirnya bisa memiliki implementasi
posix_spawn()yang layak
1 komentar
Opini Hacker News
Ada makalah terkait, A fork() in the road: https://www.microsoft.com/en-us/research/wp-content/uploads/...
Abstraknya berargumen bahwa, berlawanan dengan anggapan umum bahwa kombinasi
fork()+exec()adalah desain yang penuh inspirasi, itu memang peretasan cerdas untuk mesin dan program era 1970-an, tetapi kini merupakan abstraksi yang buruk bagi programmer modern dan juga membatasi implementasi sistem operasiAlih-alih mempertahankannya sebagai primitive kelas satu sistem operasi, mereka berpendapat bahwa ini seharusnya diajarkan sebagai artefak sejarah dan tidak menjadi cara pembuatan proses pertama yang dipelajari mahasiswa
fork()+exec()menjadi seperti itu adalah agar dapat menjalankan program yang terlalu besar untuk dimuat ke memori bersama program indukImplementasi awalnya melakukan swap-out ke disk terhadap program yang melakukan
fork()saat pemanggilan, lalu sebelum kontrol dikembalikan, entri tabel proses digandakan dan disesuaikan sehingga terbentuk proses yang berada di memori dan proses yang sudah di-swap-out, dan sisi yang ada di memori menerima kontrol untuk memanggilexec()Berkat pendekatan ini, bahkan mesin PDP-11 yang kecil bisa menjalankan program besar, dan itu diperlukan pada masa ketika memori sangat mahal
Menariknya, di QNX pemuatan program tidak ada di dalam sistem operasi melainkan di pustaka. Ia membaca header executable, mengalokasikan memori, memuat program, menyiapkannya untuk dijalankan, lalu menautkan
.soyang memulainya; program loader berjalan di user space tanpa hak istimewa. Mungkin pendekatan ini lebih dekat ke cara yang benarfork(), sangat lambatSaya setuju harus ada primitive selain
fork(), tetapi saya tidak yakin performa adalah argumen terbaiknyafork(): The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdffork()sangat bagus untuk pola zygoteSulit membayangkan optimasi lain yang seefisien dan seanggun itu
Baru-baru ini saya mengalami bug yang samar karena harus menutup lebih banyak file descriptor di proses hasil fork
Dalam pengalaman saya, “saya ingin proses yang benar-benar baru” jauh lebih umum daripada “saya ingin salinan dari proses saat ini”, tetapi terasa aneh bahwa tidak ada cara untuk mengekspresikan yang kedua itu secara langsung, dan kita hanya bisa mendekatinya dengan menyalin lalu membereskan semuanya setelahnya
O_CLOEXEC?posix_spawn?Agak aneh mengatakan bahwa “
fork()adalah system call yang relatif mahal, dan harus menyalin seluruh status proses termasuk memori untuk proses anak. Selama bertahun-tahun ada banyak optimasi, tetapi pada dasarnya ini tetap operasi yang mahal. Yang lebih buruk, setelah pemanggilanfork()sering kali segera diikutiexec(), sehingga semua memori yang dengan susah payah disalin untuk anak langsung dibuang” sambil tidak menyebut copy-on-writeItu adalah optimasi yang mencegah seluruh memori benar-benar disalin, tetapi di sini tidak disebut
Walaupun memori yang dirujuk halaman-halaman aktual tetap dibagikan, halaman baru tetap harus dialokasikan untuk menampung salinan struktur-struktur itu. Dan menelusuri semuanya untuk menyalinnya sendiri tetap mahal
fork()memang tidak menyalin memorinya sendiri, tetapi page table tetap harus disalinJika proses memegang RAM puluhan GB,
fork()bisa memakan waktu lama, dan ini terjadi setiap kali Redis melakukan dump file.rdbatau menulis ulang AOF log binerBahkan pada 2012 ada tulisan yang menunjukkan tingginya biaya operasi ini: https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...
Pada
m2.xlargeyang memakai sekitar 25GB RAM,fork()memakan 5,67 detik. Mengingat klien Redis biasanya mengalami latensi dalam milidetik satu digit untuk sebagian besar operasi, ini adalah waktu henti yang lama. Dan ini baru waktu penyalinan page tableMengejutkan bahwa huge page tidak disebut, karena di sini itu tampak seperti pertimbangan utama. Empat belas tahun kemudian perangkat keras pasti lebih cepat, tetapi instance Redis juga kemungkinan memakai lebih banyak RAM, jadi menarik jika benchmark ini dijalankan lagi
fork()tetap harus membayar biaya penyiapannya. Jika proses induk memiliki banyak thread sibuk, misalnya di Java, bisa terjadi banyak copy-on-write yang tidak perlu sebelumexec()dijalankanBahwa melakukan fork pada program dengan ukuran memori virtual besar itu lambat adalah masalah yang sudah dikenal luas
Keanggunan model
fork()+exec()terletak pada kenyataan bahwa setelahfork()kita bisa memakai API biasa apa adanya untuk melakukan segala macam konfigurasiSejauh ini, alternatif panggilan gabungan yang sudah saya lihat tampak pada dasarnya kurang memadai, karena semua opsi konfigurasi harus ditambahkan sebagai parameter pemanggilan, dan juga harus dirancang agar nantinya bisa diperluas tanpa berubah menjadi berantakan
fork()/exec()mungkin berguna dalam beberapa kasus, rasanya cukup bagus jika API menerima argumenpidfd. Nilai 0 bisa diartikan sebagai proses saat iniMasalahnya mungkin pada biner
setuid/setgid; untuk kasus ini, mungkin lebih baik ada penanganan khusus diexecMisalnya, kita bisa membuat proses yang dihentikan dengan
pidfd_t ps = spawn();, lalu menyusunnya sepertisetuid(ps, 33);,capset(ps, ...);,socket(ps, ...);,mmap(ps, ...);,process_vm_writev(ps, ...);,exec(ps, ...);,signal(ps, SIGCONT);Ini juga merupakan kritik bahwa API system call biasa tidak cukup mempertimbangkan pertanyaan, “Bagaimana kalau saya ingin melakukan ini pada proses lain yang punya izin akses bagi saya?” Dengan cara ini, keamanan thread pada
fork()juga bisa dicapai sampai tingkat tertentuNamun, saya setuju bahwa pendekatan seperti
CreateProcessyang menerima sangat banyak parameter bukanlah API ruang pengguna yang hebatSebagai contoh, ada API yang membuat suatu objek menjadi file descriptor nomor 4, lalu kita bisa menjalankan program dan membuat program itu mencari objek tersebut di descriptor 4. Ini aneh
Windows, meski punya banyak kekurangan, tidak memakai
fork()+exec(), melainkan terutama menyediakan opsi tentang cara membuat proses. Memang tidak anggun, tetapi arahnya benarfork()+exec()Di dunia lain yang tidak pernah memiliki
fork()+exec(), banyak dari “API biasa” itu akan memiliki argumenpideksplisit agar bisa mengubah konfigurasi proses lain. Fuchsia kira-kira seperti ituDunia seperti ini punya banyak kelebihan. Yang paling jelas adalah tidak perlu secara ajaib menciptakan skema IPC terpisah untuk melaporkan kesalahan konfigurasi, dan juga cukup berguna bisa memiliki proses pengelola yang menyesuaikan atribut anak. Debugger tampaknya akan sangat menyukainya
fork()adalah membuat API umum yang mengubah state proses menerima handle proses yang eksplisitDengan begitu, API yang sama bisa dipakai untuk mengonfigurasi proses kosong, dan juga bisa dikombinasikan dengan cara lain seperti IPC atau debugging
Jika proses dimulai dalam keadaan terhubung
ptracedan tanpa thread, pada tahap konfigurasi kita bisa memaksa system call dijalankan. Karena Linux bahkan tidak punya konsep “proses tanpa thread”, mungkin akan dibutuhkan thread bonekaKesalahpahaman bahwa
fork()itu murah ternyata anehnya sangat umum, padahal kompleksitasnya O(N) terhadap ukuran proses, dan memang selalu begituBenar, ini copy-on-write. Tetapi ada hubungan linear antara ukuran proses dan jumlah entri page table yang dibutuhkan untuk merepresentasikannya
Tidak mengejutkan kalau patch Chen ditolak. Kasus penggunaannya terlalu khusus sehingga nilainya rendah untuk didukung
Dari sudut pandang pengembang shell, saya setuju dengan kesimpulan bahwa “kemungkinan besar para pengembang akan menyambut implementasi native yang tidak menyembunyikan
fork()danexec()di dalam seperti implementasi saat ini”fork()terlihat mengerikan secara konseptual sejak pertama kali saya mempelajarinya. Jika yang ingin dilakukan adalah satu pekerjaan, yaitu memulai proses, seharusnya kita tidak perlu melewati mantra teka-teki berupa mem-fork proses saat ini, yang merupakan pekerjaan lain dan tidak terkaitSeperti pada contoh di tulisan, saya penasaran apa cara terbaik menangani situasi ketika satu proses meluncurkan banyak subproses
git. Rasanya tidak masuk akal untuk terus-menerus memulai ulanggitdari nol di tengah pekerjaan induk yang berjalan lama; abstraksi berbiaya rendah apa yang bisa memberi hasil yang sama?fork()sederhana secara konseptual. Tanpa menarik lapisan lain, memulai proses dari satu-satunya hal yang pasti kita tahu ada, yaitu diri sendiriKalau tidak, dibutuhkan beberapa tahap: membuat proses, mengisinya dengan sesuatu untuk dijalankan, lalu menjadwalkannya agar berjalan. Atau semuanya harus dihancurkan dan digabung permanen dengan lapisan lain seperti filesystem, object loader, dan linker seperti pada Win32
fork()+exec()sama sekali tidak masuk akal bagi saya. Sekarang saya tahu itu hanyalah keanehan historis, tetapi masih ada orang yang berpura-pura bahwafork()+exec()itu benar-benar baguslibgit2. Kita memang bisa membayangkan berkomunikasi dengan semacamgitdlewat pipe atau socket, tetapi saya tidak tahu kenapa itu ide yang bagus. Kalau bukan begitu, ya harus meluncurkan prosesAlasan
exec/forksulit digantikan adalah karena proses baru biasanya perlu dikonfigurasi. Misalnya, kita perlu mengatur signal handler, menutup atau membuka file descriptor, berpindah namespace, mengaturseccomp, dan menyesuaikan hak aksesTetapi system call untuk itu saat ini hanya berlaku pada proses saat ini, jadi dibutuhkan sarana pengganti. Usulan tulisan itu adalah membuat API baru untuk tujuan tersebut
Menurut saya, system call baru seperti
spawnbisa membuat proses kosong, memuat loader ringan ke dalamnya, lalu mengirimkan data konfigurasi arbitrer. Loader itu akan mengatur proses tersebut dan menjalankanexec()untuk program utamaDengan begitu, API lama bisa dipertahankan tanpa mem-fork memori, tetapi file descriptor dan hal-hal lainnya tetap harus diduplikasi
Maaf kalau ini bukan lelucon, tetapi
posix_spawn()memang sudah ada dan di glibc,forkhanyalah alias untukclone()Meski tidak persis sama dengan usulan awal,
fork()/exec()memang sudah sangat dekat dengan status legacyJika
forkdanexecbisa menunjukkan perilaku yang berkelanjutan dan aljabar melampaui sifat copy-on-write-nya, keduanya bukan hanya akan lebih berguna, tetapi juga lebih menarik untuk dipakai. Misalnya, itu bisa digunakan untuk evaluasi malasSudah banyak diskusi tentang API lama ini di Hacker News, misalnya https://news.ycombinator.com/item?id=31739794