Sistem file proc (selanjutnya hanya procfs) adalah sistem file virtual yang menyediakan informasi tentang proses. Dia adalah contoh "baik" dari antarmuka mengikuti paradigma "semuanya adalah file". Procfs dikembangkan sejak lama: pada saat server melayani rata-rata lusinan proses, saat membuka file dan membaca informasi proses bukanlah masalah. Namun, waktu tidak berhenti, dan sekarang server melayani ratusan ribu, atau bahkan lebih banyak proses pada saat yang bersamaan. Dalam konteks ini, gagasan "membuka file untuk setiap proses untuk mengurangi data yang menarik" tidak lagi terlihat begitu menarik, dan hal pertama yang terlintas dalam pikiran untuk mempercepat pembacaan adalah untuk memperoleh informasi tentang sekelompok proses dalam satu iterasi. Pada artikel ini, kami akan mencoba menemukan elemen procfs yang dapat dioptimalkan.

Gagasan untuk meningkatkan procf muncul ketika kami menemukan bahwa CRIU menghabiskan banyak waktu hanya membaca file procfs. Kami melihat bagaimana masalah yang sama diselesaikan untuk soket, dan memutuskan untuk melakukan sesuatu yang mirip dengan antarmuka sock-diag, tetapi hanya untuk procfs. Tentu saja, kami berasumsi betapa sulitnya untuk mengubah antarmuka yang sudah lama dan mapan di kernel, untuk meyakinkan masyarakat bahwa game ini sepadan dengan lilin ... dan kami sangat terkejut dengan jumlah orang yang mendukung pembuatan antarmuka baru. Sebenarnya, tidak ada yang tahu seperti apa tampilan antarmuka baru, tetapi tidak ada keraguan bahwa procfs tidak memenuhi persyaratan kinerja saat ini. Sebagai contoh, skenario ini: server merespon permintaan terlalu lama, vmstat menunjukkan bahwa memori telah beralih, dan "ps ax" dimulai dari 10 detik atau lebih, top tidak menunjukkan apa-apa sama sekali. Pada artikel ini kami tidak akan mempertimbangkan antarmuka baru yang spesifik, melainkan, kami akan mencoba menjelaskan masalah dan solusinya.
Setiap proses eksekusi procfs diwakili oleh direktori / proc / <pid>
.
Di setiap direktori tersebut ada banyak file dan subdirektori yang menyediakan akses ke informasi tertentu tentang proses. Subdirektori mengelompokkan data berdasarkan fitur. Misalnya ( $$
adalah variabel shell khusus yang diperluas dalam pid - pengidentifikasi proses saat ini):
$ ls -F /proc/$$ attr/ exe@ mounts projid_map status autogroup fd/ mountstats root@ syscall auxv fdinfo/ net/ sched task/ cgroup gid_map ns/ schedstat timers clear_refs io numa_maps sessionid timerslack_ns cmdline limits oom_adj setgroups uid_map comm loginuid oom_score smaps wchan coredump_filter map_files/ oom_score_adj smaps_rollup cpuset maps pagemap stack cwd@ mem patch_state stat environ mountinfo personality statm
Semua file ini menghasilkan data dalam berbagai format. Sebagian besar dalam format teks ASCII yang mudah dirasakan oleh manusia. Yah, hampir mudah:
$ cat /proc/$$/stat 24293 (bash) S 21811 24293 24293 34854 24876 4210688 6325 19702 0 10 15 7 33 35 20 0 1 0 47892016 135487488 3388 18446744073709551615 94447405350912 94447406416132 140729719486816 0 0 0 65536 3670020 1266777851 1 0 0 17 2 0 0 0 0 0 94447408516528 94447408563556 94447429677056 140729719494655 140729719494660 140729719494660 140729719496686 0
Untuk memahami apa arti setiap elemen dari set ini, pembaca harus membuka man proc (5), atau dokumentasi kernel. Sebagai contoh, elemen kedua adalah nama file yang dapat dieksekusi di dalam tanda kurung, dan elemen ke sembilan belas adalah nilai saat ini dari prioritas eksekusi (bagus).
Beberapa file cukup dapat dibaca sendiri:
$ cat /proc/$$/status | head -n 5 Name: bash Umask: 0002 State: S (sleeping) Tgid: 24293 Ngid: 0
Tetapi seberapa sering pengguna membaca informasi langsung dari file procfs? Berapa lama kernel perlu mengkonversi data biner ke format teks? Berapa overhead dari proksi? Seberapa nyaman antarmuka ini untuk program monitor status, dan berapa banyak waktu yang dihabiskan untuk memproses data teks ini? Seberapa kritis implementasi yang begitu lambat dalam situasi darurat?
Kemungkinan besar, itu tidak akan menjadi kesalahan untuk mengatakan bahwa pengguna lebih suka program seperti top atau ps, daripada membaca data dari procfs secara langsung.
Untuk menjawab pertanyaan yang tersisa, kami akan melakukan beberapa percobaan. Pertama, temukan di mana kernel menghabiskan waktu untuk menghasilkan file procfs.
Untuk mendapatkan informasi tertentu dari semua proses dalam sistem, kita harus melalui direktori / proc / dan memilih semua subdirektori yang namanya direpresentasikan oleh angka desimal. Kemudian, di masing-masing dari mereka, kita perlu membuka file, membacanya dan menutupnya.
Secara total, kami akan melakukan tiga panggilan sistem, salah satunya akan membuat file descriptor (di kernel, file deskriptor dikaitkan dengan satu set objek internal yang dialokasikan memori tambahan). Sistem panggilan terbuka () dan tutup () tidak memberikan informasi apa pun kepada kami, sehingga mereka dapat dikaitkan dengan overhead antarmuka procfs.
Mari kita coba untuk membuka () dan menutup () untuk setiap proses dalam sistem, tetapi kami tidak akan membaca isi file:
$ time ./task_proc_all --noread stat tasks: 50290 real 0m0.177s user 0m0.012s sys 0m0.162s
$ time ./task_proc_all --noread loginuid tasks: 50289 real 0m0.176s user 0m0.026s sys 0m0.145
task-proc-all - sebuah utilitas kecil, kode yang dapat ditemukan di tautan di bawah ini
Tidak masalah file mana yang dibuka, karena data nyata dihasilkan hanya pada saat dibaca ().
Sekarang lihat output dari profiler inti perf:
- 92.18% 0.00% task_proc_all [unknown] - 0x8000 - 64.01% __GI___libc_open - 50.71% entry_SYSCALL_64_fastpath - do_sys_open - 48.63% do_filp_open - path_openat - 19.60% link_path_walk - 14.23% walk_component - 13.87% lookup_fast - 7.55% pid_revalidate 4.13% get_pid_task + 1.58% security_task_to_inode 1.10% task_dump_owner 3.63% __d_lookup_rcu + 3.42% security_inode_permission + 14.76% proc_pident_lookup + 4.39% d_alloc_parallel + 2.93% get_empty_filp + 2.43% lookup_fast + 0.98% do_dentry_open 2.07% syscall_return_via_sysret 1.60% 0xfffffe000008a01b 0.97% kmem_cache_alloc 0.61% 0xfffffe000008a01e - 16.45% __getdents64 - 15.11% entry_SYSCALL_64_fastpath sys_getdents iterate_dir - proc_pid_readdir - 7.18% proc_fill_cache + 3.53% d_lookup 1.59% filldir + 6.82% next_tgid + 0.61% snprintf - 9.89% __close + 4.03% entry_SYSCALL_64_fastpath 0.98% syscall_return_via_sysret 0.85% 0xfffffe000008a01b 0.61% 0xfffffe000008a01e 1.10% syscall_return_via_sysret
Kernel menghabiskan hampir 75% dari waktu hanya untuk membuat dan menghapus file descriptor, dan sekitar 16% untuk membuat daftar proses.
Meskipun kami tahu berapa lama untuk panggilan terbuka () dan tutup () untuk setiap proses, kami masih tidak dapat memperkirakan seberapa signifikannya. Kita perlu membandingkan nilai yang diperoleh dengan sesuatu. Mari kita coba melakukan hal yang sama dengan file paling terkenal. Biasanya, ketika Anda perlu membuat daftar proses, ps atau utilitas atas digunakan. Keduanya membaca / proc / <pid>
/ stat dan / proc / <pid>
/ status untuk setiap proses pada sistem.
Mari kita mulai dengan / proc / <pid>
/ status - ini adalah file besar dengan jumlah bidang tetap:
$ time ./task_proc_all status tasks: 50283 real 0m0.455s user 0m0.033s sys 0m0.417s
- 93.84% 0.00% task_proc_all [unknown] [k] 0x0000000000008000 - 0x8000 - 61.20% read - 53.06% entry_SYSCALL_64_fastpath - sys_read - 52.80% vfs_read - 52.22% __vfs_read - seq_read - 50.43% proc_single_show - 50.38% proc_pid_status - 11.34% task_mem + seq_printf + 6.99% seq_printf - 5.77% seq_put_decimal_ull 1.94% strlen + 1.42% num_to_str - 5.73% cpuset_task_status_allowed + seq_printf - 5.37% render_cap_t + 5.31% seq_printf - 5.25% render_sigset_t 0.84% seq_putc 0.73% __task_pid_nr_ns + 0.63% __lock_task_sighand 0.53% hugetlb_report_usage + 0.68% _copy_to_user 1.10% number 1.05% seq_put_decimal_ull 0.84% vsnprintf 0.79% format_decode 0.73% syscall_return_via_sysret 0.52% 0xfffffe000003201b + 20.95% __GI___libc_open + 6.44% __getdents64 + 4.10% __close
Dapat dilihat bahwa hanya sekitar 60% dari waktu yang dihabiskan di dalam panggilan sistem read (). Jika Anda melihat profil lebih dekat, ternyata 45% dari waktu digunakan di dalam fungsi kernel seq_printf, seq_put_decimal_ull. Jadi, mengkonversi dari format biner ke teks cukup mahal. Yang menimbulkan pertanyaan mendasar: apakah kita benar-benar membutuhkan antarmuka teks untuk menarik data dari kernel? Seberapa sering pengguna ingin bekerja dengan data mentah? Dan mengapa utilitas top dan ps harus mengubah data teks ini kembali ke biner?
Mungkin menarik untuk mengetahui seberapa cepat output akan terjadi jika data biner digunakan secara langsung, dan jika tiga panggilan sistem tidak diperlukan.
Sudah ada upaya untuk membuat antarmuka seperti itu. Pada 2004 kami mencoba menggunakan mesin netlink.
[0/2][ANNOUNCE] nproc: netlink access to /proc information (https://lwn.net/Articles/99600/) nproc is an attempt to address the current problems with /proc. In short, it exposes the same information via netlink (implemented for a small subset).
Sayangnya, komunitas belum menunjukkan minat pada pekerjaan ini. Salah satu upaya terakhir untuk memperbaiki situasi terjadi dua tahun lalu.
[PATCH 0/15] task_diag: add a new interface to get information about processes (https://lwn.net/Articles/683371/)
Antarmuka task-diag didasarkan pada prinsip-prinsip berikut:
- Transaksi: mengirim permintaan, menerima respons;
- Format pesan adalah dalam bentuk netlink (sama dengan antarmuka sock_diag: binary and extensible);
- Kemampuan untuk meminta informasi tentang banyak proses dalam satu panggilan;
- Pengelompokan atribut yang dioptimalkan (atribut apa pun dalam grup tidak boleh menambah waktu respons).
Antarmuka ini telah disajikan di beberapa konferensi. Itu diintegrasikan ke pstools, utilitas CRIU, dan David Ahern mengintegrasikan task_diag ke dalam perf sebagai percobaan.
Komunitas pengembangan kernel telah tertarik pada antarmuka task_diag. Subjek utama diskusi adalah pilihan transportasi antara kernel dan ruang pengguna. Gagasan awal menggunakan soket netlink ditolak. Sebagian karena masalah yang belum terselesaikan dalam kode mesin netlink itu sendiri, dan sebagian karena banyak orang berpikir bahwa antarmuka netlink dirancang khusus untuk subsistem jaringan. Kemudian diusulkan untuk menggunakan file transaksional di dalam procfs, yaitu, pengguna membuka file, menulis permintaan ke dalamnya, dan kemudian hanya membaca jawabannya. Seperti biasa, ada lawan dari pendekatan ini. Solusi yang diinginkan semua orang hingga ditemukan.
Mari kita bandingkan kinerja task_diag dengan procfs.
Mesin task_diag memiliki utilitas pengujian yang cocok untuk eksperimen kami. Misalkan kita ingin meminta pengidentifikasi proses dan hak-hak mereka. Di bawah ini adalah output untuk satu proses:
$ ./task_diag_all one -c -p $$ pid 2305 tgid 2305 ppid 2299 sid 2305 pgid 2305 comm bash uid: 1000 1000 1000 1000 gid: 1000 1000 1000 1000 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 0000003fffffffff
Dan sekarang untuk semua proses dalam sistem, yaitu, hal yang sama yang kami lakukan untuk percobaan procfs ketika kami membaca file / proc / pid / status:
$ time ./task_diag_all all -c real 0m0.048s user 0m0.001s sys 0m0.046s
Hanya butuh 0,05 detik untuk mendapatkan data untuk membangun pohon proses. Dan dengan procfs, hanya butuh 0,177 detik hanya untuk membuka satu file untuk setiap proses, dan tanpa membaca data.
Output perf untuk antarmuka task_diag:
- 82.24% 0.00% task_diag_all [kernel.vmlinux] [k] entry_SYSCALL_64_fastpath - entry_SYSCALL_64_fastpath - 81.84% sys_read vfs_read __vfs_read proc_reg_read task_diag_read - taskdiag_dumpit + 33.84% next_tgid 13.06% __task_pid_nr_ns + 6.63% ptrace_may_access + 5.68% from_kuid_munged - 4.19% __get_task_comm 2.90% strncpy 1.29% _raw_spin_lock 3.03% __nla_reserve 1.73% nla_reserve + 1.30% skb_copy_datagram_iter + 1.21% from_kgid_munged 1.12% strncpy
Tidak ada yang menarik dalam daftar itu sendiri kecuali kenyataan bahwa tidak ada fungsi yang jelas yang cocok untuk optimasi.
Mari kita lihat output perf ketika membaca informasi tentang semua proses dalam sistem:
$ perf trace -s ./task_diag_all all -c -q Summary of events: task_diag_all (54326), 185 events, 95.4% syscall calls total min avg max stddev (msec) (msec) (msec) (msec) (%)
Untuk procfs, kita perlu melakukan lebih dari 150.000 panggilan sistem untuk mendapatkan informasi tentang semua proses, dan untuk task_diag - sedikit lebih dari 50.
Mari kita lihat situasi kehidupan nyata. Sebagai contoh, kami ingin menampilkan pohon proses bersama dengan argumen baris perintah untuk masing-masing. Untuk melakukan ini, kita perlu mengeluarkan pid dari proses, pid dari orang tuanya, dan argumen baris perintah sendiri.
Untuk antarmuka task_diag, program mengirim satu permintaan untuk mendapatkan semua parameter sekaligus:
$ time ./task_diag_all all --cmdline -q real 0m0.096s user 0m0.006s sys 0m0.090s
Untuk procfs asli, kita perlu membaca / proc // status dan / proc // cmdline untuk setiap proses:
$ time ./task_proc_all status tasks: 50278 real 0m0.463s user 0m0.030s sys 0m0.427s
$ time ./task_proc_all cmdline tasks: 50281 real 0m0.270s user 0m0.028s sys 0m0.237s
Sangat mudah untuk melihat bahwa task_diag adalah 7 kali lebih cepat daripada procfs (0,096 melawan 0,27 + 0,46). Biasanya, peningkatan kinerja beberapa persen sudah merupakan hasil yang baik, tetapi di sini kecepatannya telah meningkat hampir sebesar urutan besarnya.
Perlu disebutkan juga bahwa pembuatan objek kernel internal juga sangat memengaruhi kinerja. Terutama ketika subsistem memori berada di bawah beban berat. Bandingkan jumlah objek yang dibuat untuk procfs dan task_diag:
$ perf trace --event 'kmem:*alloc*' ./task_proc_all status 2>&1 | grep kmem | wc -l 58184 $ perf trace --event 'kmem:*alloc*' ./task_diag_all all -q 2>&1 | grep kmem | wc -l 188
Dan juga Anda perlu mencari tahu berapa banyak objek yang dibuat ketika memulai proses sederhana, misalnya, utilitas sebenarnya:
$ perf trace --event 'kmem:*alloc*' true 2>&1 | wc -l 94
Procfs menciptakan objek 600 kali lebih banyak daripada task_diag. Ini adalah salah satu alasan mengapa procfs bekerja sangat buruk ketika beban memori berat. Setidaknya karena itu perlu mengoptimalkannya.
Kami berharap artikel ini akan menarik lebih banyak pengembang untuk mengoptimalkan status procfs dari subsistem kernel.
Terima kasih banyak kepada David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman dan banyak lainnya yang telah membantu mengembangkan dan meningkatkan antarmuka task_diag.
Terima kasih kepada cromer , k001 dan Stanislav Kinsbursky untuk bantuan dalam menulis artikel ini.
Referensi