Warum gibt es unter Linux / BSD kein generisches Batching-System?

17

Hintergrund:

Der Systemaufruf-Overhead ist viel größer als der Funktionsaufruf-Overhead (Schätzungen reichen von 20-100x), was hauptsächlich auf die Kontextumschaltung vom Benutzerbereich zum Kernelbereich und zurück zurückzuführen ist. Inline-Funktionen werden häufig verwendet, um Funktionsaufruf-Overhead zu sparen, und Funktionsaufrufe sind viel billiger als Syscalls. Es liegt auf der Hand, dass Entwickler einen Teil des Systemaufruf-Overheads vermeiden möchten, indem sie so viel Kernel-Betrieb wie möglich in einem Systemaufruf erledigen.

Problem:

Dies hat eine Menge von (? Überflüssig) Systemaufrufen wie geschaffen sendmmsg () , recvmmsg () sowie den chdir, offen, lseek und / oder Symlink Kombinationen wie: openat, mkdirat, mknodat, fchownat, futimesat, newfstatat, unlinkat, fchdir, ftruncate, fchmod, renameat, linkat, symlinkat, readlinkat, fchmodat, faccessat, lsetxattr, fsetxattr, execveat, lgetxattr, llistxattr, lremovexattr, fremovexattr, flistxattr, fgetxattr, pread, pwriteetc ...

Nun hat Linux hinzugefügt, copy_file_range()das offenbar Lese- und Schreibsyscalls kombiniert. Es ist nur eine Frage der Zeit, bis fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () und lcopy_file_rangeat () angezeigt werden Mehr. OK, Linus und die verschiedenen BSD-Entwickler ließen es nicht so weit kommen, aber mein Punkt ist, dass, wenn es einen Batch-Systemaufruf gäbe, all diese (die meisten?) Im Benutzerraum implementiert werden könnten und die Kernel-Komplexität reduziert werden könnte, ohne viel hinzuzufügen wenn irgendein Aufwand auf der libc-Seite.

Es wurden viele komplexe Lösungen vorgeschlagen, die einen speziellen Syscall-Thread für nicht blockierende Syscalls zur Stapelverarbeitung von Syscalls enthalten. Diese Methoden erhöhen jedoch die Komplexität sowohl des Kernels als auch des Benutzerbereichs auf die gleiche Weise wie libxcb im Vergleich zu libX11 (die asynchronen Aufrufe erfordern viel mehr Setup).

Lösung?:

Ein generisches Batching-System. Dies würde die größten Kosten (mehrere Moduswechsel) verringern, ohne die Komplexität, die mit einem spezialisierten Kernel-Thread verbunden ist (obwohl diese Funktionalität später hinzugefügt werden könnte).

Grundsätzlich gibt es im socketcall () syscall bereits eine gute Basis für einen Prototyp. Erweitern Sie es einfach von einem Array von Argumenten zu einem Array von Rückgaben, einem Zeiger auf ein Array von Argumenten (einschließlich der Syscall-Nummer), der Anzahl der Syscalls und einem Flags-Argument ... so etwas wie:

batch(void *returns, void *args, long ncalls, long flags);

Ein Hauptunterschied wäre, dass die Argumente der Einfachheit halber wahrscheinlich alle Zeiger sein müssten, damit die Ergebnisse früherer Systemaufrufe von nachfolgenden Systemaufrufen verwendet werden können (zum Beispiel der Dateideskriptor von open()zur Verwendung in read()/ write()).

Einige mögliche Vorteile:

  • weniger User Space -> Kernel Space -> User Space Switching
  • mögliche Compiler-Schalter -fcombine-syscalls, um zu versuchen, automatisch zu stapeln
  • optionales Flag für asynchronen Betrieb (fd zurückgeben, um sofort zu sehen)
  • Fähigkeit zur Implementierung zukünftiger kombinierter Syscall-Funktionen im Userspace

Frage:

Ist es möglich, ein Batching-System zu implementieren?

  • Vermisse ich einige offensichtliche Fallstricke?
  • Überschätze ich die Vorteile?

Lohnt es sich für mich, ein Batching-System zu implementieren (ich arbeite nicht bei Intel, Google oder Redhat)?

  • Ich habe meinen eigenen Kernel schon mal gepatcht, fürchte mich aber vor dem Umgang mit dem LKML.
  • Die Geschichte hat gezeigt, dass selbst wenn etwas für "normale" Benutzer (Nicht-Firmen-Endbenutzer ohne Git-Schreibzugriff) von großem Nutzen ist, es möglicherweise nie im Upstream akzeptiert wird (unionfs, aufs, cryptodev, tuxonice, etc ...)

Verweise:

Technosaurus
quelle
4
Ein ziemlich offensichtliches Problem, das ich sehe, ist, dass der Kernel die Kontrolle über die Zeit und den Raum aufgibt, die für einen Systemaufruf benötigt werden, sowie über die Komplexität der Operationen eines einzelnen Systemaufrufs. Grundsätzlich haben Sie einen Systemaufruf erstellt, der beliebige, unbegrenzte Mengen an Kernelspeicher zuweisen, für eine beliebige, unbegrenzte Zeit ausgeführt werden kann und beliebig komplex sein kann. Indem Sie batchSyscalls in batchSyscalls verschachteln, können Sie einen beliebig tiefen Aufrufbaum beliebiger Syscalls erstellen. Grundsätzlich können Sie Ihre gesamte Anwendung in einem einzigen Systemaufruf zusammenfassen.
Jörg W Mittag
@ JörgWMittag - Ich schlage nicht vor, dass diese parallel ausgeführt werden. Daher ist die Menge des verwendeten Kernelspeichers nicht mehr als der schwerste Systemaufruf im Stapel, und die Zeit im Kernel ist immer noch durch den Parameter ncalls begrenzt (der begrenzt werden kann auf ein beliebiger Wert). Sie haben Recht damit, dass ein verschachtelter Batch-Systemaufruf ein leistungsfähiges Tool ist, vielleicht sogar so, dass es ausgeschlossen werden sollte (obwohl ich es in einer Situation mit statischen Dateiservern als nützlich erachten könnte, wenn ein Dämon absichtlich mit Zeigern in eine Kernelschleife gesteckt wird) Implementierung des alten TUX-Servers)
Technosaurus
1
Syscalls sind mit einer Änderung der Berechtigungen verbunden, dies wird jedoch nicht immer als Kontextwechsel bezeichnet. en.wikipedia.org/wiki/…
Erik Eidt
1
Lesen Sie diese gestern, die mehr Motivation und Hintergrund bietet: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
@ JörgWMittag-Verschachtelung kann deaktiviert werden, um einen Überlauf des Kernelstapels zu verhindern. Andernfalls wird der einzelne Systemanruf wie gewohnt frei. Es sollte dabei keine ressourcenschonenden Probleme geben. Der Linux-Kernel ist nicht zulässig.
PSkocik

Antworten:

5

Ich habe es auf x86_64 versucht

Patch gegen 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (auch hier https://github.com/pskocik/linux/tree/supersyscall )

diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 5aef183e2f85..8df2e98eb403 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -339,6 +339,7 @@
 330    common  pkey_alloc      sys_pkey_alloc
 331    common  pkey_free       sys_pkey_free
 332    common  statx           sys_statx
+333    common  supersyscall            sys_supersyscall

 #
 # x32-specific system call numbers start at 512 to avoid cache impact
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 980c3c9b06f8..c61c14e3ff4e 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -905,5 +905,20 @@ asmlinkage long sys_pkey_alloc(unsigned long flags, unsigned long init_val);
 asmlinkage long sys_pkey_free(int pkey);
 asmlinkage long sys_statx(int dfd, const char __user *path, unsigned flags,
              unsigned mask, struct statx __user *buffer);
-
 #endif
+
+struct supersyscall_args {
+    unsigned call_nr;
+    long     args[6];
+};
+#define SUPERSYSCALL__abort_on_failure    0
+#define SUPERSYSCALL__continue_on_failure 1
+/*#define SUPERSYSCALL__lock_something    2?*/
+
+
+asmlinkage 
+long 
+sys_supersyscall(long* Rets, 
+                 struct supersyscall_args *Args, 
+                 int Nargs, 
+                 int Flags);
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a076cf1a3a23..56184b84530f 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -732,9 +732,11 @@ __SYSCALL(__NR_pkey_alloc,    sys_pkey_alloc)
 __SYSCALL(__NR_pkey_free,     sys_pkey_free)
 #define __NR_statx 291
 __SYSCALL(__NR_statx,     sys_statx)
+#define __NR_supersyscall 292
+__SYSCALL(__NR_supersyscall,     sys_supersyscall)

 #undef __NR_syscalls
-#define __NR_syscalls 292
+#define __NR_syscalls (__NR_supersyscall+1)

 /*
  * All syscalls below here should go away really,
diff --git a/init/Kconfig b/init/Kconfig
index a92f27da4a27..25f30bf0ebbb 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2184,4 +2184,9 @@ config ASN1
      inform it as to what tags are to be expected in a stream and what
      functions to call on what tags.

+config SUPERSYSCALL
+     bool
+     help
+        System call for batching other system calls
+
 source "kernel/Kconfig.locks"
diff --git a/kernel/Makefile b/kernel/Makefile
index b302b4731d16..4d86bcf90f90 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -9,7 +9,7 @@ obj-y     = fork.o exec_domain.o panic.o \
        extable.o params.o \
        kthread.o sys_ni.o nsproxy.o \
        notifier.o ksysfs.o cred.o reboot.o \
-       async.o range.o smpboot.o ucount.o
+       async.o range.o smpboot.o ucount.o supersyscall.o

 obj-$(CONFIG_MULTIUSER) += groups.o

diff --git a/kernel/supersyscall.c b/kernel/supersyscall.c
new file mode 100644
index 000000000000..d7fac5d3f970
--- /dev/null
+++ b/kernel/supersyscall.c
@@ -0,0 +1,83 @@
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/compiler.h>
+#include <linux/sched/signal.h>
+
+/*TODO: do this properly*/
+/*#include <uapi/asm-generic/unistd.h>*/
+#ifndef __NR_syscalls
+# define __NR_syscalls (__NR_supersyscall+1)
+#endif
+
+#define uif(Cond)  if(unlikely(Cond))
+#define lif(Cond)  if(likely(Cond))
+ 
+
+typedef asmlinkage long (*sys_call_ptr_t)(unsigned long, unsigned long,
+                     unsigned long, unsigned long,
+                     unsigned long, unsigned long);
+extern const sys_call_ptr_t sys_call_table[];
+
+static bool 
+syscall__failed(unsigned long Ret)
+{
+   return (Ret > -4096UL);
+}
+
+
+static bool
+syscall(unsigned Nr, long A[6])
+{
+    uif (Nr >= __NR_syscalls )
+        return -ENOSYS;
+    return sys_call_table[Nr](A[0], A[1], A[2], A[3], A[4], A[5]);
+}
+
+
+static int 
+segfault(void const *Addr)
+{
+    struct siginfo info[1];
+    info->si_signo = SIGSEGV;
+    info->si_errno = 0;
+    info->si_code = 0;
+    info->si_addr = (void*)Addr;
+    return send_sig_info(SIGSEGV, info, current);
+    //return force_sigsegv(SIGSEGV, current);
+}
+
+asmlinkage long /*Ntried*/
+sys_supersyscall(long* Rets, 
+                 struct supersyscall_args *Args, 
+                 int Nargs, 
+                 int Flags)
+{
+    int i = 0, nfinished = 0;
+    struct supersyscall_args args; /*7 * sizeof(long) */
+    
+    for (i = 0; i<Nargs; i++){
+        long ret;
+
+        uif (0!=copy_from_user(&args, Args+i, sizeof(args))){
+            segfault(&Args+i);
+            return nfinished;
+        }
+
+        ret = syscall(args.call_nr, args.args);
+        nfinished++;
+
+        if ((Flags & 1) == SUPERSYSCALL__abort_on_failure 
+                &&  syscall__failed(ret))
+            return nfinished;
+
+
+        uif (0!=put_user(ret, Rets+1)){
+            segfault(Rets+i);
+            return nfinished;
+        }
+    }
+    return nfinished;
+
+}
+
+
diff --git a/kernel/sys_ni.c b/kernel/sys_ni.c
index 8acef8576ce9..c544883d7a13 100644
--- a/kernel/sys_ni.c
+++ b/kernel/sys_ni.c
@@ -258,3 +258,5 @@ cond_syscall(sys_membarrier);
 cond_syscall(sys_pkey_mprotect);
 cond_syscall(sys_pkey_alloc);
 cond_syscall(sys_pkey_free);
+
+cond_syscall(sys_supersyscall);

Und es scheint zu funktionieren - ich kann mit nur einem Systemaufruf Hallo an fd 1 und Welt an fd 2 schreiben:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>


struct supersyscall_args {
    unsigned  call_nr;
    long args[6];
};
#define SUPERSYSCALL__abort_on_failure    0
#define SUPERSYSCALL__continue_on_failure 1

long 
supersyscall(long* Rets, 
                 struct supersyscall_args *Args, 
                 int Nargs, 
                 int Flags);

int main(int c, char**v)
{
    puts("HELLO WORLD:");
    long r=0;
    struct supersyscall_args args[] = { 
        {SYS_write, {1, (long)"hello\n", 6 }},
        {SYS_write, {2, (long)"world\n", 6 }},
    };
    long rets[sizeof args / sizeof args[0]];

    r = supersyscall(rets, 
                     args,
                     sizeof(rets)/sizeof(rets[0]), 
                     0);
    printf("r=%ld\n", r);
    printf( 0>r ? "%m\n" : "\n");

    puts("");
#if 1

#if SEGFAULT 
    r = supersyscall(0, 
                     args,
                     sizeof(rets)/sizeof(rets[0]), 
                     0);
    printf("r=%ld\n", r);
    printf( 0>r ? "%m\n" : "\n");
#endif
#endif
    return 0;
}

long 
supersyscall(long* Rets, 
                 struct supersyscall_args *Args, 
                 int Nargs, 
                 int Flags)
{
    return syscall(333, Rets, Args, Nargs, Flags);
}

Grundsätzlich benutze ich:

long a_syscall(long, long, long, long, long, long);

als universeller syscall-prototyp, wie es auf x86_64 zu funktionieren scheint, ist mein "super" -syscall also:

struct supersyscall_args {
    unsigned call_nr;
    long     args[6];
};
#define SUPERSYSCALL__abort_on_failure    0
#define SUPERSYSCALL__continue_on_failure 1
/*#define SUPERSYSCALL__lock_something    2?*/

asmlinkage 
long 
sys_supersyscall(long* Rets, 
                 struct supersyscall_args *Args, 
                 int Nargs, 
                 int Flags);

Es gibt die Anzahl der versuchten Syscalls zurück ( ==Nargswenn das SUPERSYSCALL__continue_on_failureFlag übergeben wird, andernfalls >0 && <=Nargs) und Fehler beim Kopieren zwischen Kernel-Space und User-Space werden durch Segfaults anstelle der üblichen angezeigt -EFAULT.

Was ich nicht weiß, ist, wie sich dies auf andere Architekturen übertragen lässt, aber es wäre sicher schön, so etwas im Kernel zu haben.

Wenn dies für alle Bögen möglich wäre, stelle ich mir einen Userspace-Wrapper vor, der über einige Gewerkschaften und Makros Typensicherheit bietet (er könnte ein Gewerkschaftsmitglied basierend auf dem Syscall-Namen auswählen und alle Gewerkschaften würden dann in die 6 Longs konvertiert oder was auch immer das Äquivalent der Architektur de Jour der 6 langen sein würde).

PSkocik
quelle
1
Es ist ein guter Proof of Concept, obwohl ich eine Reihe von Zeigern zu lang anstelle von nur einer Reihe von langen sehen möchte, damit Sie Dinge wie open-write-close mit der Rückkehr von openin writeund tun können close. Das würde die Komplexität aufgrund von get / put_user etwas erhöhen, aber wahrscheinlich lohnt es sich. In Bezug auf die Portabilität von IIRC können einige Architekturen die Syscall-Register für die Args 5 und 6 überlasten, wenn ein Syscall mit 5 oder 6 Argumenten gestapelt wird. Das Hinzufügen von 2 zusätzlichen Args für die zukünftige Verwendung würde dies beheben und könnte in Zukunft für asynchrone Aufrufparameter verwendet werden, wenn ein SUPERSYSCALL__async Flag ist gesetzt
Technosaurus
1
Ich wollte auch einen sys_memcpy hinzufügen. Der Benutzer könnte es dann zwischen sys_open und sys_write einfügen, um das zurückgegebene fd in das erste Argument von sys_write zu kopieren, ohne den Modus zurück in den Userspace wechseln zu müssen.
PSkocik
3

Zwei Hauptprobleme, die einem sofort in den Sinn kommen, sind:

  • Fehlerbehandlung: Jeder einzelne Systemaufruf kann mit einem Fehler enden, der von Ihrem User-Space-Code überprüft und behandelt werden muss. Ein Batching-Aufruf müsste daher ohnehin nach jedem einzelnen Aufruf User-Space-Code ausführen, sodass die Vorteile des Batching von Kernel-Space-Aufrufen zunichte gemacht würden. Außerdem müsste die API sehr komplex sein (wenn überhaupt möglich zu entwerfen). Wie würden Sie beispielsweise die Logik ausdrücken, wenn der dritte Aufruf fehlschlägt, etwas tun und den vierten Aufruf überspringen, aber mit dem fünften fortfahren?

  • Viele "kombinierte" Aufrufe, die tatsächlich implementiert werden, bieten zusätzliche Vorteile, abgesehen davon, dass sie nicht zwischen Benutzer- und Kernel-Space wechseln müssen. Beispielsweise vermeiden sie häufig, Speicher zu kopieren und Puffer insgesamt zu verwenden (z. B. Daten direkt von einer Stelle im Seitenpuffer zu einer anderen zu übertragen, anstatt sie durch einen Zwischenpuffer zu kopieren). Dies ist natürlich nur für bestimmte Kombinationen von Aufrufen (z. B. Lesen-Schreiben) sinnvoll, nicht für beliebige Kombinationen von Gruppenaufrufen.

Michał Kosmulski
quelle
2
Betreff: Fehlerbehandlung. Ich habe darüber nachgedacht und deshalb habe ich das flags-Argument (BATCH_RET_ON_FIRST_ERR) vorgeschlagen ... ein erfolgreicher Syscall sollte ncalls zurückgeben, wenn alle Aufrufe fehlerfrei abgeschlossen wurden, oder der letzte erfolgreiche, wenn einer fehlschlägt. Auf diese Weise können Sie nach Fehlern suchen und möglicherweise beim ersten erfolglosen Aufruf erneut versuchen, indem Sie nur 2 Zeiger erhöhen und ncalls um den Rückgabewert verringern, wenn eine Ressource gerade belegt ist oder der Aufruf unterbrochen wurde. ... die nicht-kontextbezogenen Schaltelemente sind dafür nicht geeignet, aber seit Linux 4.2 könnte splice () auch diesen helfen
technosaurus
2
Der Kernel könnte die Anrufliste automatisch optimieren, um verschiedene Vorgänge zusammenzuführen und redundante Arbeiten zu vermeiden. Der Kernel würde wahrscheinlich einen besseren Job als die meisten einzelnen Entwickler machen und mit einer einfacheren API viel Aufwand einsparen.
Aleksandr Dubinsky
@technosaurus Es wäre nicht kompatibel mit Technosaurus 'Vorstellung von Ausnahmen, die mitteilen, welche Operation fehlgeschlagen ist (weil die Reihenfolge der Operationen optimiert wird). Aus diesem Grund sind Ausnahmen normalerweise nicht für die Rückgabe derart präziser Informationen ausgelegt (auch, weil der Code verwirrend und zerbrechlich wird). Glücklicherweise ist es nicht schwierig, allgemeine Ausnahmebehandlungsroutinen zu schreiben, die verschiedene Fehlermodi behandeln.
Aleksandr Dubinsky