Почему в Linux / BSD нет общего пакетного вызова syscall?

17

Фон:

Затраты на системные вызовы намного больше, чем на вызовы функций (оценки варьируются от 20 до 100x), в основном из-за переключения контекста из пространства пользователя в пространство ядра и обратно. Обычно встроенные функции сохраняют накладные расходы на вызовы функций, а вызовы функций намного дешевле, чем системные вызовы. Разумеется, разработчики хотели бы избежать некоторых накладных расходов на системные вызовы, заботясь о как можно большем количестве операций в ядре за один системный вызов.

Проблема:

Это создало много (лишние?) Системные вызовы , как sendmmsg () , recvmmsg () , а также Chdir, открытый, lseek и / или символические ссылки сочетаний , как: 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т.д. ...

Теперь Linux добавил, copy_file_range()что, по-видимому, объединяет системные вызовы read lseek и write. Это только вопрос времени, прежде чем это станет fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () и lcopy_file_rangeat () ... но, так как вместо X больше вызовов задействовано 2 файла, он может стать X ^ 2 Больше. Ладно, Линус и различные разработчики BSD не позволили бы ему зайти так далеко, но я хочу сказать, что, если бы был системный вызов в пакетном режиме, все (большинство?) Могли бы быть реализованы в пользовательском пространстве и уменьшить сложность ядра, не добавляя много если какие-либо накладные расходы на стороне libc.

Было предложено много сложных решений, которые включают в себя специальный поток системных вызовов для неблокирующих системных вызовов для пакетных системных вызовов; однако эти методы значительно усложняют как ядро, так и пользовательское пространство во многом так же, как libxcb и libX11 (асинхронные вызовы требуют гораздо большей настройки)

Решение?:

Общий пакетный системный вызов. Это позволило бы снизить наибольшую стоимость (переключение нескольких режимов) без сложностей, связанных с наличием специализированного потока ядра (хотя эта функциональность могла бы быть добавлена ​​позже).

По сути, в syscall socketcall () уже есть хорошая основа для прототипа. Просто расширите его от получения массива аргументов до взятия массива возвратов, указателя на массивы аргументов (который включает в себя номер системного вызова), количества системных вызовов и аргумента flags ... что-то вроде:

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

Одним из основных отличий будет то , что аргументы, вероятно , все нужно быть указатели для простоты так , что результаты предыдущих системных вызовов могут быть использованы в последующих системных вызовов (например , дескриптор файла из open()для использования в read()/ write())

Некоторые возможные преимущества:

  • меньше пользовательского пространства -> пространство ядра -> переключение пользовательского пространства
  • возможный ключ компилятора -fcombine-syscalls для автоматической пакетной обработки
  • необязательный флаг для асинхронной операции (возврат fd для просмотра немедленно)
  • возможность реализации будущих комбинированных функций системного вызова в пользовательском пространстве

Вопрос:

Возможно ли реализовать пакетный системный вызов?

  • Я пропускаю некоторые очевидные ошибки?
  • Я переоцениваю преимущества?

Стоит ли мне беспокоиться о реализации пакетного системного вызова (я не работаю в Intel, Google или Redhat)?

  • Я уже исправил свое собственное ядро, но боюсь иметь дело с LKML.
  • История показала, что даже если что-то широко полезно для «обычных» пользователей (не корпоративных конечных пользователей, не имеющих доступа к git-записи), оно никогда не может быть принято в апстриме (unionfs, aufs, cryptodev, tuxonice и т. Д.)

Ссылки:

technosaurus
источник
4
Одна довольно очевидная проблема, которую я вижу, состоит в том, что ядро ​​отказывается от управления временем и пространством, необходимым для системного вызова, а также сложностью операций одного системного вызова. Вы в основном создали системный вызов, который может выделять произвольные неограниченные объемы памяти ядра, работать в течение произвольного неограниченного промежутка времени и может быть сколь угодно сложным. К гнездованию batchсистемных вызовов в batchсистемные вызовы, вы можете создать сколь угодно глубокое дерево вызовов произвольных системных вызовов. По сути, вы можете поместить все приложение в один системный вызов.
Йорг Миттаг,
@ JörgWMittag - я не предполагаю, что они работают параллельно, поэтому объем используемой памяти ядра будет не больше, чем самый тяжелый системный вызов в пакете, а время в ядре все еще ограничено параметром ncalls (который может быть ограничен какое-то произвольное значение). Вы правы в том, что вложенный пакетный системный вызов является мощным инструментом, возможно, настолько значительным, что его следует исключить (хотя я мог видеть, что он полезен в ситуации статического файлового сервера - преднамеренно вставляя демона в цикл ядра с помощью указателей - в основном реализация старого сервера TUX)
технозавр
1
Системные вызовы включают изменение привилегий, но это не всегда характеризуется как переключение контекста. en.wikipedia.org/wiki/…
Эрик Эйдт
1
прочитайте это вчера, что дает дополнительную мотивацию и справочную информацию: matildah.github.io/posts/2016-01-30-unikernel-security.html
Том
Вложение @ JörgWMittag может быть запрещено для предотвращения переполнения стека ядра. В противном случае индивидуальный системный вызов освободится после себя, как обычно. С этим не должно быть проблем с ресурсом. Ядро Linux является приоритетным.
PSkocik

Ответы:

5

Я пробовал это на x86_64

Патч для 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (также здесь 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);

И это похоже на работу - я могу написать привет на fd 1 и world на fd 2 с помощью только одного системного вызова:

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

В основном я использую:

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

как универсальный прототип системного вызова, который, как кажется, работает на x86_64, поэтому мой «супер» системный вызов:

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

Он возвращает количество попыток системных вызовов ( ==Nargsесли SUPERSYSCALL__continue_on_failureфлаг передан в противном случае >0 && <=Nargs), и ошибки копирования между пространством ядра и пользовательским пространством сигнализируются с помощью segfaults вместо обычного -EFAULT.

Чего я не знаю, так это того, как это будет портировать на другие архитектуры, но было бы неплохо иметь что-то подобное в ядре.

Если бы это было возможно для всех архитектур, я думаю, что могла бы существовать оболочка пользовательского пространства, которая обеспечивала бы безопасность типов через некоторые объединения и макросы (она могла бы выбрать член объединения на основе имени системного вызова, и тогда все объединения были бы преобразованы в 6 длинных или какой бы ни был архитектурный эквивалент де 6 длин).

PSkocik
источник
1
Это хорошее доказательство концепции, хотя я хотел бы видеть массив указателей на long вместо массива long, чтобы вы могли делать такие вещи, как open-write-close, используя возвращение openin writeи close. Это немного увеличит сложность из-за get / put_user, но, вероятно, оно того стоит. Что касается переносимости IIRC, некоторые архитектуры могут заглушить регистры системного вызова для аргументов 5 и 6, если системный вызов аргумента 5 или 6 пакетирован ... добавление 2 дополнительных аргументов для будущего использования исправит это и может быть использовано в будущем для параметров асинхронного вызова, если установлен флаг SUPERSYSCALL__async
технозавр
1
Моим намерением было также добавить sys_memcpy. Затем пользователь может поместить его между sys_open и sys_write, чтобы скопировать возвращенный fd в первый аргумент sys_write без необходимости переключать режим обратно в пользовательское пространство.
PSkocik
3

Два основных момента, которые сразу приходят на ум:

  • Обработка ошибок: каждый отдельный системный вызов может заканчиваться ошибкой, которая должна быть проверена и обработана вашим кодом пользовательского пространства. Поэтому пакетный вызов должен был бы в любом случае запускать код пространства пользователя после каждого отдельного вызова, поэтому преимущества пакетных вызовов пространства ядра будут сведены на нет. Кроме того, API-интерфейс должен быть очень сложным (если вообще возможно его спроектировать) - например, как бы вы выразили логику, например, «если третий вызов не удался, сделайте что-нибудь и пропустите четвертый вызов, но продолжите с пятым»)?

  • Многие «комбинированные» вызовы, которые действительно реализуются, предлагают дополнительные преимущества, кроме того, что нет необходимости перемещаться между пользователем и пространством ядра. Например, они часто избегают копирования памяти и использования буферов в целом (например, переносят данные непосредственно из одного места в буфере страницы в другое вместо того, чтобы копировать их через промежуточный буфер). Конечно, это имеет смысл только для конкретных комбинаций вызовов (например, чтение-запись-запись), а не для произвольных комбинаций пакетных вызовов.

Михал Космульский
источник
2
Re: обработка ошибок. Я подумал об этом, и поэтому я предложил аргумент flags (BATCH_RET_ON_FIRST_ERR) ... успешный системный вызов должен вернуть ncalls, если все вызовы завершены без ошибок, или последний успешный вызов, если произошел сбой. Это позволит вам проверить наличие ошибок и, возможно, попытаться снова начать с первого неудачного вызова, просто увеличив значение на 2 указателя и уменьшив ncall на возвращаемое значение, если ресурс был просто занят или вызов был прерван. ... части, не связанные с переключением контекста, выходят за рамки этого, но начиная с Linux 4.2, splice () тоже может им помочь
технозавр
2
Ядро может автоматически оптимизировать список вызовов для объединения различных операций и устранения лишней работы. Ядро, вероятно, сделало бы лучшую работу, чем большинство отдельных разработчиков, с большой экономией усилий благодаря более простому API.
Александр Дубинский
@technosaurus Это не совместимо с идеей технозавров об исключениях, которые сообщают, какая операция завершилась неудачей (потому что порядок операций оптимизируется). Вот почему исключения обычно не предназначены для возврата такой точной информации (также потому, что код становится запутанным и хрупким). К счастью, нетрудно написать универсальные обработчики исключений, которые обрабатывают различные режимы сбоев.
Александр Дубинский