Linux / BSDに汎用のバッチ処理システムコールがないのはなぜですか?


17

バックグラウンド:

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

問題:

これは、のような(?余計な)システムコールをたくさん作成したsendmmsgを() recvmmsg()などのようにchdir、オープン、のlseekおよび/またはシンボリックリンクの組み合わせ:openatmkdiratmknodatfchownatfutimesatnewfstatatunlinkatfchdirftruncatefchmodrenameatlinkatsymlinkatreadlinkatfchmodatfaccessatlsetxattrfsetxattrexecveatlgetxattrllistxattrlremovexattrfremovexattrflistxattrfgetxattrpreadpwrite等...

現在copy_file_range()、読み取りlseekと書き込みsyscallを組み合わせたLinuxが追加されました。これがfcopy_file_range()、lcopy_file_range()、copy_file_rangeat()、fcopy_file_rangeat()およびlcopy_file_rangeat()になるまでの時間の問題です...しかし、X個の呼び出しの代わりに2個のファイルがあるため、X ^ 2になる可能性がありますもっと。わかりました、LinusとさまざまなBSD開発者はそこまで行きませんが、私のポイントは、バッチシステムコールがあれば、これらのすべて(ほとんど?) libc側にオーバーヘッドがある場合。

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

解決?:

汎用バッチ処理システムコール。これにより、特殊なカーネルスレッドを使用することに伴う複雑さなしに、最大コスト(複数モードスイッチ)が軽減されます(ただし、その機能は後で追加できます)。

socketcall()syscallには、基本的にプロトタイプの基本がすでにあります。引数の配列を取ることからそれを拡張して、代わりに戻り値の配列、引数の配列へのポインター(syscall番号を含む)、syscallの数、およびflags引数などを取得します。

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

大きな違いの1つは、前のsyscallsの結果を後続のsyscalls(たとえば/ で使用するためのファイル記述子)で使用できるように、引数はおそらくすべて単純化のためのポインターである必要があることです。open()read()write()

考えられるいくつかの利点:

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

質問:

バッチ処理システムコールを実装することは可能ですか?

  • 明らかな落とし穴がありませんか?
  • メリットを過大評価していますか?

バッチ処理システムコールの実装を気にすることは価値がありますか(Intel、Google、またはRedhatで働いていません)。

  • 以前に自分のカーネルにパッチを適用しましたが、LKMLを扱うのは恐ろしいです。
  • 歴史は、「普通の」ユーザー(git書き込みアクセスのない非企業のエンドユーザー)にとって何かが広く有用であっても、上流(unionfs、aufs、cryptodev、tuxoniceなど)に受け入れられることはないことを示しています。

参照:


4
私が見ているかなり明白な問題の1つは、カーネルがシステムコールに必要な時間とスペース、および単一のシステムコールの操作の複雑さに関する制御を放棄することです。基本的に、任意の無制限の量のカーネルメモリを割り当て、任意の無制限の時間実行し、任意の複雑なsyscallを作成しました。batchsyscallをbatchsyscallにネストすることにより、任意のsyscallの任意の深さの呼び出しツリーを作成できます。基本的に、アプリケーション全体を単一のシステムコールに入れることができます。
ヨルグWミットタグ

@JörgWMittag-これらを並行して実行することはお勧めしません。したがって、使用されるカーネルメモリの量は、バッチ内で最も重いシステムコール以上であり、カーネル内の時間は、ncallsパラメーターによって制限されます(任意の値)。ネストされたバッチsyscallが強力なツールであるのは正しいことです。おそらく、それを排除する必要があるほどです(静的ファイルサーバーの状況では、ポインターを使用して意図的にデーモンをカーネルループに固定することにより、古いTUXサーバーの実装)
テクノサウルス

1
Syscallには特権の変更が含まれますが、これは常にコンテキストスイッチとして特徴付けられるわけではありません。 en.wikipedia.org/wiki/...
エリックEidt

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

そして、それは動作しているようです-私はただ1つのシステムコールでfd 1にhellを、fd 2にworldを書くことができます:

#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上で物事がどのように機能するかのように見える普遍的なsyscallプロトタイプとして、私の「スーパー」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

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

これがすべてのアーチで可能であれば、いくつかのユニオンとマクロを介してタイプセーフを提供するユーザースペースラッパーがあると思います(システムコール名に基づいてユニオンメンバーを選択すると、すべてのユニオンが6つのlongに変換されます)または、アーキテクチャde jourの6つのlongと同等のもの)。


1
概念の優れた証拠ですが、openin writeおよびの戻り値を使用してopen-write-closeなどの操作を行えるように、単なるlong配列ではなくlongへのポインターの配列を見たいと思いますclose。get / put_userのために少し複雑さが増しますが、おそらくそれだけの価値があります。移植性IIRCに関しては、一部のアーキテクチャでは、5または6引数のsyscallがバッチ処理される場合、引数5および6のsyscallレジスタを上書きする可能性があります... SUPERSYSCALL__asyncフラグが設定されている
テクノサウルス

1
私の意図は、sys_memcpyも追加することでした。その後、ユーザーはsys_openとsys_writeの間に配置して、モードをユーザースペースに戻すことなく、返されたfdをsys_writeの最初の引数にコピーできます。
PSkocik

3

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

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

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


2
再:エラー処理。それについて考えたので、flags引数(BATCH_RET_ON_FIRST_ERR)を提案しました...すべての呼び出しがエラーなしで完了した場合、または失敗した場合は最後の成功した場合、成功したsyscallはncallを返します。これにより、エラーをチェックし、2つのポインターをインクリメントし、リソースがビジー状態であるかコールが中断された場合に戻り値でncallsをデクリメントするだけで、最初の失敗したコールから再試行することができます。...非コンテキストスイッチ部分はこれの範囲外ですが、Linux 4.2以降、splice()はそれらにも役立つ可能性があります
テクノサウルス

2
カーネルは呼び出しリストを自動的に最適化して、さまざまな操作をマージし、冗長な作業を排除できます。カーネルは、おそらく、よりシンプルなAPIを使用して労力を大幅に節約することで、ほとんどの個々の開発者よりも良い仕事をするでしょう。
アレクサンドルドゥビンスキー

@technosaurusどの操作が失敗したかを伝える例外のテクノサウルスの考えとは互換性がありません(操作の順序が最適化されるため)。これは、例外がそのような正確な情報を返すように通常設計されていない理由です(また、コードが混乱して壊れやすくなるため)。幸いなことに、さまざまな障害モードを処理する汎用の例外ハンドラーを作成することは難しくありません。
アレクサンドルドゥビンスキー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.