ما مدى كفاءة نظام الملفات الظاهري procfs وهل من الممكن تحسينه

نظام الملفات proc (فيما يلي ببساطة procfs) هو نظام ملفات افتراضي يوفر معلومات حول العمليات. إنها مثال "جيد" للواجهات التي تتبع النموذج "كل شيء ملف". تم تطوير Procfs منذ وقت طويل جدًا: في الوقت الذي كانت فيه الخوادم تخدم عشرات العمليات في المتوسط ​​، عند فتح ملف وقراءة معلومات عملية لم تكن مشكلة. ومع ذلك ، لا يقف الوقت ثابتًا ، والآن تخدم الخوادم مئات الآلاف ، أو حتى المزيد من العمليات في نفس الوقت. في هذا السياق ، لم تعد فكرة "فتح ملف لكل عملية من أجل طرح البيانات ذات الأهمية" جذابة للغاية ، وأول ما يتبادر إلى الذهن لتسريع القراءة هو الحصول على معلومات حول مجموعة من العمليات في تكرار واحد. في هذه المقالة ، سنحاول العثور على عناصر procfs التي يمكن تحسينها.


الصورة


جاءت فكرة تحسين procfs عندما اكتشفنا أن CRIU تقضي وقتًا طويلاً في قراءة ملفات procfs. لقد رأينا كيف تم حل مشكلة مشابهة للمقابس ، وقررنا أن نفعل شيئًا مشابهًا لواجهة sock-diag ، ولكن فقط لل procfs. بالطبع ، افترضنا كم سيكون من الصعب تغيير الواجهة القديمة الراسخة في النواة ، لإقناع المجتمع بأن اللعبة تستحق الشمعة ... وفوجئنا بسرور بعدد الأشخاص الذين دعموا إنشاء الواجهة الجديدة. بالمعنى الدقيق للكلمة ، لم يعرف أحد كيف يجب أن تبدو الواجهة الجديدة ، ولكن ليس هناك شك في أن procfs لا يلبي متطلبات الأداء الحالية. على سبيل المثال ، هذا السيناريو: يستجيب الخادم لطلبات لفترة طويلة جدًا ، ويظهر vmstat أن الذاكرة قد تم تبديلها ، وأن "ps ax" يبدأ من 10 ثوانٍ أو أكثر ، ولا يعرض الجزء العلوي أي شيء على الإطلاق. في هذه المقالة لن ننظر في أي واجهة جديدة محددة ، بل سنحاول وصف المشاكل وحلولها.


يتم تمثيل كل عملية تنفيذ procfs بواسطة الدليل / proc / <pid> .
يوجد في كل دليل من هذا القبيل العديد من الملفات والدلائل الفرعية التي توفر الوصول إلى معلومات معينة حول العملية. بيانات الدلائل الفرعية حسب الميزة. على سبيل المثال ( $$ هو متغير غلاف خاص يتم توسيعه في pid - معرف العملية الحالية):


 $ 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 

جميع هذه الملفات تنتج البيانات بتنسيقات مختلفة. معظمها بتنسيق ASCII يمكن للمستخدمين إدراكه بسهولة. حسنًا ، من السهل تقريبًا:


 $ 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 

لفهم معنى كل عنصر من هذه المجموعة ، سيتعين على القارئ فتح man proc (5) ، أو توثيق النواة. على سبيل المثال ، العنصر الثاني هو اسم الملف القابل للتنفيذ بين قوسين ، والعنصر التاسع عشر هو القيمة الحالية لأولوية التنفيذ (لطيفة).


بعض الملفات قابلة للقراءة من تلقاء نفسها:


 $ cat /proc/$$/status | head -n 5 Name: bash Umask: 0002 State: S (sleeping) Tgid: 24293 Ngid: 0 

ولكن كم مرة يقرأ المستخدمون المعلومات مباشرة من ملفات procfs؟ ما المدة التي تحتاجها النواة لتحويل البيانات الثنائية إلى تنسيق نصي؟ ما هي النفقات العامة؟ ما مدى ملاءمة هذه الواجهة لبرامج مراقبة الحالة ، وكم من الوقت يقضيونه لمعالجة هذه البيانات النصية؟ ما مدى أهمية هذا التنفيذ البطيء في حالات الطوارئ؟


على الأرجح ، لن يكون من الخطأ أن نقول أن المستخدمين يفضلون برامج مثل القمة أو ps ، بدلاً من قراءة البيانات من procfs مباشرة.


للإجابة على الأسئلة المتبقية ، سنجري العديد من التجارب. أولاً ، ابحث عن المكان الذي تقضي فيه kernel الوقت لإنشاء ملفات procfs.


للحصول على معلومات معينة من جميع العمليات في النظام ، سيتعين علينا مراجعة الدليل / proc وتحديد جميع الأدلة الفرعية التي يتم تمثيل اسمها بأرقام عشرية. بعد ذلك ، نحتاج في كل واحد منهم إلى فتح الملف وقراءته وإغلاقه.


في المجموع ، سنقوم بإجراء ثلاث مكالمات نظام ، أحدها سيقوم بإنشاء واصف ملف (في kernel ، يرتبط واصف الملف بمجموعة من الكائنات الداخلية التي يتم تخصيص ذاكرة إضافية لها). إن مكالمات النظام المفتوحة () والإغلاق () نفسها لا تعطينا أي معلومات ، لذلك يمكن أن تعزى إلى النفقات العامة لواجهة procfs.


دعنا فقط نحاول فتح () وإغلاق () لكل عملية في النظام ، لكننا لن نقرأ محتويات الملفات:


 $ 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 - أداة صغيرة ، يمكن العثور على رمزها على الرابط أدناه


لا يهم الملف الذي سيتم فتحه ، حيث يتم إنشاء البيانات الحقيقية فقط في وقت القراءة ().


انظر الآن إلى ناتج محلل الأداء المثالي:


 - 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 

تقضي النواة 75٪ من الوقت تقريبًا لإنشاء وحذف واصف الملف ، وحوالي 16٪ لسرد العمليات.


على الرغم من أننا نعرف المدة التي تستغرقها مكالمات مفتوحة () وإغلاق () لكل عملية ، لا يزال يتعذر علينا تقدير مدى أهميتها. نحن بحاجة لمقارنة القيم التي تم الحصول عليها بشيء. دعونا نحاول أن نفعل نفس الشيء مع الملفات الأكثر شهرة. عادة ، عندما تحتاج إلى سرد العمليات ، يتم استخدام ps أو أعلى أداة. كلاهما يقرأ / proc / <pid> / stat و / proc / <pid> / الحالة لكل عملية على النظام.


لنبدأ بـ / proc / <pid> / status - هذا ملف ضخم يحتوي على عدد ثابت من الحقول:


 $ 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 

يمكن ملاحظة أن 60٪ فقط من الوقت الذي يقضيه داخل مكالمة النظام read (). إذا نظرت إلى الملف الشخصي عن كثب ، اتضح أنه يتم استخدام 45 ٪ من الوقت داخل وظائف kernel seq_printf ، seq_put_decimal_ull. لذا ، فإن التحويل من تنسيق ثنائي إلى تنسيق نصي عملية مكلفة للغاية. ما الذي يطرح السؤال الراسخ: هل نحتاج حقًا إلى واجهة نصية لسحب البيانات من النواة؟ كم مرة يرغب المستخدمون في العمل مع البيانات الأولية؟ ولماذا يجب على أدوات المساعدة ps و top تحويل هذه البيانات النصية إلى بيانات ثنائية؟


قد يكون من المثير للاهتمام معرفة مدى سرعة الناتج إذا تم استخدام البيانات الثنائية مباشرة ، وإذا لم تكن هناك حاجة إلى ثلاث مكالمات نظام.


كانت هناك بالفعل محاولات لإنشاء مثل هذه الواجهة. في عام 2004 حاولنا استخدام محرك 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). 

لسوء الحظ ، لم يبد المجتمع الكثير من الاهتمام بهذا العمل. حدثت واحدة من المحاولات الأخيرة لتصحيح الوضع قبل عامين.


 [PATCH 0/15] task_diag: add a new interface to get information about processes (https://lwn.net/Articles/683371/) 

تعتمد واجهة مخطط المهام على المبادئ التالية:


  • المعاملة: أرسلت طلبًا ، تلقت ردًا ؛
  • شكل الرسائل في شكل netlink (نفس الواجهة sock_diag: ثنائي وقابل للتوسيع) ؛
  • القدرة على طلب معلومات حول العديد من العمليات في مكالمة واحدة ؛
  • التجميع المحسّن للسمات (يجب ألا تزيد أي سمة في المجموعة من وقت الاستجابة).

تم عرض هذه الواجهة في العديد من المؤتمرات. تم دمجها في pstools ، والمرافق CRIU ، و David Ahern دمج task_diag في الأداء كتجربة.


أصبح مجتمع تطوير kernel مهتمًا بواجهة Task_diag. كان الموضوع الرئيسي للمناقشة هو اختيار النقل بين النواة ومساحة المستخدم. تم رفض الفكرة الأولية لاستخدام مآخذ netlink. يرجع ذلك جزئيًا إلى المشكلات التي لم يتم حلها في كود محرك netlink نفسه ، وجزئيًا لأن العديد من الناس يعتقدون أن واجهة netlink مصممة حصريًا للنظام الفرعي للشبكة. ثم تم اقتراح استخدام ملفات المعاملات داخل procfs ، أي أن المستخدم يفتح الملف ، يكتب الطلب فيه ، ثم يقرأ الجواب ببساطة. كالعادة ، كان هناك معارضو لهذا النهج. الحل الذي يريده الجميع حتى يتم العثور عليه.


دعونا نقارن بين أداء المهام والمهام.


يحتوي محرك task_diag على أداة اختبار مناسبة تمامًا لتجاربنا. افترض أننا نريد طلب معرفات العملية وحقوقهم. فيما يلي إخراج عملية واحدة:


 $ ./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 

والآن لجميع العمليات في النظام ، أي نفس الشيء الذي قمنا به لتجربة procfs عندما نقرأ ملف / proc / pid / status:


 $ time ./task_diag_all all -c real 0m0.048s user 0m0.001s sys 0m0.046s 

استغرق الأمر 0.05 ثانية فقط للحصول على البيانات لبناء شجرة العملية. ومع procfs ، استغرق الأمر 0.177 ثانية فقط لفتح ملف واحد لكل عملية ، وبدون قراءة البيانات.


إخراج الأداء لواجهة 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 

لا يوجد شيء مثير للاهتمام في القائمة نفسها باستثناء حقيقة أنه لا توجد وظائف واضحة مناسبة للتحسين.


دعونا نلقي نظرة على ناتج الأداء عند قراءة معلومات حول جميع العمليات في النظام:


  $ 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) (%) --------------- -------- --------- --------- --------- --------- ------ read 49 40.209 0.002 0.821 4.126 9.50% mmap 11 0.051 0.003 0.005 0.007 9.94% mprotect 8 0.047 0.003 0.006 0.009 10.42% openat 5 0.042 0.005 0.008 0.020 34.86% munmap 1 0.014 0.014 0.014 0.014 0.00% fstat 4 0.006 0.001 0.002 0.002 10.47% access 1 0.006 0.006 0.006 0.006 0.00% close 4 0.004 0.001 0.001 0.001 2.11% write 1 0.003 0.003 0.003 0.003 0.00% rt_sigaction 2 0.003 0.001 0.001 0.002 15.43% brk 1 0.002 0.002 0.002 0.002 0.00% prlimit64 1 0.001 0.001 0.001 0.001 0.00% arch_prctl 1 0.001 0.001 0.001 0.001 0.00% rt_sigprocmask 1 0.001 0.001 0.001 0.001 0.00% set_robust_list 1 0.001 0.001 0.001 0.001 0.00% set_tid_address 1 0.001 0.001 0.001 0.001 0.00% 

بالنسبة إلى procfs ، نحتاج إلى إجراء أكثر من 150.000 مكالمة نظام للحصول على معلومات حول جميع العمليات ، وبالنسبة إلى task_diag - ما يزيد قليلاً عن 50.


دعونا نلقي نظرة على مواقف الحياة الحقيقية. على سبيل المثال ، نريد عرض شجرة عملية مع وسيطات سطر الأوامر لكل منها. للقيام بذلك ، نحتاج إلى سحب pid للعملية ، و pid الخاص بها ، وحجج سطر الأوامر نفسها.


بالنسبة لواجهة task_diag ، يرسل البرنامج طلبًا واحدًا للحصول على جميع المعلمات دفعة واحدة:


 $ time ./task_diag_all all --cmdline -q real 0m0.096s user 0m0.006s sys 0m0.090s 

بالنسبة إلى procfs الأصلية ، نحتاج إلى قراءة / proc // status و / proc // cmdline لكل عملية:

 $ 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 

من السهل أن تلاحظ أن task_diag أسرع 7 مرات من procfs (0.096 مقابل 0.27 + 0.46). عادة ، يعد تحسن الأداء بنسبة عدة في المائة نتيجة جيدة بالفعل ، ولكن هنا زادت السرعة بما يقرب من الحجم.


ومن الجدير بالذكر أيضًا أن إنشاء كائنات النواة الداخلية يؤثر أيضًا بشكل كبير على الأداء. خاصة عندما يكون النظام الفرعي للذاكرة تحت عبء ثقيل. قارن عدد العناصر التي تم إنشاؤها لـ procfs و 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 

وتحتاج أيضًا إلى معرفة عدد الكائنات التي يتم إنشاؤها عند بدء عملية بسيطة ، على سبيل المثال ، الأداة المساعدة الحقيقية:


 $ perf trace --event 'kmem:*alloc*' true 2>&1 | wc -l 94 

ينشئ Procfs كائنات 600 مرة أكثر من task_diag. هذا أحد الأسباب التي تجعل procfs يعمل بشكل سيئ للغاية عندما يكون حمل الذاكرة ثقيلًا. لذلك على الأقل يستحق التحسين.


نأمل أن تجذب المقالة المزيد من المطورين لتحسين حالة procfs للنظام الفرعي kernel.


شكرًا جزيلًا لكل من David Ahern و Andy Lutomirski و Stephen Hemming و Oleg Nesterov و W. Trevor King و Arnd Bergmann و Eric W. Biederman وغيرهم ممن ساعدوا في تطوير وتحسين واجهة المهام.


بفضل cromer و k001 و Stanislav Kinsbursky للمساعدة في كتابة هذا المقال.


المراجع


Source: https://habr.com/ru/post/ar418715/


All Articles