web-dev-qa-db-ja.com

Linux / BSDに汎用バッチsyscallがないのはなぜですか?

背景:

システムコールのオーバーヘッドは、関数呼び出しのオーバーヘッドよりもはるかに大きく(推定範囲は20〜100倍)、主にユーザー空間からカーネル空間へのコンテキストの切り替えとその逆によるものです。関数呼び出しのオーバーヘッドを節約するためにインライン関数が一般的であり、関数呼び出しはsyscallよりはるかに安価です。開発者が1つのシステムコールでカーネル内の操作をできる限り処理することにより、システムコールのオーバーヘッドの一部を回避したいのは当然のことです。

問題:

これにより、 sendmmsg()recvmmsg() のような多くの(余分な?)システムコールが作成され、さらに次のようなchdir、open、lseek、symlinkの組み合わせも作成されます。 openatmkdiratmknodatfchownatfutimesatnewfstatatunlinkatfchdirftruncatefchmodrenameatlinkatsymlinkatreadlinkatfchmodatfaccessatlsetxattrfsetxattrexecveatlgetxattrllistxattrlremovexattrfremovexattrflistxattrfgetxattrpreadpwriteなど...

Linuxにcopy_file_range()が追加されました。これは、読み取りlseekと書き込みsyscallを組み合わせたようです。これがfcopy_file_range()、lcopy_file_range()、copy_file_rangeat()、fcopy_file_rangeat()、lcopy_file_rangeat()になるまでの時間の問題ですが、X回の呼び出しの代わりに2つのファイルが含まれているため、X ^ 2になる可能性がありますもっと。さて、LinusやさまざまなBSD開発者はそれをこれほど遠くに行かせませんでしたが、私のポイントは、バッチ処理システムコールがあった場合、これらのすべて(ほとんど?)がユーザー空間に実装され、あまり追加せずにカーネルの複雑さを軽減できるということです。 libc側にオーバーヘッドがある場合。

多くの複雑なソリューションが提案されており、syscallをバッチ処理するための非ブロッキングsyscallのための何らかの特別なsyscallスレッドが含まれています。ただし、これらのメソッドは、libxcbとlibX11の場合とほぼ同じ方法で、カーネルとユーザー空間の両方にかなりの複雑さを追加します(非同期呼び出しには、さらに多くの設定が必要です)。

ソリューション?:

一般的なバッチsyscall。これにより、特別なカーネルスレッドを使用することに伴う複雑さなしに、最大のコスト(マルチモードスイッチ)が軽減されます(ただし、この機能は後で追加できます)。

基本的に、socketcall()syscallのプロトタイプには、すでに適切な基礎があります。引数の配列を取ることから、代わりに戻り値の配列、引数の配列へのポインタ(syscall番号を含む)、syscallsの数、およびフラグ引数を取るように拡張するだけです。

_batch(void *returns, void *args, long ncalls, long flags);
_

1つの大きな違いは、引数がおそらくallを単純にするためにポインタである必要があるため、前のシステムコールの結果を後続のシステムコールで使用できるようにすることです(たとえば、[ open()-read()/write()で使用)

いくつかの可能な利点:

  • ユーザースペースの削減->カーネルスペース->ユーザースペースの切り替え
  • 自動的にバッチ処理を試みるコンパイラスイッチ-fcombine-syscalls
  • 非同期操作のオプションのフラグ(すぐに監視するにはfdを返します)
  • ユーザー空間で将来の組み合わせたsyscall関数を実装する機能

質問:

バッチsyscallを実装することは可能ですか?

  • 私はいくつかの明らかな落とし穴を逃していますか?
  • メリットを過大評価していますか?

バッチsyscallを実装する手間をかける価値はありますか(私はIntel、Google、またはRedhatで働いていません)?

  • 以前に自分のカーネルにパッチを当てたことがありますが、LKMLを扱うのは怖いです。
  • 歴史は、「通常の」ユーザー(git書き込みアクセス権のない非企業のエンドユーザー)にとって何かが広く役立つとしても、上流で受け入れられない可能性があることを示しています(unionfs、aufs、cryptovev、tuxoniceなど...)

参考文献:

18
technosaurus

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

そして、それは機能しているようです-syscallを1つだけ使用して、hdを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);

ユニバーサル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)を返し、カーネルスペースとユーザースペース間でのコピーの失敗は、通常の-EFAULTではなくsegfaultによって通知されます。

私が知らないことは、これが他のアーキテクチャにどのように移植されるかですが、カーネルにこのようなものがあるといいでしょう。

これがすべてのアーチで可能である場合、いくつかのユニオンとマクロを介してタイプセーフを提供するユーザー空間ラッパーがあると思います(syscall名に基づいてユニオンメンバーを選択し、すべてのユニオンを6つのlongに変換することができます)または、アーキテクチャのde jourで6つのlongに相当するものは何でも)。

5
PSkocik

すぐに頭に浮かぶ2つの主な落とし穴は次のとおりです。

  • エラー処理:個々のsyscallはエラーで終了する可能性があり、ユーザー空間コードでチェックおよび処理する必要があります。したがって、バッチ呼び出しでは、個々の呼び出しの後にユーザー空間コードを実行する必要があるため、カーネル空間呼び出しのバッチ処理の利点は無効になります。さらに、APIは非常に複雑である必要があります(可能な場合は設計する)-たとえば、「3番目の呼び出しが失敗した場合、何かを実行して4番目の呼び出しをスキップして5番目の呼び出しを続行する」などのロジックをどのように表現しますか?

  • 実際に実装される多くの「結合」呼び出しは、ユーザー空間とカーネル空間の間を移動する必要がないことを除いて、追加の利点を提供します。たとえば、メモリのコピーやバッファの使用を完全に回避することがよくあります(たとえば、中間バッファを介してコピーするのではなく、ページバッファのある場所から別の場所に直接データを転送します)。もちろん、これは呼び出しの特定の組み合わせ(読み取りと書き込みなど)に対してのみ意味があり、バッチ呼び出しの任意の組み合わせに対しては意味がありません。

4