Dlaczego w Linuksie / BSD nie ma ogólnego systemu wsadowego?

17

Tło:

Narzut wywołania systemowego jest znacznie większy niż narzut wywołania funkcji (szacowany zakres od 20-100x), głównie z powodu przełączania kontekstu z przestrzeni użytkownika na przestrzeń jądra iz powrotem. Zazwyczaj funkcje wbudowane oszczędzają narzutu wywołania funkcji, a wywołania funkcji są znacznie tańsze niż wywołania systemowe. Jest oczywiste, że programiści chcieliby uniknąć narzutu wywołania systemowego, dbając o jak najwięcej operacji wewnątrz jądra w jednym wywołaniu systemowym.

Problem:

W ten sposób powstało wiele (zbędne?) Wywołań systemowych, takich jak sendmmsg () , recvmmsg () jak również CHDIR, otwarty, lseek i / lub kombinacji dowiązań symbolicznych, takich jak: 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, pwriteitd ...

Teraz Linux dodał, copy_file_range()który najwyraźniej łączy odczyt lseek i pisanie wywołań systemowych. To tylko kwestia czasu, zanim zmieni się w fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () i lcopy_file_rangeat () ... ale ponieważ są dwa pliki zaangażowane zamiast X więcej wywołań, może stać się X ^ 2 więcej. OK, Linus i różni programiści BSD nie pozwolili by posunąć się tak daleko, ale chodzi mi o to, że gdyby istniało wsadowe wywołanie systemowe, wszystkie (większość?) Można by zaimplementować w przestrzeni użytkownika i zmniejszyć złożoność jądra bez dodawania dużej ilości jeśli narzut po stronie libc.

Zaproponowano wiele złożonych rozwiązań, które obejmują niektóre specjalne wątki systemowe do nie wywoływania bloków systemowych do przetwarzania wsadowego; jednak metody te znacznie zwiększają złożoność przestrzeni jądra i użytkownika, podobnie jak libxcb vs. libX11 (wywołania asynchroniczne wymagają znacznie większej konfiguracji)

Rozwiązanie?:

Ogólny system wsadowy. Zmniejszyłoby to największy koszt (przełączniki wielu trybów) bez złożoności związanej z posiadaniem specjalistycznego wątku jądra (chociaż tę funkcjonalność można by dodać później).

Zasadniczo istnieje już dobra podstawa dla prototypu w funkcji systemowej socketcall (). Po prostu rozszerz go od pobierania tablicy argumentów, aby zamiast tego pobierał tablicę zwrotów, wskaźnik do tablic argumentów (w tym numer syscall), liczbę syscall i argument flagi ... coś w stylu:

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

Jedną z głównych różnic jest to, że argumenty prawdopodobnie wszystko trzeba być wskaźniki dla uproszczenia tak, że wyniki poprzednich syscalli mogłyby zostać wykorzystane przez kolejnych syscalli (na przykład deskryptor pliku z open()do zastosowania w read()/ write())

Niektóre możliwe zalety:

  • mniejsza przestrzeń użytkownika -> przestrzeń jądra -> przełączanie przestrzeni użytkownika
  • możliwy przełącznik kompilatora -fcombine-syscalls, aby spróbować wsadowo zautomatyzować
  • opcjonalna flaga dla operacji asynchronicznej (zwróć fd, aby obejrzeć natychmiast)
  • możliwość implementacji przyszłych połączonych funkcji syscall w przestrzeni użytkownika

Pytanie:

Czy można wdrożyć syscall wsadowy?

  • Czy brakuje mi oczywistych problemów?
  • Czy przeceniam korzyści?

Czy warto zawracać sobie głowę wdrażaniem systemu wsadowego (nie pracuję w Intelu, Google ani Redhat)?

  • Już wcześniej załatałem własne jądro, ale boję się radzenia sobie z LKML.
  • Historia pokazała, że ​​nawet jeśli coś jest bardzo przydatne dla „zwykłych” użytkowników (nie-korporacyjnych użytkowników końcowych bez dostępu do zapisu git), może nigdy nie zostać zaakceptowane powyżej (unionfs, aufs, cryptodev, tuxonice itp.)

Bibliografia:

technozaur
źródło
4
Jednym z dość oczywistych problemów, jakie widzę, jest to, że jądro rezygnuje z kontroli czasu i miejsca wymaganego do wywołania systemowego, a także złożoności operacji pojedynczego wywołania systemowego. Zasadniczo utworzyłeś syscall, który może przydzielić dowolną, nieograniczoną ilość pamięci jądra, działać przez dowolną, nieograniczoną ilość czasu i może być dowolnie złożony. Poprzez zagnieżdżanie batchsyscall w batchsyscall, możesz stworzyć dowolnie głębokie drzewo wywołań arbitralnych syscall. Zasadniczo możesz umieścić całą aplikację w jednym wywołaniu systemowym.
Jörg W Mittag
@ JörgWMittag - Nie sugeruję, aby działały równolegle, więc ilość użytej pamięci jądra byłaby nie większa niż najcięższe wywołanie systemowe w partii, a czas w jądrze jest nadal ograniczony przez parametr ncalls (który może być ograniczony do pewna dowolna wartość). Masz rację, że zagnieżdżony pakiet wsadowy jest potężnym narzędziem, być może do tego stopnia, że ​​należy go wykluczyć (chociaż widziałem, że jest przydatny w sytuacji statycznego serwera plików - celowo wbijając demona w pętlę jądra za pomocą wskaźników - w zasadzie implementacja starego serwera TUX)
technozaur
1
Połączenia systemowe obejmują zmianę uprawnień, ale nie zawsze jest to określane jako zmiana kontekstu. en.wikipedia.org/wiki/…
Erik Eidt
1
przeczytaj to wczoraj, co stanowi dodatkową motywację i tło: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
@ Zagnieżdżanie JörgWMittag może być niedozwolone, aby zapobiec przepełnieniu stosu jądra. W przeciwnym razie indywidualne wywołanie systemowe uwolni się po sobie, tak jak zwykle. Nie powinno być z tym żadnych problemów z gromadzeniem zasobów. Jądro Linux jest możliwe do uniknięcia.
PSkocik

Odpowiedzi:

5

Próbowałem tego na x86_64

Łatka przeciwko 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (także tutaj 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);

I wydaje się, że działa - mogę napisać cześć na fd 1 i world na fd 2 za pomocą tylko jednego połączenia systemowego:

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

Zasadniczo używam:

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

jako uniwersalny prototyp syscall, który wygląda jak działa na x86_64, więc mój „super” syscall to:

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

Zwraca liczbę próbnych wywołań systemowych ( ==Nargsjeśli SUPERSYSCALL__continue_on_failureflaga zostanie przekazana, w przeciwnym razie >0 && <=Nargs), a niepowodzenia kopiowania między przestrzenią jądra a przestrzenią użytkownika są sygnalizowane przez segfault zamiast zwykłych -EFAULT.

Nie wiem, w jaki sposób można by to przenieść na inne architektury, ale na pewno byłoby miło mieć coś takiego w jądrze.

Gdyby było to możliwe dla wszystkich łuków, wyobrażam sobie, że może istnieć opakowanie przestrzeni użytkownika, które zapewniłoby bezpieczeństwo typu przez niektóre związki i makra (mogłoby wybrać członka związku na podstawie nazwy syscall, a wszystkie związki przekształciłyby się w 6 długości) lub cokolwiek by to było równoważne 6 desek architektury de Jour).

PSkocik
źródło
1
Jego dobry dowód koncepcji, chociaż chciałbym zobaczyć tablicę wskaźników na długo, a nie tylko tablicą długo, tak, że można robić rzeczy, jak open-write-close używając zwrotu openw writei close. Zwiększy to nieco złożoność z powodu get / put_user, ale prawdopodobnie warto. Jeśli chodzi o przenośność IIRC, niektóre architektury mogą zablokować rejestry syscall dla argumentów 5 i 6, jeśli syscall 5 lub 6 arg jest wsadowy ... dodanie 2 dodatkowych argumentów do wykorzystania w przyszłości naprawiłoby to i mogłoby być użyte w przyszłości dla asynchronicznych parametrów wywołania, jeśli ustawiona jest flaga SUPERSYSCALL__async
technosaurus
1
Moim zamiarem było także dodanie sys_memcpy. Użytkownik może następnie umieścić go między sys_open i sys_write, aby skopiować zwrócone fd do pierwszego argumentu sys_write bez konieczności przełączania trybu z powrotem do przestrzeni użytkownika.
PSkocik
3

Dwie główne wpadki, które natychmiast przychodzą mi do głowy:

  • Obsługa błędów: każde indywidualne wywołanie systemowe może zakończyć się błędem, który należy sprawdzić i obsłużyć za pomocą kodu przestrzeni użytkownika. W związku z tym wywołanie wsadowe musiałoby uruchamiać kod przestrzeni użytkownika po każdym indywidualnym wywołaniu, więc korzyści płynące z pakietowania wywołań w przestrzeni jądra zostałyby zanegowane. Ponadto interfejs API musiałby być bardzo złożony (jeśli w ogóle możliwe zaprojektowanie) - na przykład, jak wyraziłbyś logikę, np. „Jeśli trzecie połączenie się nie powiedzie, zrób coś i pomiń czwarte połączenie, ale kontynuuj piąte”)?

  • Wiele „połączonych” wywołań, które faktycznie się implementują, oferuje dodatkowe korzyści oprócz konieczności przechodzenia między przestrzenią użytkownika a jądrem. Na przykład często unikają kopiowania pamięci i całkowitego używania buforów (np. Przenoszą dane bezpośrednio z jednego miejsca w buforze strony do innego zamiast kopiować je przez bufor pośredni). Oczywiście ma to sens tylko w przypadku określonych kombinacji wywołań (np. Odczyt i zapis), a nie w przypadku dowolnych kombinacji wywołań pakietowych.

Michał Kosmulski
źródło
2
Re: obsługa błędów. Pomyślałem o tym i dlatego zasugerowałem argument flagi (BATCH_RET_ON_FIRST_ERR) ... pomyślne wywołanie systemowe powinno zwrócić ncall, jeśli wszystkie wywołania zakończą się bez błędu, lub ostatnie, które zakończy się powodzeniem, jeśli się nie powiedzie. Pozwoliłoby to sprawdzić błędy i być może spróbować ponownie, zaczynając od pierwszego nieudanego połączenia, zwiększając 2 wskaźniki i zmniejszając liczbę wywołań o wartość zwracaną, jeśli zasób był zajęty lub połączenie zostało przerwane. ... części kontekstowe przełączające są poza tym, ale od Linuksa 4.2 splice () też może im pomóc
technozaur
2
Jądro może automatycznie zoptymalizować listę połączeń, aby połączyć różne operacje i wyeliminować zbędną pracę. Jądro prawdopodobnie wykonałoby lepszą robotę niż większość indywidualnych programistów przy ogromnych oszczędnościach dzięki prostszemu API.
Aleksandr Dubinsky
@technosaurus Nie byłoby to zgodne z ideą technozaura dotyczącą wyjątków, które komunikują, która operacja się nie powiodła (ponieważ kolejność operacji zostaje zoptymalizowana). Dlatego wyjątki zwykle nie są zaprojektowane do zwracania tak dokładnych informacji (również dlatego, że kod staje się mylący i kruchy). Na szczęście nie jest trudno napisać ogólne programy obsługi wyjątków, które obsługują różne tryby awarii.
Aleksandr Dubinsky