Perché non esiste un syscall di batch generico in Linux / BSD?


17

Sfondo:

L'overhead delle chiamate di sistema è molto più ampio dell'overhead delle chiamate di funzione (le stime vanno da 20 a 100x) principalmente a causa del passaggio dal contesto dello spazio utente allo spazio del kernel e viceversa. È comune incorporare le funzioni per risparmiare sull'overhead delle chiamate di funzione e le chiamate di funzione sono molto più economiche delle syscall. È ovvio che gli sviluppatori vorrebbero evitare un po 'del sovraccarico delle chiamate di sistema occupandosi di quante più operazioni possibili nel kernel in un syscall possibile.

Problema:

Questo ha creato un sacco di chiamate (superflui?) Di sistema come sendmmsg () , recvmmsg () così come il chdir, aperto, lseek e / o combinazioni di link simbolici come: 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, pwriteecc ...

Ora Linux ha aggiunto copy_file_range() che apparentemente combina leggi cerca e scrivi syscalls. È solo questione di tempo prima che 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 e i vari sviluppatori di BSD non lo lascerebbero andare così lontano, ma il mio punto è che se ci fosse un syscall di 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 dal lato libc.

Sono state proposte molte soluzioni complesse che includono alcuni thread di syscall speciali per syscall non bloccanti per syscall di processo batch; tuttavia questi metodi aggiungono una notevole complessità sia al kernel che allo spazio utente allo stesso modo di libxcb vs. libX11 (le chiamate asincrone richiedono molta più configurazione)

Soluzione?:

Un syscall di batch generico. Ciò allevierebbe il costo più grande (switch di modalità multipla) senza le complessità associate all'avere thread del kernel specializzati (sebbene tale funzionalità possa essere aggiunta in seguito).

Fondamentalmente esiste già una buona base per un prototipo nel syscall socketcall (). Basta estenderlo dal prendere una matrice di argomenti per prendere invece una matrice di ritorni, puntatore a matrici di argomenti (che include il numero di syscall), il numero di syscalls e un argomento flags ... qualcosa come:

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

Una differenza sostanziale sarebbe che gli argomenti dovrebbero probabilmente essere tutti puntatori per semplicità in modo che i risultati delle precedenti chiamate possano essere usati dalle successive chiamate (ad esempio il descrittore di file da open()utilizzare in read()/ write())

Alcuni possibili vantaggi:

  • meno spazio utente -> spazio kernel -> commutazione spazio utente
  • possibile switch del compilatore -fcombine-syscalls per provare a eseguire il batch automaticamente
  • flag opzionale per il funzionamento asincrono (restituire fd per guardare immediatamente)
  • capacità di implementare future funzioni di syscall combinate nello spazio utente

Domanda:

È possibile implementare un syscall di batch?

  • Mi sto perdendo degli ovvi gotchas?
  • Sto sopravvalutando i benefici?

Vale la pena preoccuparmi di implementare un syscall di batch (non lavoro in Intel, Google o Redhat)?

  • In precedenza ho patchato il mio kernel, ma temo di avere a che fare con LKML.
  • La storia ha dimostrato che anche se qualcosa è ampiamente utile per gli utenti "normali" (utenti finali non aziendali senza accesso in scrittura git), potrebbe non essere mai accettato a monte (unionfs, aufs, cryptodev, tuxonice, ecc ...)

Riferimenti:


4
Un problema abbastanza ovvio che sto vedendo è che il kernel rinuncia al controllo sul tempo e sullo spazio richiesti per una scala di sistema, nonché sulla complessità delle operazioni di una singola scala di sistema. Fondamentalmente hai creato un syscall che può allocare quantità arbitrarie e illimitate di memoria del kernel, funzionare per un periodo di tempo arbitrario e illimitato e può essere arbitrariamente complesso. Nidificando le batchsyscalls in batchsyscalls, è possibile creare un albero di chiamate arbitrariamente profondo di syscalls arbitrarie. Fondamentalmente, puoi mettere l'intera applicazione in un unico syscall.
Jörg W Mittag,

@ JörgWMittag - Non sto suggerendo che funzionino in parallelo, quindi la quantità di memoria del kernel utilizzata non sarebbe altro che la più pesante syscall nel batch e il tempo nel kernel è ancora limitato dal parametro ncalls (che potrebbe essere limitato a un valore arbitrario). Hai ragione sul fatto che un syscall batch nidificato sia uno strumento potente, forse così tanto che dovrebbe essere precluso (anche se potrei vederlo utile in una situazione di file server statico - attaccando intenzionalmente un demone in un ciclo del kernel usando i puntatori - in sostanza implementazione del vecchio server TUX)
technosaurus,

1
Syscalls comporta un cambiamento di privilegio, ma questo non è sempre caratterizzato come un cambio di contesto. en.wikipedia.org/wiki/…
Erik Eidt,

1
leggi questo ieri che fornisce ulteriori motivazioni e informazioni: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom

L'annidamento di @ JörgWMittag potrebbe non essere consentito per impedire l'overflow dello stack del kernel. Altrimenti, i singoli syscall si libereranno dopo se stessi come fanno normalmente. Non ci dovrebbero essere problemi di gestione delle risorse con questo. Il kernel di Linux è preimpostabile.
PSkocik,

Risposte:


5

Ho provato questo su x86_64

Patch contro 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (anche qui 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);

E sembra funzionare: posso scrivere ciao a fd 1 e mondo 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 di syscall universale, 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 syscall provati (==Nargs se la SUPERSYSCALL__continue_on_failurebandiera viene passata, altrimenti >0 && <=Nargs) e gli errori nella copia tra spazio kernel e spazio utente vengono 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 archi, immagino che potrebbe esserci un wrapper dello spazio utenti che fornirebbe la sicurezza del tipo attraverso alcuni sindacati e macro (potrebbe selezionare un membro del sindacato in base al nome di syscall e tutti i sindacati verrebbero quindi convertiti in 6 long o qualunque sia l'equivalente di 6 lunghe lunghe di architecture de jour).


1
È una buona prova del concetto, anche se mi piacerebbe vedere una serie di puntatori a long invece che solo una matrice di long, in modo da poter fare cose come open-write-close usando il ritorno di openin writee close. Ciò aumenterebbe un po 'la complessità a causa di get / put_user, ma probabilmente ne vale la pena. Per quanto riguarda la portabilità IIRC, alcune architetture potrebbero ostruire i registri di syscall per gli arg 5 e 6 se un syscall di 5 o 6 arg viene messo in batch ... l'aggiunta di 2 arg aggiuntivi per un uso futuro risolverebbe questo problema e potrebbe essere utilizzata in futuro per parametri di chiamata asincroni se viene impostata una bandiera SUPERSYSCALL__async
technosaurus

1
La mia intenzione era di aggiungere anche un sys_memcpy. L'utente potrebbe quindi inserirlo tra sys_open e sys_write per copiare il file fd restituito nel primo argomento di sys_write senza dover tornare alla modalità spazio utente.
PSkocik,

3

Due gotcha principali che vengono subito in mente sono:

  • Gestione degli errori: ogni singolo syscall può terminare con un errore che deve essere verificato e gestito dal codice dello spazio utente. Una chiamata in batch dovrebbe quindi eseguire il codice dello spazio utente dopo ogni singola chiamata in modo da vanificare i vantaggi del batch delle chiamate nello spazio del kernel. Inoltre, l'API dovrebbe essere molto complessa (se possibile progettare del tutto) - per 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 in realtà vengono implementate offrono ulteriori vantaggi oltre a non dover spostarsi tra spazio utente e kernel. Ad esempio, eviteranno spesso di copiare la memoria e di utilizzare del tutto i buffer (ad esempio, trasferire i dati direttamente da una posizione nel buffer della pagina a un'altra invece di copiarla attraverso un buffer intermedio). Ovviamente, ciò ha senso solo per combinazioni specifiche di chiamate (ad es. Lettura-poi-scrittura), non per combinazioni arbitrarie di chiamate in batch.


2
Ri: gestione degli errori. Ci ho pensato ed è per questo che ho suggerito l'argomento flags (BATCH_RET_ON_FIRST_ERR) ... una syscall di successo dovrebbe restituire ncalls se tutte le chiamate vengono completate senza errori o l'ultima di successo se si fallisce. Ciò consentirebbe di verificare la presenza di errori e, eventualmente, riprovare a partire dalla prima chiamata non riuscita semplicemente incrementando 2 puntatori e diminuendo le chiamate in base al valore restituito se una risorsa era appena occupata o la chiamata è stata interrotta. ... le parti di switiching non contestuali sono fuori portata per questo, ma dal momento che Linux 4.2, splice () potrebbe aiutare anche quelli
technosaurus,

2
Il kernel potrebbe ottimizzare automaticamente l'elenco delle chiamate per unire varie operazioni ed eliminare il lavoro ridondante. Il kernel probabilmente farebbe un lavoro migliore rispetto alla maggior parte dei singoli sviluppatori con un notevole risparmio in termini di sforzo con un'API più semplice.
Aleksandr Dubinsky

@technosaurus Non sarebbe compatibile con l'idea delle eccezioni di technosaurus che comunicano quale operazione è fallita (perché l'ordine delle operazioni viene ottimizzato). Questo è il motivo per cui le eccezioni non sono normalmente progettate per restituire informazioni così precise (anche perché il codice diventa confuso e fragile). Fortunatamente, non è difficile scrivere gestori di eccezioni generici che gestiscono varie modalità di errore.
Aleksandr Dubinsky
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.