Pourquoi n'y a-t-il pas de système générique de traitement par lots sous Linux / BSD?

17

Contexte:

La surcharge des appels système est beaucoup plus importante que la surcharge des appels de fonction (les estimations vont de 20 à 100x), principalement en raison du changement de contexte de l'espace utilisateur vers l'espace noyau et inversement. Il est courant d'utiliser des fonctions en ligne pour économiser la surcharge des appels de fonction et les appels de fonction sont beaucoup moins chers que les appels système. Il va de soi que les développeurs voudraient éviter une partie de la surcharge des appels système en prenant en charge autant d'opérations dans le noyau en un seul appel système que possible.

Problème:

Cela a créé beaucoup d'appels système (superflu?) Comme sendmmsg () , recvmmsg () , ainsi que le chdir, ouvert, lseek et / ou des combinaisons symlink comme: 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 ...

Maintenant, Linux a ajouté copy_file_range()qui combine apparemment lseek et write syscalls. Ce n'est qu'une question de temps avant que cela ne devienne fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () et lcopy_file_rangeat () ... mais comme il y a 2 fichiers impliqués au lieu de X appels supplémentaires, cela pourrait devenir X ^ 2 plus. OK, Linus et les différents développeurs BSD ne laisseraient pas aller aussi loin, mais mon point est que s'il y avait un appel système par lots, tous (la plupart?) Pourraient être implémentés dans l'espace utilisateur et réduire la complexité du noyau sans ajouter beaucoup s'il y a des frais généraux sur le côté libc.

De nombreuses solutions complexes ont été proposées qui incluent une certaine forme de thread syscall spécial pour les appels sys non bloquants pour les appels sys de traitement par lots; cependant, ces méthodes ajoutent une complexité significative au noyau et à l'espace utilisateur de la même manière que libxcb vs libX11 (les appels asynchrones nécessitent beaucoup plus de configuration)

Solution?:

Un appel système générique par lots. Cela réduirait le coût le plus élevé (commutateurs multi-modes) sans les complexités associées à la présence d'un thread noyau spécialisé (bien que cette fonctionnalité puisse être ajoutée plus tard).

Il existe fondamentalement déjà une bonne base pour un prototype dans le syscall socketcall (). Il suffit de l'étendre de prendre un tableau d'arguments pour prendre à la place un tableau de retours, un pointeur sur des tableaux d'arguments (qui inclut le numéro de syscall), le nombre de syscalls et un argument flags ... quelque chose comme:

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

Une différence majeure serait que les arguments devraient probablement tous être des pointeurs de simplicité afin que les résultats des appels système précédents puissent être utilisés par les appels système suivants (par exemple, le descripteur de fichier open()à utiliser dans read()/ write())

Quelques avantages possibles:

  • moins d'espace utilisateur -> espace noyau -> changement d'espace utilisateur
  • commutateur de compilateur possible -fcombine-syscalls pour essayer de créer un lot de manière automatique
  • drapeau optionnel pour un fonctionnement asynchrone (retournez fd pour regarder immédiatement)
  • pouvoir implémenter les futures fonctions de syscall combinées dans l'espace utilisateur

Question:

Est-il possible de mettre en œuvre un appel système par lots?

  • Suis-je en train de rater des problèmes évidents?
  • Suis-je surestimer les avantages?

Vaut-il la peine de mettre en œuvre un appel système par lots (je ne travaille pas chez Intel, Google ou Redhat)?

  • J'ai déjà patché mon propre noyau, mais je crains d'avoir affaire au LKML.
  • L'histoire a montré que même si quelque chose est largement utile aux utilisateurs "normaux" (utilisateurs finaux non-entreprise sans accès en écriture git), il peut ne jamais être accepté en amont (unionfs, aufs, cryptodev, tuxonice, etc ...)

Les références:

technosaurus
la source
4
Un problème assez évident que je vois est que le noyau abandonne le contrôle du temps et de l'espace requis pour un appel système ainsi que la complexité des opérations d'un seul appel système. Vous avez essentiellement créé un appel système qui peut allouer des quantités arbitraires et illimitées de mémoire du noyau, s'exécuter pendant une durée arbitraire et illimitée et peut être arbitrairement complexe. En imbriquant des batchappels système dans des batchappels système, vous pouvez créer une arborescence d'appels arbitrairement approfondie de appels système arbitraires. Fondamentalement, vous pouvez mettre votre application entière dans un seul appel système.
Jörg W Mittag
@ JörgWMittag - Je ne suggère pas que ceux-ci fonctionnent en parallèle, donc la quantité de mémoire du noyau utilisée ne serait pas plus que le plus lourd syscall du lot et le temps dans le noyau est toujours limité par le paramètre ncalls (qui pourrait être limité à une valeur arbitraire). Vous avez raison sur le fait qu'un appel système par lots imbriqué est un outil puissant, peut-être à tel point qu'il devrait être exclu (bien que je puisse le voir utile dans une situation de serveur de fichiers statique - en collant intentionnellement un démon dans une boucle du noyau à l'aide de pointeurs - essentiellement implémentation de l'ancien serveur TUX)
technosaurus
1
Les appels système impliquent un changement de privilège mais cela n'est pas toujours caractérisé comme un changement de contexte. en.wikipedia.org/wiki/…
Erik Eidt
1
lire ceci hier qui fournit un peu plus de motivation et de contexte: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
@ L'imbrication de JörgWMittag peut être interdite pour éviter un débordement de la pile du noyau. Sinon, un syscall individuel se libérera comme il le fait normalement. Il ne devrait pas y avoir de problème de consommation de ressources avec cela. Le noyau Linux est préemptible.
PSkocik

Réponses:

5

J'ai essayé ceci sur x86_64

Patch contre 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (également ici 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);

Et cela semble fonctionner - je peux écrire bonjour à fd 1 et world à fd 2 avec un seul 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);
}

Fondamentalement, j'utilise:

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

comme un prototype de syscall universel, qui semble être la façon dont les choses fonctionnent sur x86_64, donc mon "super" syscall est:

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

Il retourne le nombre d'appels sys essayés ( ==Nargssi le SUPERSYSCALL__continue_on_failuredrapeau est passé, sinon >0 && <=Nargs) et les échecs de copie entre l'espace noyaux et l'espace utilisateur sont signalés par segfaults au lieu de l'habituel -EFAULT.

Ce que je ne sais pas, c'est comment cela pourrait porter sur d'autres architectures, mais ce serait bien d'avoir quelque chose comme ça dans le noyau.

Si cela était possible pour toutes les arches, j'imagine qu'il pourrait y avoir un wrapper d'espace utilisateur qui fournirait une sécurité de type via certaines unions et macros (il pourrait sélectionner un membre de l'union en fonction du nom de l'appel système et toutes les unions seraient ensuite converties en 6 longs ou quel que soit l'équivalent en architecture de jour des 6 longs).

PSkocik
la source
1
C'est une bonne preuve de concept, bien que j'aimerais voir un tableau de pointeurs sur long au lieu d'un simple tableau de long, afin que vous puissiez faire des choses comme open-write-close en utilisant le retour de openin writeet close. Cela augmenterait un peu la complexité en raison de get / put_user, mais cela en vaut probablement la peine. En ce qui concerne la portabilité IIRC, certaines architectures peuvent encombrer les registres d'appel système pour les arguments 5 et 6 si un appel système 5 ou 6 arguments est groupé ... l'ajout de 2 arguments supplémentaires pour une utilisation future résoudrait cela et pourrait être utilisé à l'avenir pour les paramètres d'appel asynchrones si un drapeau SUPERSYSCALL__async est défini
technosaurus
1
Mon intention était également d'ajouter un sys_memcpy. L'utilisateur pourrait alors le mettre entre sys_open et sys_write pour copier le fd retourné dans le premier argument de sys_write sans avoir à revenir en mode utilisateur.
PSkocik
3

Deux principaux problèmes qui viennent immédiatement à l'esprit sont:

  • Gestion des erreurs: chaque appel système individuel peut se terminer par une erreur qui doit être vérifiée et gérée par votre code d'espace utilisateur. Un appel par lots devrait donc de toute façon exécuter du code d'espace utilisateur après chaque appel individuel, de sorte que les avantages des appels par lots d'espace noyau seraient annulés. De plus, l'API devrait être très complexe (si possible à concevoir) - par exemple, comment exprimer une logique telle que "si le troisième appel a échoué, faire quelque chose et sauter le quatrième appel mais continuer avec le cinquième")?

  • De nombreux appels "combinés" qui sont effectivement mis en œuvre offrent des avantages supplémentaires en plus de ne pas avoir à se déplacer entre l'espace utilisateur et le noyau. Par exemple, ils éviteront souvent de copier la mémoire et d'utiliser des tampons (par exemple, transférer des données directement d'un endroit dans le tampon de page à un autre au lieu de les copier via un tampon intermédiaire). Bien sûr, cela n'a de sens que pour des combinaisons spécifiques d'appels (par exemple, lecture-écriture), et non pour des combinaisons arbitraires d'appels groupés.

Michał Kosmulski
la source
2
Re: gestion des erreurs. J'y ai pensé et c'est pourquoi j'ai suggéré l'argument flags (BATCH_RET_ON_FIRST_ERR) ... un syscall réussi devrait retourner ncalls si tous les appels se terminent sans erreur ou le dernier succès si un échoue. Cela vous permettrait de vérifier les erreurs et de réessayer éventuellement en commençant au premier appel infructueux simplement en incrémentant 2 pointeurs et en décrémentant ncalls par la valeur de retour si une ressource était juste occupée ou l'appel était interrompu. ... les parties de commutation non contextuelles sont hors de portée pour cela, mais depuis Linux 4.2, splice () pourrait aussi les aider
technosaurus
2
Le noyau pourrait automatiquement optimiser la liste d'appels pour fusionner diverses opérations et éliminer le travail redondant. Le noyau ferait probablement un meilleur travail que la plupart des développeurs individuels avec une grande économie d'effort avec une API plus simple.
Aleksandr Dubinsky
@technosaurus Il ne serait pas compatible avec l'idée d'exceptions du technosaurus qui communique quelle opération a échoué (car l'ordre des opérations est optimisé). C'est pourquoi les exceptions ne sont normalement pas conçues pour renvoyer des informations aussi précises (également, car le code devient confus et fragile). Heureusement, il n'est pas difficile d'écrire des gestionnaires d'exceptions génériques qui gèrent divers modes de défaillance.
Aleksandr Dubinsky