procfs虚拟文件系统的效率如何,并且有可能对其进行优化

proc文件系统(以下简称为procfs)是提供有关进程信息的虚拟文件系统。 她是遵循“一切都是文件”范式的接口的“很好”示例。 Procfs是很久以前开发的:当时服务器平均为数十个进程提供服务,而打开文件并读取进程信息则不是问题。 但是,时间并不会停滞不前,现在服务器可以同时服务数十万个甚至更多的进程。 在这种情况下,“为每个进程打开文件以减去感兴趣的数据”的想法不再那么吸引人了,想到加快读取速度的第一件事就是一次迭代地获取有关一组进程的信息。 在本文中,我们将尝试找到可以优化的procfs元素。


图片


当我们发现CRIU仅花费大量时间读取procfs文件时,就产生了改进procfs的想法。 我们看到了如何解决套接字的类似问题,并决定做一些类似于sock-diag接口的操作,但只针对procfs。 当然,我们假设要更改内核中长期存在且完善的界面,使社区相信游戏值得接受......这将是多么困难......我们为支持创建新界面的人数而感到惊讶。 严格来说,没有人知道新界面的外观,但是毫无疑问,procfs不能满足当前的性能要求。 例如,这种情况:服务器对请求的响应时间过长,vmstat显示内存已进行交换,并且“ ps ax”从10秒或更长时间开始,top根本不显示任何内容。 在本文中,我们将不考虑任何特定的新接口;而是,我们将尝试描述问题及其解决方案。


每个执行的procfs进程都由/ proc / <pid>目录表示。
在每个这样的目录中,有许多文件和子目录可提供对有关该过程的某些信息的访问。 子目录按功能分组数据。 例如( $$是一个特殊的shell变量,以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)或内核文档。 例如,第二个元素是括号中的可执行文件的名称,第十九个元素是执行优先级的当前值(nice)。


某些文件本身很容易阅读:


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

但是,用户有多少次直接从procfs文件中读取信息? 内核需要多长时间将二进制数据转换为文本格式? procfs的开销是多少? 该界面对于状态监控程序有多方便,它们花费多少时间来处理此文本数据? 在紧急情况下如此缓慢的实施有多重要?


很有可能说用户喜欢使用top或ps之类的程序,而不是直接从procfs中读取数据,这不是错误的。


为了回答其余问题,我们将进行几个实验。 首先,找到内核在哪里花费时间来生成procfs文件。


为了从系统中的所有进程中获取某些信息,我们将必须遍历/ proc /目录并选择其名称以十进制数字表示的所有子目录。 然后,在每个文件中,我们需要打开文件,读取文件并将其关闭。


总的来说,我们将进行三个系统调用,其中一个将创建文件描述符(在内核中,文件描述符与一组内部对象相关联,并为其分配了额外的内存)。 open()和close()系统调用本身不会提供任何信息,因此可以将它们归因于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-一个小型实用程序,其代码可在下面的链接中找到


打开哪个文件都没有关系,因为实际数据仅在read()时生成。


现在看一下perf core profiler的输出:


 - 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%的时间来列出进程。


尽管我们知道每个进程的open()和close()调用需要多长时间,但是我们仍然无法估计它的重要性。 我们需要将获得的值与某些值进行比较。 让我们尝试对最著名的文件执行相同的操作。 通常,当您需要列出进程时,将使用ps或top实用程序。 它们都为系统上的每个进程读取/ 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%的时间是在内核函数seq_printf,seq_put_decimal_ull中使用的。 因此,从二进制格式转换为文本格式是一项非常昂贵的操作。 哪个提出了一个有根据的问题:我们真的需要一个文本接口来从内核中提取数据吗? 用户想多久使用一次原始数据? 为什么top和ps实用程序必须将此文本数据转换回二进制文件?


如果直接使用二进制数据,并且不需要三个系统调用,那么知道输出将有多快可能会很有趣。


已经尝试创建这样的接口。 在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集成到perf中。


内核开发社区已经对task_diag接口感兴趣。 讨论的主要主题是内核和用户空间之间传输的选择。 使用netlink套接字的最初想法被拒绝了。 部分原因是netlink引擎本身的代码中未解决的问题,部分原因是许多人认为netlink接口是专门为网络子系统设计的。 然后提出了在procfs中使用事务性文件的方法,即用户打开文件,将请求写入其中,然后简单地读取答案。 像往常一样,有人反对这种方法。 一个人人都希望找到的解决方案。


让我们将task_diag性能与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 

现在,对于系统中的所有进程,也就是我们读取/ proc / pid / status文件时对procfs实验所做的相同操作:


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

仅花费0.05秒即可获得数据来构建流程树。 使用procfs,每个过程只需要花费0.177秒即可打开一个文件,而无需读取数据。


task_diag接口的Perf输出:


 - 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输出:


  $ 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 //状态和/ 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比procfs快7倍(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创建的对象比task_diag多600倍。 这是当内存负载很重时procfs如此糟糕的原因之一。 至少因此值得对其进行优化。


我们希望本文将吸引更多的开发人员来优化内核子系统的procfs状态。


非常感谢David Ahern,Andy Lutomirski,Stephen Hemming,Oleg Nesterov,W.Trevor King,Arnd Bergmann,Eric W.Biederman以及其他许多帮助开发和改进task_diag界面的人。


感谢cromerk001和Stanislav Kinsbursky撰写本文的帮助。


参考文献


Source: https://habr.com/ru/post/zh-CN418715/


All Articles