Чому в Linux / BSD не існує загальної пакетної системної виклику?


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()що, очевидно, поєднує в собі читання lseek та запису системних дзвінків. Є лише питання часу, перш ніж це стане fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () та lcopy_file_rangeat () ... але оскільки замість X більше дзвінків задіяні 2 файли, це може стати X ^ 2 більше. Гаразд, Лінус та різні розробники BSD не давали б цього зайти, але я висловлюю думку про те, що якби пакетна системна виклик, все (більшість?) З них можна було б реалізувати в просторі користувача та зменшити складність ядра, не додаючи багато якщо є накладні на стороні libc.

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

Рішення ?:

Загальний системний виклик. Це зменшило б найбільшу вартість (перемикачі декількох режимів) без складностей, пов’язаних із наявністю спеціалізованої нитки ядра (хоча цю функціональність можна буде додати пізніше).

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

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

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

Деякі можливі переваги:

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

Питання:

Чи можливо здійснити пакетний системний виклик?

  • Невже я пропускаю якісь очевидні прищі?
  • Чи я завищую переваги?

Чи варто мені заважати впроваджувати пакетний syscall (я не працюю в Intel, Google чи Redhat)?

  • Я вже виправляв власне ядро, але боявся мати справу з LKML.
  • Історія показала, що навіть якщо "звичайним" користувачам щось є корисним (некорпоративним кінцевим користувачам без доступу до запису git), воно ніколи не може бути прийнятим вгору за течією (unionfs, aufs, cryptodev, tuxonice тощо).

Список літератури:


4
Одна досить очевидна проблема, яку я бачу, полягає в тому, що ядро ​​відмовляється від часу та простору, необхідного для системного виклику, а також складності операцій однієї системної виклику. Ви в основному створили системний виклик, який може виділяти довільну, необмежену кількість пам’яті ядра, запускати довільну, необмежену кількість часу і може бути довільно складним. До гніздування batchсистемних викликів в batchсистемні виклики, ви можете створити скільки завгодно глибоке дерево викликів довільних системних викликів. В основному, ви можете помістити всю свою програму в єдиний системний виклик.
Йорг W Міттаг

@ JörgWMittag - Я не припускаю, що вони працюватимуть паралельно, тому об'єм використовуваної пам'яті ядра буде не більшим, ніж найважчий syscall у пакеті, а час у ядрі все ще обмежений параметром ncalls (який може бути обмежений деяке довільне значення). Ви маєте рацію в тому, що вкладений пакетний syscall є потужним інструментом, можливо, настільки, що його слід виключити (хоча я міг би бачити, що це корисно в ситуації із сервером статичних файлових файлів - навмисно вставляючи демон у цикл ядра за допомогою покажчиків - в основному реалізація старого сервера 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 to fd лише одним 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);
}

В основному я використовую:

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

як універсальний прототип syscall, який, як видається, працює на x86_64, тому мій "супер" 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);

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

Чого я не знаю, це як би це порту для інших архітектур, але було б точно було б мати щось подібне в ядрі.

Якби це було можливим для всіх архівів, я думаю, може існувати обгортка простору користувача, яка б забезпечувала безпеку типу через деякі союзи та макроси (вона могла вибрати члена об'єднання на основі імені syscall, і всі об'єднання потім перетворилися б на 6 довгих або будь-яка еквівалентна архітектура де Журі 6-ти довгім).


1
Це хороший доказ концепції, хоча я хотів би бачити масив покажчиків на довгі замість просто масиву довгих, щоб ви могли робити такі речі, як відкрити-записати-закрити, використовуючи повернення openв 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: поводження з помилками. Я подумав про це, і саме тому я запропонував аргумент прапорів (BATCH_RET_ON_FIRST_ERR) ... успішний системний виклик повинен повернути ncalls, якщо всі виклики завершені без помилки або останній успішний, якщо один не вдасться. Це дозволить вам перевірити наявність помилок і, можливо, спробувати знову починати при першому невдалому виклику, просто збільшивши 2 покажчики та зменшивши ncalls на повернене значення, якщо ресурс був лише зайнятий або виклик був перерваний. ... частини, що не переключаються на контекст, для цього не входять в рамки, але оскільки Linux 4.2, сплайс () також може допомогти і цим
технозавр

2
Ядро може автоматично оптимізувати список викликів для об'єднання різних операцій та усунення зайвих робіт. Ядро, ймовірно, зробить кращу роботу, ніж більшість окремих розробників за великої економії зусиль з більш простим API.
Олександр Дубінський

@technosaurus Це не було б сумісно з ідеєю технозавра про винятки, які повідомляють, яка операція не вдалася (оскільки порядок операцій стає оптимізованим). Ось чому виняток зазвичай не призначений для повернення такої точної інформації (також, оскільки код стає заплутаним і неміцним). На щастя, не важко написати загальні обробники винятків, які обробляють різні режими відмов.
Олександр Дубінський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.