Por que não existe syscall em lote genérico no Linux / BSD?

17

Fundo:

A sobrecarga de chamadas do sistema é muito maior que a sobrecarga de chamadas de função (as estimativas variam de 20 a 100x) principalmente devido à alternância de contexto do espaço do usuário para o espaço do kernel e vice-versa. É comum que as funções embutidas reduzam as despesas gerais das chamadas de função e as chamadas de função são muito mais baratas que os syscalls. É lógico que os desenvolvedores desejam evitar parte da sobrecarga de chamadas do sistema cuidando da maior operação possível no kernel em um syscall.

Problema:

Isso criou um grande número de chamadas (supérfluos?) Do sistema, como sendmmsg () , recvmmsg () , bem como o chdir, aberto, lseek e / ou combinações link simbólico 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 ...

Agora, o Linux adicionou o copy_file_range()que aparentemente combina lseek de leitura e syscalls de gravação. É apenas uma questão de tempo até que isso se torne fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () e lcopy_file_rangeat () ... mas como existem 2 arquivos envolvidos em vez de mais X chamadas, ele pode se tornar X ^ 2 Mais. OK, Linus e os vários desenvolvedores do BSD não deixaram isso ir tão longe, mas o que quero dizer é que, se houvesse um syscall em lote, todos (a maioria?) Deles poderiam ser implementados no espaço do usuário e reduzir a complexidade do kernel sem adicionar muito se houver alguma sobrecarga no lado da libc.

Muitas soluções complexas foram propostas que incluem algum thread syscall especial de formulário para syscalls sem bloqueio para syscalls de processos em lote; no entanto, esses métodos adicionam complexidade significativa ao espaço do kernel e do usuário da mesma maneira que libxcb vs. libX11 (as chamadas assíncronas exigem muito mais configuração)

Solução?:

Um syscall de lote genérico. Isso aliviaria o maior custo (comutadores de modo múltiplo) sem as complexidades associadas à existência de encadeamento especializado do kernel (embora essa funcionalidade possa ser adicionada posteriormente).

Basicamente, já existe uma boa base para um protótipo no syscall socketcall (). Apenas estenda a partir de uma matriz de argumentos para obter uma matriz de retornos, ponteiro para matrizes de argumentos (que inclui o número syscall), o número de syscalls e um argumento flags ... algo como:

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

Uma diferença importante seria que os argumentos provavelmente todos necessidade de ser ponteiros para simplicidade para que os resultados de syscalls anteriores poderia ser usado por syscalls subsequentes (por exemplo, o descritor de arquivo de open()para uso em read()/ write())

Algumas vantagens possíveis:

  • menos espaço do usuário -> espaço do kernel -> comutação do espaço do usuário
  • possível alternador do compilador -fcombine-syscalls para tentar agrupar automaticamente
  • sinalizador opcional para operação assíncrona (retorne fd para assistir imediatamente)
  • capacidade de implementar futuras funções syscall combinadas no espaço do usuário

Questão:

É possível implementar uma syscall em lote?

  • Estou perdendo algumas dicas óbvias?
  • Estou superestimando os benefícios?

Vale a pena me preocupar em implementar uma syscall em lotes (não trabalho na Intel, Google ou Redhat)?

  • Eu já corrigi meu próprio kernel antes, mas tenho medo de lidar com o LKML.
  • A história mostrou que, mesmo que algo seja amplamente útil para usuários "normais" (usuários finais não corporativos sem acesso de gravação git), ele nunca poderá ser aceito a montante (unionfs, aufs, cryptodev, tuxonice, etc ...)

Referências:

technosaurus
fonte
4
Um problema bastante óbvio que estou vendo é que o kernel desiste do controle sobre o tempo e o espaço necessários para um syscall, bem como a complexidade das operações de um único syscall. Você basicamente criou um syscall que pode alocar quantidades arbitrárias e ilimitadas de memória do kernel, executar por um período arbitrário e ilimitado e pode ser arbitrariamente complexo. Aninhando batchsyscalls em batchsyscalls, você pode criar uma árvore chamada arbitrariamente profunda de syscalls arbitrárias. Basicamente, você pode colocar todo o aplicativo em um único syscall.
Jörg W Mittag 26/02
@ JörgWMittag - Eu não estou sugerindo que eles sejam executados em paralelo, portanto, a quantidade de memória do kernel usada não seria mais que a syscall mais pesada do lote e o tempo no kernel ainda é limitado pelo parâmetro ncalls (que pode ser limitado a algum valor arbitrário). Você está certo sobre um syscall de lote aninhado ser uma ferramenta poderosa, talvez tanto que deva ser impedida (embora eu possa vê-lo útil em uma situação estática de servidor de arquivos - colocando intencionalmente um daemon em um loop do kernel usando ponteiros - basicamente implementando o antigo servidor TUX)
technosaurus 26/02
1
Syscalls envolvem uma mudança de privilégio, mas isso nem sempre é caracterizado como uma troca de contexto. en.wikipedia.org/wiki/…
Erik Eidt 26/02
1
leia isto ontem, que fornece um pouco mais de motivação e histórico: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
O aninhamento @ JörgWMittag pode não ser permitido para impedir o estouro da pilha do kernel. Caso contrário, o syscall individual se libertará como eles normalmente fazem. Não deve haver nenhum problema com recursos. O kernel do Linux é preemptivo.
PSKocik #

Respostas:

5

Eu tentei isso em x86_64

Correção contra 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (também aqui 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 parece funcionar - eu posso escrever olá para fd 1 e world para fd 2 com apenas um 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);
}

Basicamente, estou usando:

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

como um protótipo universal de syscall, que parece ser como as coisas funcionam no x86_64, então meu "super" syscall é:

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

Ele retorna o número de syscalls tentados ( ==Nargsse o SUPERSYSCALL__continue_on_failuresinalizador for passado, caso contrário >0 && <=Nargs) e as falhas na cópia entre o espaço dos kernels e o espaço do usuário são sinalizadas por segfaults em vez do usual-EFAULT .

O que eu não sei é como isso seria portado para outras arquiteturas, mas seria bom ter algo assim no kernel.

Se isso fosse possível para todos os arcos, imagino que poderia haver um wrapper de espaço de usuário que proporcionasse segurança de tipo através de algumas uniões e macros (ele poderia selecionar um membro da união com base no nome do syscall e todas as uniões seriam convertidas para os 6 longos) ou qualquer que seja o equivalente da arquitetura de jour aos 6 longos).

PSkocik
fonte
1
É uma boa prova de conceito, embora eu gostaria de ver uma série de indicadores por muito tempo, em vez de apenas uma série, para que você possa fazer coisas como abrir-gravar-fechar usando o retorno de openin writee close. Isso aumentaria um pouco a complexidade devido a get / put_user, mas provavelmente vale a pena. Quanto à portabilidade IIRC, algumas arquiteturas podem atrapalhar os registros syscall para os argumentos 5 e 6 se um syscall de 5 ou 6 argumentos for agrupado em lotes ... adicionar 2 argumentos adicionais para uso futuro resolveria isso e poderia ser usado no futuro para parâmetros de chamada assíncrona. bandeira um SUPERSYSCALL__async está definido
Technosaurus
1
Minha intenção era também adicionar um sys_memcpy. O usuário poderia colocá-lo entre sys_open e sys_write para copiar o fd retornado para o primeiro argumento de sys_write sem precisar alternar o modo novamente para o espaço do usuário.
PSKocik
3

Duas dicas principais que vêm à mente imediatamente são:

  • Tratamento de erros: cada syscall individual pode terminar com um erro que precisa ser verificado e tratado pelo seu código de espaço do usuário. Uma chamada em lote, portanto, teria que executar o código de espaço do usuário após cada chamada individual de qualquer maneira, para que os benefícios das chamadas em lote do espaço do kernel fossem negados. Além disso, a API teria que ser muito complexa (se possível projetar) - por exemplo, como você expressaria lógica como "se a terceira chamada falhar, faça alguma coisa e pule a quarta chamada, mas continue com a quinta")?

  • Muitas chamadas "combinadas" que realmente são implementadas oferecem benefícios adicionais, além de não ter que se deslocar entre o usuário e o espaço do kernel. Por exemplo, eles geralmente evitam copiar a memória e usar os buffers completamente (por exemplo, transferir dados diretamente de um local no buffer da página para outro em vez de copiá-los através de um buffer intermediário). Obviamente, isso só faz sentido para combinações específicas de chamadas (por exemplo, leitura e gravação), não para combinações arbitrárias de chamadas em lote.

Michał Kosmulski
fonte
2
Re: tratamento de erros. Eu pensei sobre isso e foi por isso que sugeri o argumento flags (BATCH_RET_ON_FIRST_ERR) ... um syscall bem-sucedido deve retornar ncalls se todas as chamadas forem concluídas sem erro ou a última bem-sucedida se uma falhar. Isso permitiria verificar erros e, possivelmente, tentar novamente, iniciando na primeira chamada malsucedida, apenas aumentando 2 ponteiros e diminuindo ncalls pelo valor de retorno se um recurso estivesse ocupado ou a chamada fosse interrompida. ... a não contexto switiching partes estão fora do escopo deste, mas desde Linux 4.2, splice () poderia ajudar os demais
Technosaurus
2
O kernel pode otimizar automaticamente a lista de chamadas para mesclar várias operações e eliminar trabalhos redundantes. O kernel provavelmente faria um trabalho melhor do que a maioria dos desenvolvedores individuais com uma grande economia de esforço com uma API mais simples.
Aleksandr Dubinsky 03/03
@technosaurus Não seria compatível com a idéia de exceções do technosaurus que comunica qual operação falhou (porque a ordem das operações é otimizada). É por isso que as exceções normalmente não são projetadas para retornar informações tão precisas (também porque o código se torna confuso e frágil). Felizmente, não é difícil escrever manipuladores de exceção genéricos que lidam com vários modos de falha.
Aleksandr Dubinsky