2 poin oleh GN⁺ 2 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • 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 execfd atau filename jalur 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 implementasi posix_spawn() di ruang pengguna

Keterbatasan model pembuatan proses Unix

  • Sejak awal Unix, fork() adalah system call inti yang membuat proses anak sebagai salinan induknya, sementara exec() 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 diikuti exec(), dan exec() membuang seluruh memori yang sudah disalin untuk anak
  • Sudah ada upaya optimasi seperti vfork(), tetapi pola fork() lalu exec() 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() dan exec()
  • 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);
  • Pemanggilan ini mengembalikan file descriptor yang merepresentasikan templat file eksekusi
  • File eksekusi harus ditentukan lewat file descriptor execfd atau jalur absolut filename, 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_args
    • argv adalah pointer ke daftar argumen yang diteruskan ke program
    • envp adalah pointer ke environment program
    • actions adalah pointer ke array spawn_template_action yang meneruskan perubahan file descriptor dan penanganan sinyal
    Iklan
  • spawn_template_action terdiri dari field type, flags, fd, newfd, dan arg
    • Jika file descriptor 4 harus ditutup di proses anak, type disetel ke SPAWN_TEMPLATE_ACTION_CLOSE dan fd disetel ke 4
    • Aksi lain mendukung duplikasi file descriptor, membuka file, mengubah direktori kerja, dan mengubah penanganan sinyal
  • 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);
  • 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 pola fork()/exec()
  • Implementasi saat ini menyembunyikan fork() dan exec() 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

 
GN⁺ 2 jam lalu
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 operasi
    Alih-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

    • Alasan fork()+exec() menjadi seperti itu adalah agar dapat menjalankan program yang terlalu besar untuk dimuat ke memori bersama program induk
      Implementasi 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 memanggil exec()
      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 .so yang memulainya; program loader berjalan di user space tanpa hak istimewa. Mungkin pendekatan ini lebih dekat ke cara yang benar
    • Menarik bahwa pembuatan proses di Windows, sistem operasi “besar” yang paling luas digunakan dan tidak memakai fork(), sangat lambat
      Saya setuju harus ada primitive selain fork(), tetapi saya tidak yakin performa adalah argumen terbaiknya
    • Makalah ini juga bagus, dan referensi [29] juga sangat bagus karena membahas sisi-sisi halus dari antarmuka yang dapat diskalakan, termasuk fork(): The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf
    • Diskusi saat itu ada di sini: https://news.ycombinator.com/item?id=19621799 - A fork() in the road (2019-04-10, 178 comments)
    • fork() sangat bagus untuk pola zygote
      Sulit 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

    • Biasanya Anda ingin berkomunikasi dengan proses itu, jadi misalnya Anda perlu menyiapkan hal-hal seperti file descriptor dan meneruskan informasi proses induk
    • Bukankah itu diselesaikan dengan O_CLOEXEC?
    • Jika yang dimaksud “cara untuk mengekspresikan yang kedua secara langsung”, bukankah itu memang tujuan posix_spawn?
    • Tepatnya apa yang dimaksud dengan “proses yang benar-benar baru”?
  • 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 pemanggilan fork() sering kali segera diikuti exec(), sehingga semua memori yang dengan susah payah disalin untuk anak langsung dibuang” sambil tidak menyebut copy-on-write
    Itu adalah optimasi yang mencegah seluruh memori benar-benar disalin, tetapi di sini tidak disebut

    • Artikel itu menanganinya secara implisit, tetapi salinan status proses di sini berarti struktur manajemen memori. Terutama page table dan VMA
      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
    • Redis adalah jenis proses di mana biaya ini sangat penting. fork() memang tidak menyalin memorinya sendiri, tetapi page table tetap harus disalin
      Jika proses memegang RAM puluhan GB, fork() bisa memakan waktu lama, dan ini terjadi setiap kali Redis melakukan dump file .rdb atau menulis ulang AOF log biner
      Bahkan pada 2012 ada tulisan yang menunjukkan tingginya biaya operasi ini: https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...
      Pada m2.xlarge yang 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 table
      Mengejutkan 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
    • Bagi pembaca yang memang menjadi sasaran makalah seperti ini, copy-on-write tampaknya dianggap pengetahuan dasar sehingga dihilangkan
    • Bahkan dengan copy-on-write, 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 sebelum exec() dijalankan
    • Teks aslinya mengatakan “status”. Walau memakai copy-on-write dan tidak menyalin isinya, tetap ada biaya yang sebanding dengan jumlah entri page table
      Bahwa 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 setelah fork() kita bisa memakai API biasa apa adanya untuk melakukan segala macam konfigurasi
    Sejauh 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

    • Saya agak tidak setuju, tetapi kegunaannya tetap terlihat. Meski fork()/exec() mungkin berguna dalam beberapa kasus, rasanya cukup bagus jika API menerima argumen pidfd. Nilai 0 bisa diartikan sebagai proses saat ini
      Masalahnya mungkin pada biner setuid/setgid; untuk kasus ini, mungkin lebih baik ada penanganan khusus di exec
      Misalnya, kita bisa membuat proses yang dihentikan dengan pidfd_t ps = spawn();, lalu menyusunnya seperti setuid(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 tertentu
      Namun, saya setuju bahwa pendekatan seperti CreateProcess yang menerima sangat banyak parameter bukanlah API ruang pengguna yang hebat
    • Saya sepenuhnya berpandangan sebaliknya. Kesalahan besar model bergaya UNIX adalah terlalu banyak state yang dipertahankan saat membuat proses
      Sebagai 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 benar
    • Menyebut itu anggun adalah ketergantungan jalur dari sejarah fork()+exec()
      Di dunia lain yang tidak pernah memiliki fork()+exec(), banyak dari “API biasa” itu akan memiliki argumen pid eksplisit agar bisa mengubah konfigurasi proses lain. Fuchsia kira-kira seperti itu
      Dunia 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
    • Cara yang benar untuk menghapus fork() adalah membuat API umum yang mengubah state proses menerima handle proses yang eksplisit
      Dengan begitu, API yang sama bisa dipakai untuk mengonfigurasi proses kosong, dan juga bisa dikombinasikan dengan cara lain seperti IPC atau debugging
    • Urutannya seharusnya spawn, configure, exec
      Jika proses dimulai dalam keadaan terhubung ptrace dan tanpa thread, pada tahap konfigurasi kita bisa memaksa system call dijalankan. Karena Linux bahkan tidak punya konsep “proses tanpa thread”, mungkin akan dibutuhkan thread boneka
  • Kesalahpahaman bahwa fork() itu murah ternyata anehnya sangat umum, padahal kompleksitasnya O(N) terhadap ukuran proses, dan memang selalu begitu
    Benar, 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() dan exec() di dalam seperti implementasi saat ini”

    • Tampaknya yang menarik perhatian bukan implementasi tertentu, melainkan konsep itu sendiri
  • 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 terkait
    Seperti 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 ulang git dari 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 sendiri
      Kalau 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
    • Sebagai orang yang berangkat dari Windows, model fork()+exec() sama sekali tidak masuk akal bagi saya. Sekarang saya tahu itu hanyalah keanehan historis, tetapi masih ada orang yang berpura-pura bahwa fork()+exec() itu benar-benar bagus
    • Ada libgit2. Kita memang bisa membayangkan berkomunikasi dengan semacam gitd lewat pipe atau socket, tetapi saya tidak tahu kenapa itu ide yang bagus. Kalau bukan begitu, ya harus meluncurkan proses
  • Alasan exec/fork sulit digantikan adalah karena proses baru biasanya perlu dikonfigurasi. Misalnya, kita perlu mengatur signal handler, menutup atau membuka file descriptor, berpindah namespace, mengatur seccomp, dan menyesuaikan hak akses
    Tetapi 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 spawn bisa membuat proses kosong, memuat loader ringan ke dalamnya, lalu mengirimkan data konfigurasi arbitrer. Loader itu akan mengatur proses tersebut dan menjalankan exec() untuk program utama
    Dengan begitu, API lama bisa dipertahankan tanpa mem-fork memori, tetapi file descriptor dan hal-hal lainnya tetap harus diduplikasi

    • Untungnya, sepertinya ada seseorang yang naik mesin waktu, membaca tulisan ini, lalu menambahkannya ke POSIX.1-2001 :)
      Maaf kalau ini bukan lelucon, tetapi posix_spawn() memang sudah ada dan di glibc, fork hanyalah alias untuk clone()
      Meski tidak persis sama dengan usulan awal, fork()/exec() memang sudah sangat dekat dengan status legacy
  • Jika fork dan exec bisa 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 malas

  • Sudah banyak diskusi tentang API lama ini di Hacker News, misalnya https://news.ycombinator.com/item?id=31739794