¿Por qué no hay syscall genérico por lotes en Linux / BSD?

17

Antecedentes:

La sobrecarga de llamadas del sistema es mucho mayor que la sobrecarga de llamadas de función (el rango de estimaciones es de 20-100x) principalmente debido al cambio de contexto del espacio del usuario al espacio del kernel y viceversa. Es común que las funciones en línea ahorren sobrecarga de llamadas a funciones y las llamadas a funciones son mucho más baratas que las llamadas al sistema. Es lógico que los desarrolladores deseen evitar parte de la sobrecarga de llamadas del sistema al ocuparse de la mayor cantidad posible de operaciones en el núcleo en una llamada al sistema.

Problema:

Esto ha creado una gran cantidad de (superfluos?) Llamadas al sistema como sendmmsg () , recvmmsg () , así como la chdir, abierto, lseek y / o combinaciones de enlaces simbólicos como: 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 ...

Ahora se ha agregado Linux, copy_file_range()que aparentemente combina lecturas de lseek y syscalls de escritura. Es solo una cuestión de tiempo antes de que esto se convierta en fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () y lcopy_file_rangeat () ... pero dado que hay 2 archivos involucrados en lugar de X llamadas más, podría convertirse en X ^ 2 más. De acuerdo, Linus y los diversos desarrolladores de BSD no lo dejarían ir tan lejos, pero mi punto es que si hubiera una syscall por lotes, todos (¿la mayoría?) Podrían implementarse en el espacio del usuario y reducir la complejidad del kernel sin agregar mucho si hay alguna sobrecarga en el lado de la biblioteca.

Se han propuesto muchas soluciones complejas que incluyen alguna hebra especial de syscall para llamadas syscall sin bloqueo para llamadas syscalls de proceso por lotes; sin embargo, estos métodos agregan una complejidad significativa tanto al kernel como al espacio del usuario de la misma manera que libxcb vs. libX11 (las llamadas asincrónicas requieren mucha más configuración)

¿Solución?:

Un syscall genérico por lotes. Esto aliviaría el mayor costo (conmutadores de modo múltiple) sin las complejidades asociadas con tener un hilo de kernel especializado (aunque esa funcionalidad podría agregarse más adelante).

Básicamente, ya existe una buena base para un prototipo en la llamada al sistema socketcall (). Simplemente extiéndalo de tomar una matriz de argumentos para tomar una matriz de retornos, puntero a matrices de argumentos (que incluye el número de syscall), el número de syscalls y un argumento de banderas ... algo como:

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

Una diferencia importante sería que los argumentos probablemente toda necesidad de ser punteros para simplicidad de manera que los resultados de las llamadas al sistema anteriores podrían ser utilizados por llamadas al sistema posteriores (por ejemplo, el descriptor de archivo de open()para su uso en read()/ write())

Algunas posibles ventajas:

  • menos espacio de usuario -> espacio de kernel -> cambio de espacio de usuario
  • posible compilador conmutador -fcombine-syscalls para tratar de procesar automáticamente
  • Indicador opcional para operación asincrónica (devuelva fd para mirar inmediatamente)
  • capacidad para implementar futuras funciones combinadas de syscall en el espacio de usuario

Pregunta:

¿Es factible implementar una syscall por lotes?

  • ¿Me estoy perdiendo algunas trampas obvias?
  • ¿Estoy sobreestimando los beneficios?

¿Me vale la pena molestarme en implementar un syscall por lotes (no trabajo en Intel, Google o Redhat)?

  • He parcheado mi propio kernel antes, pero temo tratar con el LKML.
  • La historia ha demostrado que incluso si algo es ampliamente útil para los usuarios "normales" (usuarios finales no corporativos sin acceso de escritura git), es posible que nunca se acepte en sentido ascendente (unionfs, aufs, cryptodev, tuxonice, etc.)

Referencias

technosaurus
fuente
44
Un problema bastante obvio que estoy viendo es que el kernel cede el control sobre el tiempo y el espacio necesarios para una llamada al sistema, así como la complejidad de las operaciones de una sola llamada al sistema. Básicamente, ha creado una llamada al sistema que puede asignar cantidades arbitrarias y sin límites de memoria del núcleo, ejecutarse durante un período de tiempo arbitrario y sin límites, y puede ser arbitrariamente complejo. Al anidar batchsyscalls en batchsyscalls, puede crear un árbol de llamadas arbitrariamente profundas de syscalls arbitrarias. Básicamente, puede poner toda su aplicación en una sola llamada al sistema.
Jörg W Mittag
@ JörgWMittag: no estoy sugiriendo que se ejecuten en paralelo, por lo que la cantidad de memoria del kernel utilizada no sería más que la llamada al sistema más pesada en el lote y el tiempo en el kernel todavía está limitado por el parámetro ncalls (que podría limitarse a algún valor arbitrario). Tiene razón acerca de que un syscall por lotes anidado es una herramienta poderosa, tal vez tanto que debería descartarse (aunque podría ver que es útil en una situación de servidor de archivos estático, al pegar intencionalmente un demonio en un bucle del núcleo usando punteros, básicamente implementando el antiguo servidor TUX)
technosaurus
1
Las llamadas al sistema implican un cambio de privilegios, pero esto no siempre se caracteriza como un cambio de contexto. en.wikipedia.org/wiki/…
Erik Eidt
1
lea esto ayer que proporciona más motivación y antecedentes: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
El anidamiento @ JörgWMittag podría no permitirse para evitar el desbordamiento de la pila del núcleo. De lo contrario, syscall individual se liberará después de sí mismo como lo hace normalmente. No debería haber ningún problema de acaparamiento de recursos con esto. El kernel de Linux es preferente.
PSkocik

Respuestas:

5

Probé esto en x86_64

Parche contra 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (también aquí 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);

Y parece funcionar: puedo escribir hola a fd 1 y world a fd 2 con solo una llamada al sistema:

#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);
}

Básicamente estoy usando:

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

como un prototipo universal de syscall, que parece ser cómo funcionan las cosas en x86_64, por lo que mi "súper" syscall es:

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);

Devuelve el número de llamadas al sistema intentadas ( ==Nargssi SUPERSYSCALL__continue_on_failurese pasa el indicador, de lo contrario >0 && <=Nargs) y las fallas para copiar entre el espacio del núcleo y el espacio del usuario se señalan mediante segfaults en lugar de lo habitual -EFAULT.

Lo que no sé es cómo esto se portaría a otras arquitecturas, pero seguramente sería bueno tener algo como esto en el núcleo.

Si esto fuera posible para todos los arcos, imagino que podría haber un contenedor de espacio de usuario que proporcionaría seguridad de tipo a través de algunos sindicatos y macros (podría seleccionar un miembro del sindicato basado en el nombre de syscall y todos los sindicatos se convertirían a los 6 largos o cualquiera que sea el equivalente de la arquitectura de jour de los 6 largos sería).

PSkocik
fuente
1
Es una buena prueba de concepto, aunque me gustaría ver una serie de punteros largos en lugar de solo una larga, para que puedas hacer cosas como abrir-escribir-cerrar usando el retorno de openin writey close. Eso aumentaría un poco la complejidad debido a get / put_user, pero probablemente valga la pena. En cuanto a la portabilidad IIRC, algunas arquitecturas pueden bloquear los registros de syscall para los args 5 y 6 si se combina un syscall de 5 o 6 arg ... agregar 2 args adicionales para uso futuro solucionaría eso y podría usarse en el futuro para parámetros de llamadas asíncronas si se establece una bandera SUPERSYSCALL__async
technosaurus
1
Mi intención era también agregar un sys_memcpy. El usuario podría ponerlo entre sys_open y sys_write para copiar el fd devuelto al primer argumento de sys_write sin tener que volver a cambiar el modo al espacio de usuario.
PSkocik
3

Dos problemas principales que vienen a la mente de inmediato son:

  • Manejo de errores: cada llamada al sistema individual puede terminar con un error que debe ser verificado y manejado por su código de espacio de usuario. Por lo tanto, una llamada de procesamiento por lotes tendría que ejecutar un código de espacio de usuario después de cada llamada individual de todos modos, por lo que los beneficios de las llamadas de espacio de kernel por lotes se negarían. Además, la API tendría que ser muy compleja (si es posible diseñarla), por ejemplo, ¿cómo expresaría una lógica como "si la tercera llamada falla, haga algo y salte la cuarta llamada pero continúe con la quinta"?

  • Muchas llamadas "combinadas" que realmente se implementan ofrecen beneficios adicionales además de no tener que moverse entre el espacio del usuario y del kernel. Por ejemplo, a menudo evitarán copiar memoria y usar memorias intermedias por completo (por ejemplo, transferir datos directamente de un lugar en el búfer de la página a otro en lugar de copiarlos a través de un búfer intermedio). Por supuesto, esto solo tiene sentido para combinaciones específicas de llamadas (por ejemplo, leer-luego-escribir), no para combinaciones arbitrarias de llamadas por lotes.

Michał Kosmulski
fuente
2
Re: manejo de errores. Pensé en eso y por eso sugerí el argumento de las banderas (BATCH_RET_ON_FIRST_ERR) ... una syscall exitosa debería devolver ncalls si todas las llamadas se completan sin error o la última exitosa si falla. Esto le permitiría verificar errores y posiblemente volver a intentarlo comenzando en la primera llamada fallida simplemente incrementando 2 punteros y disminuyendo ncalls por el valor de retorno si un recurso estaba ocupado o la llamada se interrumpió. ... las partes que no cambian de contexto están fuera de alcance para esto, pero desde Linux 4.2, splice () también podría ayudarlas
technosaurus
2
El núcleo podría optimizar automáticamente la lista de llamadas para fusionar varias operaciones y eliminar el trabajo redundante. El kernel probablemente haría un mejor trabajo que la mayoría de los desarrolladores individuales con un gran ahorro de esfuerzo con una API más simple.
Aleksandr Dubinsky
@technosaurus No sería compatible con la idea del technosaurus de excepciones que comunican qué operación falló (porque el orden de las operaciones se optimiza). Esta es la razón por la cual las excepciones normalmente no están diseñadas para devolver información tan precisa (también, porque el código se vuelve confuso y frágil). Afortunadamente, no es difícil escribir manejadores de excepciones genéricos que manejen varios modos de falla.
Aleksandr Dubinsky