为什么Linux / BSD中没有通用批处理系统调用?


17

背景:

系统调用开销比函数调用开销大​​得多(估计范围为20-100x),这主要是由于上下文从用户空间切换到内核空间再返回。内联函数通常可以节省函数调用开销,并且函数调用比syscall便宜得多。可以说,开发人员希望通过尽可能多地在一个系统调用中进行内核内操作来避免一些系统调用开销。

问题:

这已经创造了很多(多余的?)系统调用像sendmmsg() 的recvmmsg()还有CHDIR,开放,lseek的和/或类似的符号链接组合:openatmkdiratmknodatfchownatfutimesatnewfstatatunlinkatfchdirftruncatefchmodrenameatlinkatsymlinkatreadlinkatfchmodatfaccessatlsetxattrfsetxattrexecveatlgetxattrllistxattrlremovexattrfremovexattrflistxattrfgetxattrpreadpwrite等...

现在,Linux已添加copy_file_range(),显然将读取lseek和写入syscall组合在一起。变成fcopy_file_range(),lcopy_file_range(),copy_file_rangeat(),fcopy_file_rangeat()和lcopy_file_rangeat()只是时间问题...但是由于涉及2个文件而不是另外X个调用,因此它可能变成X ^ 2更多。好的,Linus和各种BSD开发人员不会放任不管,但我的观点是,如果有一个批处理syscall,那么所有这些(大部分?)都可以在用户空间中实现,并且可以减少内核复杂性而无需增加太多如果libc方面有任何开销。

已经提出了许多复杂的解决方案,其中包括某种形式的特殊syscall线程,用于非阻塞syscall到批处理syscall。但是,这些方法以与libxcb和libX11几乎相同的方式为内核和用户空间增加了相当大的复杂性(异步调用需要更多的设置)

解?:

通用批处理系统调用。这将减轻最大的成本(多模式切换),而不会具有与专用内核线程相关的复杂性(尽管以后可以添加功能)。

socketcall()系统调用中的原型基本上已经有了良好的基础。只需将其从采用参数数组扩展为采用返回数组,指向参数数组的指针(包括系统调用号),系统调用数和标志参数即可,如下所示:

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

一个主要的区别是,为了简单起见,参数可能全部都需要用作指针,以便先前的syscall的结果可以被后续的syscall使用(例如open()read()/中使用的文件描述符write()

一些可能的优点:

  • 更少的用户空间->内核空间->用户空间切换
  • 可能的编译器开关-fcombine-syscalls尝试自动进行批处理
  • 异步操作的可选标志(返回fd立即观看)
  • 在用户空间中实现将来的组合syscall功能的能力

题:

实现批处理系统调用是否可行?

  • 我是否缺少一些明显的陷阱?
  • 我是否高估了收益?

对我来说,麻烦实施批处理系统调用是否值得(我不在Intel,Google或Redhat工作)?

  • 我之前已经修补了自己的内核,但不愿处理LKML。
  • 历史表明,即使某些东西对“普通”用户(没有git写访问权限的非公司最终用户)广泛有用,它也可能永远不会在上游被接受(unionfs,aufs,cryptodev,tuxonice等)。

参考文献:


4
我看到的一个相当明显的问题是内核放弃了对系统调用所需的时间和空间以及单个系统调用的操作复杂性的控制。您基本上已经创建了一个syscall,它可以分配任意数量的内核内存,可以运行任意数量的时间,并且可以任意复杂。通过将batch系统batch调用嵌套到系统调用中,可以创建任意系统调用的任意深度的调用树。基本上,您可以将整个应用程序放入单个syscall中。
约尔格W¯¯米塔格

@JörgWMittag-我不建议它们并行运行,因此内核内存使用量不超过批处理中最重的syscall,内核中的时间仍然受ncalls参数限制(可以限制为一些任意值)。嵌套批处理系统调用是一个功能强大的工具,这也许是对的,所以应该排除它(虽然我可以看到它在静态文件服务器的情况下很有用-通过有意使用指针将守护程序粘贴到内核循环中,这基本上是对的)实施旧的TUX服务器)
technosaurus,2016年

1
系统调用涉及特权更改,但这并不总是以上下文切换为特征。 en.wikipedia.org/wiki/...
埃里克Eidt

1
昨天阅读了这篇文章,它提供了更多的动机和背景信息:matildah.github.io/posts/2016-01-30-unikernel-security.html
汤姆

禁止使用@JörgWMittag嵌套以防止内核堆栈溢出。否则,单个系统调用将像往常一样自行释放。不应有任何占用资源的问题。Linux内核是可抢占的。
PSkocik

Answers:


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

它似乎可行-我可以通过一个syscall向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上的工作方式,因此我的“超级”系统调用为:

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

它返回尝试的syscall数量(==Nargs如果SUPERSYSCALL__continue_on_failure通过了该标志,则返回>0 && <=Nargs),并且在segfaults而不是通常的segfaults信号指示无法在内核空间和用户空间之间进行复制-EFAULT

我不知道这将如何移植到其他体系结构,但是在内核中包含类似的东西肯定会很好。

如果所有拱形都可行,那么我想可能会有一个用户空间包装器通过一些联合和宏来提供类型安全性(它可以根据syscall名称选择一个联合成员,然后所有联合都将转换为6个long或任何相当于6个long的dejour架构)。


1
这是一个很好的概念证明,尽管我希望看到一个指向long的指针数组,而不只是一个long数组,以便您可以使用openin write和返回值来执行诸如open-write-close之类的操作close。由于get / put_user,这会稍微增加复杂性,但可能值得。关于可移植性IIRC,如果批处理5个或6个arg系统调用,则某些体系结构可能会破坏args 5和6的syscall寄存器...添加2个额外的args供将来使用将解决该问题,并且如果将来可用于异步调用参数设置了SUPERSYSCALL__async标志
Technosaurus

1
我的意图是还要添加sys_memcpy。然后,用户可以将其放在sys_open和sys_write之间,以将返回的fd复制到sys_write的第一个参数,而不必将模式切换回用户空间。
PSkocik

3

立即想到的两个主要陷阱是:

  • 错误处理:每个单独的syscall都可能以错误结尾,该错误需要由用户空间代码检查和处理。因此,批处理调用无论如何都要在每个单独的调用之后运行用户空间代码,因此批处理内核空间调用的好处将被否定。此外,API必须非常复杂(如果可能的话,完全可以进行设计)-例如,您将如何表达逻辑,例如“如果第三次调用失败,则执行某些操作并跳过第四次调用,但继续第五次调用”)?

  • 除了不必在用户空间和内核空间之间移动之外,实际上确实实现了许多“组合”调用还提供了其他好处。例如,他们通常会避免复制内存并完全使用缓冲区(例如,将数据从页面缓冲区中的一个位置直接传输到另一位置,而不是通过中间缓冲区复制数据)。当然,这仅对特定的调用组合(例如,先读后写)有意义,而对于批处理调用的任意组合都没有意义。


2
回复:错误处理。我对此进行了考虑,这就是为什么我建议使用flags参数(BATCH_RET_ON_FIRST_ERR)的原因...如果所有调用均已成功完成而没有错误,则成功的syscall应返回ncall;如果失败则返回最后的成功。这将允许您检查错误,并可能在第一个不成功的调用中再次尝试,只要增加2个指针,然后将ncalls减小返回值(如果资源正忙或调用已中断),就可以再次尝试。...非上下文切换部分超出了此范围,但是自Linux 4.2起,splice()也可以帮助解决这些问题
technosaurus

2
内核可以自动优化呼叫列表以合并各种操作并消除多余的工作。内核可能会比大多数单独的开发人员做得更好,并且可以通过更简单的API节省大量精力。
Aleksandr Dubinsky

@technosaurus这与technosaurus的异常思想不兼容,后者会传达哪个操作失败(因为优化了操作顺序)。这就是为什么通常不将异常设计为返回如此精确的信息的原因(同样,因为代码变得混乱和脆弱)。幸运的是,编写处理各种故障模式的通用异常处理程序并不难。
Aleksandr Dubinsky
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.