Linux / BSD에 일반 배치 syscall이없는 이유는 무엇입니까?


17

배경:

시스템 호출 오버 헤드는 주로 사용자 공간에서 커널 공간으로의 컨텍스트 전환 및 그에 따른 컨텍스트 호출로 인해 함수 호출 오버 헤드 (20-100x로 추정)보다 훨씬 큽니다. 함수 호출 오버 헤드를 절약하기 위해 함수를 인라인하는 것이 일반적이며 함수 호출은 syscall보다 훨씬 저렴합니다. 개발자가 한 시스템 호출에서 가능한 한 많은 커널 내부 작업을 처리하여 일부 시스템 호출 오버 헤드를 피하려고하는 이유가 있습니다.

문제:

이 같은 (? 불필요한) 시스템 호출을 많이 만들었습니다 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 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과 거의 같은 방식으로 커널과 사용자 공간 모두에 상당한 복잡성을 추가합니다 (비동기 호출에는 더 많은 설정이 필요함)

해결책?:

일반적인 배치 시스템 콜. 이는 특수한 커널 스레드를 갖는 것과 관련된 복잡성없이 최대 비용 (복수 모드 스위치)을 완화합니다 (기능은 나중에 추가 할 수 있음).

socketcall () syscall에는 기본적으로 프로토 타입을위한 좋은 기초가 있습니다. 인수 배열을 사용하여 반환 배열, 인수 배열에 대한 포인터 (syscall 번호 포함), syscall 수 및 플래그 인수 ...를 확장하십시오.

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

한 가지 중요한 차이점은 인수가 모두 단순성을위한 포인터 여야하므로 이전 syscall의 결과를 후속 syscall에서 사용할 수 있다는 것입니다 (예 : / open()에서 사용하기위한 파일 설명자 ).read()write()

몇 가지 가능한 장점 :

  • 적은 사용자 공간-> 커널 공간-> 사용자 공간 전환
  • 가능한 컴파일러 스위치 -fcombine-syscalls 자동 배치를 시도
  • 비동기 작업을위한 옵션 플래그 (fd를 반환하여 즉시 확인)
  • 사용자 공간에서 미래의 결합 된 syscall 함수를 구현하는 기능

질문:

배치 시스템 콜을 구현하는 것이 가능합니까?

  • 나는 명백한 문제를 놓치고 있습니까?
  • 혜택을 과대 평가하고 있습니까?

배치 시스템 콜 구현을 귀찮게하는 것이 가치가 있습니까 (Intel, Google 또는 Redhat에서 일하지 않습니다)?

  • 나는 전에 내 자신의 커널을 패치했지만 LKML을 다루는 것을 두려워합니다.
  • 역사는 "일반적인"사용자들 (git write access가없는 비 기업의 최종 사용자들)에게 광범위하게 유용한 것이라도 업스트림 (unionfs, aufs, cryptodev, tuxonice 등)에는 결코 받아 들여지지 않을 수 있음을 보여주었습니다.

참고 문헌 :


4
내가보고있는 상당히 명백한 문제 중 하나는 커널이 syscall에 필요한 시간과 공간뿐만 아니라 단일 syscall 작업의 복잡성에 대한 제어를 포기한다는 것입니다. 기본적으로 임의의 무제한 양의 커널 메모리를 할당하고 임의의 무제한 시간 동안 실행하며 임의로 복잡 할 수있는 syscall을 만들었습니다. batchsyscall을 batchsyscall에 내포 하면 임의의 syscall로 임의로 깊은 호출 트리를 만들 수 있습니다. 기본적으로 전체 응용 프로그램을 단일 syscall에 넣을 수 있습니다.
Jörg W Mittag

@ JörgWMittag-이것들이 병렬로 실행된다고 제안하지는 않으므로 사용 된 커널 메모리의 양은 배치에서 가장 큰 syscall 이상이 아니며 커널의 시간은 여전히 ​​ncalls 매개 변수에 의해 제한됩니다 ( 임의의 값). 중첩 된 배치 syscall이 강력한 도구 일 것입니다. 어쩌면 너무 배제되어야합니다 (정적 파일 서버 상황에서 유용합니다-의도적으로 포인터를 사용하여 커널 루프에 데몬을 붙여서)-기본적으로 이전 TUX 서버 구현)
technosaurus

1
Syscall에는 권한 변경이 포함되지만 이것이 항상 상황 전환으로 특징되는 것은 아닙니다. en.wikipedia.org/wiki/…
Erik Eidt

1
좀 더 동기와 배경을 제공하는이 어제 읽어 matildah.github.io/posts/2016-01-30-unikernel-security.html

커널 스택 오버플로를 방지하기 위해 @ JörgWMittag 중첩이 허용되지 않을 수 있습니다. 그렇지 않으면, 개별 syscall은 평상시와 같이 자체적으로 해제됩니다. 이와 관련하여 어떤 자원 문제도 없어야합니다. 리눅스 커널은 선점 가능합니다.
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);

그리고 그것은 작동하는 것처럼 보입니다. 단 하나의 syscall로 hello를 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에서 작동하는 방식으로 보이는 범용 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)과 실패는 커널 공간과 사용자 공간 대신 보통의 세그먼테이션 폴트 (segfault)에 의해 신호하는 사이에 복사합니다 -EFAULT.

내가 모르는 것은 이것이 다른 아키텍처로 포팅되는 방법이지만 커널에서 이와 같은 것을 갖는 것이 좋을 것입니다.

이것이 모든 아치에서 가능하다면, 일부 공용체와 매크로를 통해 유형 안전을 제공하는 사용자 공간 래퍼가있을 수 있다고 생각합니다 (syscall 이름을 기반으로 공용체 멤버를 선택할 수 있으며 모든 공용체가 6 long으로 변환됩니다) 또는 아키텍처가 6 long과 동등한 것이 무엇이든간에).


1
개념의 좋은 증거이지만 long 배열 대신 long 배열에 대한 포인터 배열을보고 싶기 때문에 openin write및 의 반환을 사용하여 open-write-close와 같은 작업을 수행 할 수 close있습니다. get / put_user로 인해 복잡성이 약간 증가하지만 그만한 가치가 있습니다. 이식성 IIRC와 관련하여 5 또는 6 개의 arg syscall이 일괄 처리되는 경우 일부 아키텍처는 args 5 및 6에 대한 syscall 레지스터를 방해 할 수 있습니다. SUPERSYSCALL__async 플래그 설정
technosaurus

1
내 의도는 sys_memcpy도 추가하는 것이 었습니다. 그런 다음 사용자는 sys_open과 sys_write 사이에 넣어서 반환 된 fd를 사용자 공간으로 다시 전환하지 않고도 sys_write의 첫 번째 인수에 복사 할 수 있습니다.
PSkocik

3

즉시 생각 나는 두 가지 주요 문제점은 다음과 같습니다.

  • 오류 처리 : 각 개별 syscall은 사용자 공간 코드로 확인하고 처리해야하는 오류로 끝날 수 있습니다. 따라서 배치 호출은 각 개별 호출 후에 사용자 공간 코드를 실행해야하므로 배치 커널 공간 호출의 이점은 무시됩니다. 또한 API는 매우 복잡해야합니다 (가능한 경우 전혀 디자인해야 함). 예를 들어 "세 번째 호출이 실패한 경우 무언가를 수행하고 네 번째 호출을 건너 뛰고 다섯 번째 호출은 계속합니다"와 같은 논리를 표현하는 방법은 무엇입니까?

  • 실제로 구현되는 많은 "결합 된"호출은 사용자와 커널 공간 사이를 이동할 필요없이 추가 이점을 제공합니다. 예를 들어 메모리 복사와 버퍼 사용을 피하는 경우가 종종 있습니다 (예 : 중간 버퍼를 통해 복사하는 대신 페이지 버퍼의 한 곳에서 다른 곳으로 데이터를 직접 전송). 물론, 이는 일괄 통화의 임의 조합이 아닌 특정 통화 조합 (예 : 읽기-쓰기)에만 적합합니다.


2
다시 : 오류 처리. 나는 그것에 대해 생각했기 때문에 플래그 인수 (BATCH_RET_ON_FIRST_ERR)를 제안했습니다 ... 모든 호출이 오류없이 완료되면 성공한 syscall이 ncalls를 반환하거나 실패하면 마지막 성공한 syscall을 반환해야합니다. 이를 통해 오류가 있는지 확인하고 리소스가 사용 중이거나 통화가 중단 된 경우 포인터 2 개를 늘리고 반환 값만큼 ncall을 줄임으로써 첫 번째 실패한 통화에서 다시 시작할 수 있습니다. ... 문맥이 아닌 switiching 부분은 이것에 대한 범위를 벗어 났지만, Linux 4.2 이후 splice ()도 그것들을 도울 수 있습니다
technosaurus

2
커널은 호출 목록을 자동으로 최적화하여 다양한 작업을 병합하고 중복 작업을 제거 할 수 있습니다. 커널은 아마도 더 간단한 API로 많은 비용을 절약하면서 대부분의 개별 개발자보다 더 나은 작업을 수행 할 것입니다.
Aleksandr Dubinsky

@technosaurus 어떤 작업이 실패했는지를 알려주는 technosaurus의 예외 아이디어와 호환되지 않습니다 (작업 순서가 최적화 되었기 때문에). 그렇기 때문에 예외는 일반적으로 정확한 정보를 반환하도록 설계되지 않은 이유입니다 (코드가 혼동되고 깨지기 쉬우므로). 다행히도 다양한 실패 모드를 처리하는 일반 예외 처리기를 작성하는 것은 어렵지 않습니다.
Aleksandr Dubinsky
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.