Perché non ci sono batch di syscall generici in Linux / BSD?

16

Sfondo:

L'overhead delle chiamate di sistema è molto più ampio dell'overhead delle chiamate di funzione (stima varia da 20 a 100x), principalmente a causa del passaggio da uno spazio utente all'altro e dallo spazio del kernel. È comune alle funzioni in linea salvare l'overhead delle chiamate di funzione e le chiamate di funzione sono molto più economiche delle syscalls. È ovvio che gli sviluppatori vorrebbero evitare alcune delle spese generali di chiamata del sistema occupandosi del maggior numero di operazioni nel kernel in un syscall possibile.

Problema:

Questo ha creato molte chiamate di sistema (superflue?) come sendmmsg () 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 , pwrite ecc ...

Ora Linux ha aggiunto copy_file_range() che apparentemente combina lseek di lettura e scrive syscalls. È solo questione di tempo prima che questo diventi fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () e lcopy_file_rangeat () ... ma poiché ci sono 2 file coinvolti invece di X più chiamate, potrebbe diventare X ^ 2 Di Più. OK, Linus ed i vari sviluppatori di BSD non lascerebbero andare così lontano, ma il mio punto è che se ci fosse un syscall in batch, tutti (la maggior parte?) Di questi potrebbero essere implementati nello spazio utente e ridurre la complessità del kernel senza aggiungere molto se c'è un sovraccarico sul lato libc.

Sono state proposte molte soluzioni complesse che includono alcuni tipi di thread syscall speciali per syscalls non bloccanti a syscalls di processi batch; tuttavia, questi metodi aggiungono una notevole complessità sia al kernel che allo user space più o meno allo stesso modo di libxcb vs libX11 (le chiamate asincrone richiedono molte più impostazioni)

Soluzione:?

Un syscall di batch generico. Ciò allevierebbe il costo maggiore (switch in modalità multipla) senza le complessità associate alla presenza di thread del kernel specializzati (sebbene tale funzionalità possa essere aggiunta in seguito).

C'è fondamentalmente già una buona base per un prototipo nel syscall di socketcall (). Estendetelo semplicemente prendendo una serie di argomenti per prendere invece una serie di ritorni, puntatore a matrici di argomenti (che include il numero di syscall), il numero di syscall e un argomento flags ... qualcosa del tipo:

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

Una delle principali differenze potrebbe essere che gli argomenti probabilmente tutti devono essere puntatori per semplicità in modo che i risultati di syscalls precedenti possano essere utilizzati dalle successive syscalls (ad esempio il descrittore di file da open() da utilizzare in read() / write() )

Alcuni possibili vantaggi:

  • meno spazio utente - > spazio del kernel - > commutazione dello spazio utente
  • possibile switch del compilatore -fcombine-syscalls per provare a eseguire il batch in modo automatico
  • indicatore opzionale per operazioni asincrone (restituire fd per guardarlo immediatamente)
  • capacità di implementare le future funzioni combinate di syscall nello spazio utente

Domanda:

È possibile implementare un syscall di batch?

  • Mi mancano alcuni trucchi ovvi?
  • Sto sopravvalutando i vantaggi?

Vale la pena per me preoccuparmi di implementare un syscall di batch (non lavoro su Intel, Google o RedHat)?

  • Ho già applicato patch al mio kernel, ma ho avuto problemi con LKML.
  • La storia ha dimostrato che anche se qualcosa è ampiamente utile per gli utenti "normali" (utenti finali non aziendali senza accesso git write), potrebbe non essere mai accettato upstream (unionfs, aufs, cryptodev, tuxonice, ecc ...)

References:

posta technosaurus 26.02.2016 - 10:39
fonte

2 risposte

6

Ho provato questo su x86_64

Patch contro 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (anche qui link )

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

E sembra funzionare - Posso scrivere ciao a fd 1 e world a fd 2 con un solo syscall:

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

Fondamentalmente sto usando:

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

come prototipo universale di syscall, che sembra essere il modo in cui le cose funzionano su x86_64, quindi il mio syscall "super" è:

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

Restituisce il numero di tentativi di syscalls ( ==Nargs se viene passato il flag SUPERSYSCALL__continue_on_failure , altrimenti >0 && <=Nargs ) e gli errori di copia tra spazio kernel e spazio utente sono segnalati da segfaults invece del solito -EFAULT .

Quello che non so è come questo porterebbe ad altre architetture, ma sarebbe sicuramente bello avere qualcosa di simile nel kernel.

Se ciò fosse possibile per tutti gli arch, immagino ci possa essere un wrapper in userspace che fornisca sicurezza di tipo attraverso alcuni sindacati e macro (potrebbe selezionare un membro del sindacato basato sul nome syscall e tutti i sindacati sarebbero poi convertiti in i 6 lunghi o qualunque sia l'equivalente di architecture de jour dei 6 lunghi).

    
risposta data 05.06.2017 - 11:07
fonte
3

Due trucchi principali che vengono in mente immediatamente sono:

  • Gestione degli errori: ogni singolo syscall può terminare con un errore che deve essere controllato e gestito dal codice dello spazio utente. Una chiamata batch dovrebbe quindi eseguire il codice spazio utente dopo ogni singola chiamata, in modo tale che i vantaggi del batching delle chiamate nello spazio del kernel vengano annullati. Inoltre, l'API dovrebbe essere molto complessa (se possibile progettare del tutto), ad esempio come esprimeresti una logica come "se la terza chiamata fallisce, fai qualcosa e salta la quarta chiamata ma continua con la quinta")?

  • Molte chiamate "combinate" che effettivamente vengono implementate offrono ulteriori vantaggi oltre a non dover spostarsi tra lo spazio dell'utente e del kernel. Ad esempio, eviteranno spesso di copiare la memoria e di utilizzare i buffer del tutto (ad esempio, trasferire i dati direttamente da un punto nel buffer di pagine a un altro invece di copiarlo attraverso un buffer intermedio). Naturalmente, questo ha senso solo per combinazioni specifiche di chiamate (ad esempio read-then-write), non per combinazioni arbitrarie di chiamate in batch.

risposta data 26.02.2016 - 11:56
fonte

Leggi altre domande sui tag