Das proc-Dateisystem (im Folgenden einfach procfs) ist ein virtuelles Dateisystem, das Informationen über Prozesse bereitstellt. Sie ist ein „gutes“ Beispiel für Schnittstellen, die dem Paradigma „Alles ist eine Datei“ folgen. Procfs wurde vor sehr langer Zeit entwickelt: Zu einer Zeit, als Server durchschnittlich Dutzende von Prozessen bedienten, war das Öffnen einer Datei und das Lesen von Prozessinformationen kein Problem. Die Zeit steht jedoch nicht still und jetzt bedienen Server Hunderttausende oder sogar mehr Prozesse gleichzeitig. In diesem Zusammenhang sieht die Idee, „für jeden Prozess eine Datei zu öffnen, um die interessierenden Daten zu subtrahieren“, nicht mehr so attraktiv aus. Um das Lesen zu beschleunigen, müssen Sie zunächst Informationen über eine Gruppe von Prozessen in einer Iteration abrufen. In diesem Artikel werden wir versuchen, procfs-Elemente zu finden, die optimiert werden können.

Die Idee, procfs zu verbessern, entstand, als wir entdeckten, dass CRIU viel Zeit damit verbringt, nur procfs-Dateien zu lesen. Wir haben gesehen, wie ein ähnliches Problem für Sockets gelöst wurde, und beschlossen, etwas Ähnliches wie die Sockendiag-Schnittstelle zu tun, jedoch nur für procfs. Natürlich gingen wir davon aus, wie schwierig es sein würde, die langjährige und gut etablierte Benutzeroberfläche im Kernel zu ändern, um die Community davon zu überzeugen, dass das Spiel die Kerze wert ist ... und wir waren angenehm überrascht von der Anzahl der Personen, die die Erstellung der neuen Benutzeroberfläche unterstützten. Genau genommen wusste niemand, wie die neue Benutzeroberfläche aussehen sollte, aber es besteht kein Zweifel, dass procfs die aktuellen Leistungsanforderungen nicht erfüllt. Beispiel: Dieses Szenario: Der Server antwortet zu lange auf Anforderungen, vmstat zeigt an, dass der Speicher ausgelagert wurde, und "ps ax" wird ab 10 Sekunden oder länger gestartet. Top zeigt überhaupt nichts an. In diesem Artikel werden wir keine spezifische neue Schnittstelle betrachten, sondern versuchen, die Probleme und ihre Lösungen zu beschreiben.
Jeder ausführende procfs-Prozess wird durch das Verzeichnis / proc / <pid>
.
In jedem dieser Verzeichnisse befinden sich viele Dateien und Unterverzeichnisse, die Zugriff auf bestimmte Informationen über den Prozess bieten. Unterverzeichnisse gruppieren Daten nach Funktionen. Zum Beispiel ( $$
ist eine spezielle Shell-Variable, die in pid erweitert wird - der Kennung des aktuellen Prozesses):
$ 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
Alle diese Dateien geben Daten in verschiedenen Formaten aus. Die meisten sind im ASCII-Format und werden vom Menschen leicht wahrgenommen. Na ja, fast einfach:
$ 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
Um zu verstehen, was jedes Element dieses Satzes bedeutet, muss der Leser man proc (5) oder die Kerneldokumentation öffnen. Das zweite Element ist beispielsweise der Name der ausführbaren Datei in Klammern, und das neunzehnte Element ist der aktuelle Wert der Ausführungspriorität (nett).
Einige Dateien sind für sich gut lesbar:
$ cat /proc/$$/status | head -n 5 Name: bash Umask: 0002 State: S (sleeping) Tgid: 24293 Ngid: 0
Aber wie oft lesen Benutzer Informationen direkt aus procfs-Dateien? Wie lange braucht der Kernel, um Binärdaten in das Textformat zu konvertieren? Was ist der Overhead von procfs? Wie bequem ist diese Schnittstelle für Statusüberwachungsprogramme und wie viel Zeit verbringen sie mit der Verarbeitung dieser Textdaten? Wie kritisch ist eine so langsame Implementierung in Notsituationen?
Höchstwahrscheinlich ist es kein Fehler zu sagen, dass Benutzer Programme wie top oder ps bevorzugen, anstatt Daten aus procfs direkt zu lesen.
Um die verbleibenden Fragen zu beantworten, werden wir mehrere Experimente durchführen. Stellen Sie zunächst fest, wo der Kernel Zeit zum Generieren von procfs-Dateien verbringt.
Um bestimmte Informationen von allen Prozessen im System zu erhalten, müssen wir das Verzeichnis / proc / durchsuchen und alle Unterverzeichnisse auswählen, deren Name durch Dezimalstellen dargestellt wird. Dann müssen wir in jedem von ihnen die Datei öffnen, lesen und schließen.
Insgesamt werden drei Systemaufrufe durchgeführt, von denen einer einen Dateideskriptor erstellt (im Kernel ist ein Dateideskriptor einer Reihe interner Objekte zugeordnet, für die zusätzlicher Speicher zugewiesen ist). Die Systemaufrufe open () und close () selbst geben uns keine Informationen, sodass sie dem Overhead der procfs-Schnittstelle zugeordnet werden können.
Versuchen wir einfach, für jeden Prozess im System zu öffnen () und zu schließen (), aber wir werden den Inhalt der Dateien nicht lesen:
$ 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 - ein kleines Dienstprogramm, dessen Code unter dem folgenden Link zu finden ist
Es spielt keine Rolle, welche Datei geöffnet werden soll, da echte Daten nur zum Zeitpunkt von read () generiert werden.
Schauen Sie sich nun die Ausgabe des Perf Core Profilers an:
- 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
Der Kernel verbringt fast 75% der Zeit damit, den Dateideskriptor zu erstellen und zu löschen, und etwa 16% damit, die Prozesse aufzulisten.
Obwohl wir wissen, wie lange es dauert, bis open () und close () für jeden Prozess aufgerufen werden, können wir immer noch nicht abschätzen, wie wichtig er ist. Wir müssen die erhaltenen Werte mit etwas vergleichen. Versuchen wir, dasselbe mit den bekanntesten Dateien zu tun. Wenn Sie die Prozesse auflisten müssen, wird normalerweise ps oder top verwendet. Beide lesen / proc / <pid>
/ stat und / proc / <pid>
/ status für jeden Prozess auf dem System.
Beginnen wir mit / proc / <pid>
/ status - dies ist eine massive Datei mit einer festen Anzahl von Feldern:
$ 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
Es ist ersichtlich, dass nur etwa 60% der Zeit im Systemaufruf read () verbracht wurden. Wenn Sie sich das Profil genauer ansehen, stellt sich heraus, dass 45% der Zeit in den Kernelfunktionen seq_printf, seq_put_decimal_ull verwendet wird. Das Konvertieren vom Binär- in das Textformat ist daher eine ziemlich kostspielige Operation. Was die begründete Frage aufwirft: Brauchen wir wirklich eine Textschnittstelle, um Daten aus dem Kernel zu ziehen? Wie oft möchten Benutzer mit Rohdaten arbeiten? Und warum müssen die Dienstprogramme top und ps diese Textdaten wieder in Binärdaten konvertieren?
Es wäre wahrscheinlich interessant zu wissen, wie viel schneller die Ausgabe wäre, wenn Binärdaten direkt verwendet würden und drei Systemaufrufe nicht erforderlich wären.
Es wurden bereits Versuche unternommen, eine solche Schnittstelle zu erstellen. Im Jahr 2004 haben wir versucht, die Netlink-Engine zu verwenden.
[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).
Leider hat die Community kein großes Interesse an dieser Arbeit gezeigt. Einer der letzten Versuche, die Situation zu korrigieren, fand vor zwei Jahren statt.
[PATCH 0/15] task_diag: add a new interface to get information about processes (https://lwn.net/Articles/683371/)
Die Task-Diag-Schnittstelle basiert auf folgenden Prinzipien:
- Transaktion: Anfrage gesendet, Antwort erhalten;
- Das Format der Nachrichten hat die Form eines Netlinks (dasselbe wie die Schnittstelle sock_diag: binär und erweiterbar).
- Die Möglichkeit, Informationen zu vielen Prozessen in einem Aufruf anzufordern.
- Optimierte Gruppierung von Attributen (jedes Attribut in der Gruppe sollte die Antwortzeit nicht verlängern).
Diese Schnittstelle wurde auf mehreren Konferenzen vorgestellt. Es wurde in pstools, CRIU-Dienstprogramme und David Ahern integriert, um task_diag als Experiment in perf zu integrieren.
Die Kernel-Entwicklergemeinde hat sich für die Schnittstelle task_diag interessiert. Das Hauptthema der Diskussion war die Wahl des Transports zwischen dem Kernel und dem Benutzerraum. Die ursprüngliche Idee, Netlink-Sockets zu verwenden, wurde abgelehnt. Teilweise aufgrund ungelöster Probleme im Code der Netlink-Engine selbst und teilweise, weil viele Leute denken, dass die Netlink-Schnittstelle ausschließlich für das Netzwerk-Subsystem entwickelt wurde. Dann wurde vorgeschlagen, Transaktionsdateien in procfs zu verwenden, dh der Benutzer öffnet die Datei, schreibt die Anforderung hinein und liest dann einfach die Antwort. Wie üblich gab es Gegner dieses Ansatzes. Eine Lösung, die jeder gerne hätte, bis sie gefunden ist.
Vergleichen wir die Leistung von task_diag mit procfs.
Die task_diag-Engine verfügt über ein Testdienstprogramm, das für unsere Experimente gut geeignet ist. Angenommen, wir möchten Prozesskennungen und ihre Rechte anfordern. Unten ist die Ausgabe für einen Prozess:
$ ./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
Und jetzt für alle Prozesse im System, das heißt, dasselbe, was wir für das procfs-Experiment getan haben, als wir die Datei / proc / pid / status gelesen haben:
$ time ./task_diag_all all -c real 0m0.048s user 0m0.001s sys 0m0.046s
Es dauerte nur 0,05 Sekunden, bis die Daten zum Erstellen des Prozessbaums abgerufen wurden. Und mit procfs dauerte es nur 0,177 Sekunden, um eine Datei für jeden Prozess zu öffnen, ohne Daten zu lesen.
Perf-Ausgabe für die Schnittstelle 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
Die Auflistung selbst enthält nichts Interessantes außer der Tatsache, dass es keine offensichtlichen Funktionen gibt, die für die Optimierung geeignet sind.
Schauen wir uns die Perf-Ausgabe an, wenn wir Informationen zu allen Prozessen im System lesen:
$ 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) (%)
Für procfs müssen wir mehr als 150.000 Systemaufrufe durchführen, um Informationen über alle Prozesse zu erhalten, und für task_diag - etwas mehr als 50.
Schauen wir uns reale Situationen an. Zum Beispiel möchten wir einen Prozessbaum zusammen mit Befehlszeilenargumenten für jedes anzeigen. Dazu müssen wir die PID des Prozesses, die PID des übergeordneten Prozesses und die Befehlszeilenargumente selbst herausziehen.
Für die Schnittstelle task_diag sendet das Programm eine Anforderung, um alle Parameter gleichzeitig abzurufen:
$ time ./task_diag_all all --cmdline -q real 0m0.096s user 0m0.006s sys 0m0.090s
Für die ursprünglichen Prozesse müssen wir für jeden Prozess / proc // status und / proc // cmdline lesen:
$ 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
Es ist leicht zu bemerken, dass task_diag siebenmal schneller ist als procfs (0,096 gegenüber 0,27 + 0,46). Normalerweise ist eine Leistungsverbesserung von mehreren Prozent bereits ein gutes Ergebnis, aber hier hat sich die Geschwindigkeit um fast eine Größenordnung erhöht.
Erwähnenswert ist auch, dass die Erstellung interner Kernelobjekte die Leistung stark beeinflusst. Insbesondere wenn das Speichersubsystem stark ausgelastet ist. Vergleichen Sie die Anzahl der für procfs und task_diag erstellten Objekte:
$ 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
Außerdem müssen Sie herausfinden, wie viele Objekte beim Starten eines einfachen Prozesses erstellt werden, z. B. das wahre Dienstprogramm:
$ perf trace --event 'kmem:*alloc*' true 2>&1 | wc -l 94
Procfs erstellt 600-mal mehr Objekte als task_diag. Dies ist einer der Gründe, warum procfs bei hoher Speicherauslastung so schlecht funktioniert. Zumindest lohnt es sich deshalb, es zu optimieren.
Wir hoffen, dass der Artikel mehr Entwickler anzieht, um den procfs-Status des Kernel-Subsystems zu optimieren.
Vielen Dank an David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman und viele andere, die zur Entwicklung und Verbesserung der Schnittstelle task_diag beigetragen haben.
Vielen Dank an cromer , k001 und Stanislav Kinsbursky für die Hilfe beim Schreiben dieses Artikels.
Referenzen