Mengapa tidak ada syscall batching generik di Linux / BSD?

17

Latar Belakang:

Overhead panggilan sistem jauh lebih besar daripada overhead panggilan fungsi (perkiraan berkisar antara 20-100x) sebagian besar karena pergantian konteks dari ruang pengguna ke ruang kernel dan kembali. Adalah umum untuk fungsi sebaris untuk menyimpan overhead panggilan fungsi dan panggilan fungsi jauh lebih murah daripada syscalls. Masuk akal bahwa pengembang ingin menghindari beberapa overhead panggilan sistem dengan menjaga sebanyak mungkin operasi dalam kernel dalam satu syscall mungkin.

Masalah:

Hal ini telah menciptakan banyak (berlebihan?) Panggilan sistem seperti sendmmsg () , recvmmsg () serta chdir, terbuka, lseek dan / atau kombinasi symlink seperti: 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, pwritedll ...

Sekarang Linux telah menambahkan copy_file_range()yang tampaknya menggabungkan read lseek dan write syscalls. Hanya masalah waktu sebelum ini menjadi fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () ... dan lcopy_file_rangeat () ... tetapi karena ada 2 file yang terlibat, bukan X lebih banyak panggilan, itu bisa menjadi X ^ 2 lebih. OK, Linus dan berbagai pengembang BSD tidak akan membiarkannya sejauh itu, tetapi poin saya adalah bahwa jika ada syscall batching, semua (sebagian besar?) Ini dapat diimplementasikan di ruang pengguna dan mengurangi kompleksitas kernel tanpa menambahkan banyak jika ada overhead di sisi libc.

Banyak solusi kompleks telah diusulkan yang mencakup beberapa bentuk syscall thread khusus untuk syscalls non-blocking ke syscalls proses batch; namun metode ini menambah kompleksitas yang signifikan pada kernel dan ruang pengguna dengan cara yang hampir sama dengan libxcb vs libX11 (panggilan asinkron membutuhkan lebih banyak pengaturan)

Larutan?:

Syscall batching generik. Ini akan mengurangi biaya terbesar (beberapa mode switch) tanpa kerumitan yang terkait dengan memiliki utas kernel khusus (meskipun fungsionalitas itu dapat ditambahkan kemudian).

Pada dasarnya sudah ada dasar yang baik untuk prototipe di soketcall () syscall. Hanya perluas dari mengambil array argumen untuk bukannya mengambil array pengembalian, pointer ke array argumen (yang termasuk nomor syscall), jumlah syscalls dan argumen flags ... sesuatu seperti:

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

Satu perbedaan utama adalah bahwa argumen mungkin semua harus menjadi petunjuk untuk kesederhanaan sehingga hasil dari syscall sebelumnya dapat digunakan oleh syscalls berikutnya (misalnya deskriptor file dari open()untuk digunakan di read()/ write())

Beberapa kemungkinan keuntungan:

  • lebih sedikit ruang pengguna -> ruang kernel -> perpindahan ruang pengguna
  • switch kompiler yang mungkin -fcombine-syscalls untuk mencoba melakukan batch secara otomatis
  • panji opsional untuk operasi asinkron (kembalikan fd untuk menonton segera)
  • kemampuan untuk mengimplementasikan fungsi syscall gabungan di masa depan dalam userspace

Pertanyaan:

Apakah layak untuk menerapkan syscall batching?

  • Apakah saya kehilangan beberapa Gotcha yang jelas?
  • Apakah saya melebih-lebihkan manfaatnya?

Apakah ada gunanya bagi saya untuk repot menerapkan syscall batching (saya tidak bekerja di Intel, Google atau Redhat)?

  • Saya telah menambal kernel saya sendiri sebelumnya, tetapi takut berurusan dengan LKML.
  • Sejarah telah menunjukkan bahwa meskipun sesuatu secara luas bermanfaat bagi pengguna "normal" (pengguna akhir non-perusahaan tanpa akses tulis git), itu mungkin tidak akan pernah diterima di hulu (unionfs, aufs, cryptodev, tuxonice, dll ...)

Referensi:

technosaurus
sumber
4
Satu masalah yang cukup jelas yang saya lihat adalah bahwa kernel memberikan kontrol tentang waktu dan ruang yang diperlukan untuk syscall serta kompleksitas operasi dari syscall tunggal. Anda pada dasarnya telah membuat syscall yang dapat mengalokasikan jumlah memori kernel yang sewenang-wenang, tidak terikat, dijalankan untuk jumlah waktu yang sewenang-wenang dan tidak terikat, dan dapat rumit secara sewenang-wenang. Dengan batchmemasukkan syscalls ke dalam batchsyscalls, Anda dapat membuat pohon panggilan yang dalam secara sewenang-wenang dari syscall yang sewenang-wenang. Pada dasarnya, Anda dapat menempatkan seluruh aplikasi Anda menjadi satu syscall.
Jörg W Mittag
@ JörgWMittag - Saya tidak menyarankan ini berjalan secara paralel, sehingga jumlah memori kernel yang digunakan tidak lebih dari syscall terberat dalam batch dan waktu di kernel masih dibatasi oleh parameter ncalls (yang bisa dibatasi hingga beberapa nilai arbitrer). Anda benar tentang syscall batch bersarang menjadi alat yang kuat, mungkin begitu banyak sehingga harus dihilangkan (meskipun saya bisa melihatnya berguna dalam situasi server file statis - dengan sengaja memasukkan daemon ke dalam loop kernel menggunakan pointer - pada dasarnya mengimplementasikan server TUX lama)
technosaurus
1
Syscalls melibatkan perubahan hak istimewa tetapi ini tidak selalu ditandai sebagai saklar konteks. en.wikipedia.org/wiki/…
Erik Eidt
1
baca ini kemarin yang memberikan beberapa motivasi dan latar belakang: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom
@ JörgWMittag nesting dapat dianulir untuk mencegah dari stack stack overflow. Jika tidak, syscall individual akan membebaskan diri mereka sendiri seperti biasanya. Seharusnya tidak ada masalah memonopoli sumber daya dengan ini. Kernel Linux adalah preemptible.
PSkocik

Jawaban:

5

Saya mencoba ini di x86_64

Patch terhadap 94836ecf1e7378b64d37624fbb81fe48fbd4c772: (juga di sini 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);

Dan tampaknya berfungsi - saya dapat menulis halo ke fd 1 dan dunia ke fd 2 hanya dengan satu 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);
}

Pada dasarnya saya menggunakan:

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

sebagai prototipe syscall universal, yang tampaknya merupakan cara kerja x86_64, jadi syscall "super" saya adalah:

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

Ia mengembalikan jumlah syscalls yang dicoba ( ==Nargsjika SUPERSYSCALL__continue_on_failurebendera dilewatkan, jika tidak >0 && <=Nargs) dan kegagalan untuk menyalin antara ruang kernel dan ruang pengguna ditandai oleh segfault bukannya biasa -EFAULT.

Apa yang saya tidak tahu adalah bagaimana ini akan port ke arsitektur lain, tetapi tentu akan menyenangkan untuk memiliki sesuatu seperti ini di kernel.

Jika ini memungkinkan untuk semua lengkungan, saya membayangkan mungkin ada pembungkus userspace yang akan memberikan keamanan tipe melalui beberapa serikat pekerja dan makro (bisa memilih anggota serikat berdasarkan nama syscall dan semua serikat pekerja kemudian akan dikonversi ke 6 rindu atau apa pun yang setara dengan arsitektur 6 jam dari).

PSkocik
sumber
1
Ini bukti konsep yang bagus, meskipun saya ingin melihat array pointer ke panjang, bukan hanya array panjang, sehingga Anda bisa melakukan hal-hal seperti buka-tulis-tutup menggunakan kembalinya opendalam writedan close. Itu akan meningkatkan kompleksitas sedikit karena mendapatkan / put_user, tetapi mungkin sepadan. Mengenai portabilitas IIRC, beberapa arsitektur dapat mengalahkan register syscall untuk args 5 dan 6 jika syscall 5 atau 6 arg disatukan ... menambahkan 2 arg tambahan untuk penggunaan di masa mendatang akan memperbaikinya dan dapat digunakan di masa depan untuk parameter panggilan asinkron jika bendera SUPERSYSCALL__async disetel
technosaurus
1
Niat saya adalah untuk juga menambahkan sys_memcpy. Pengguna kemudian dapat meletakkannya di antara sys_open dan sys_write untuk menyalin kembali fd ke argumen pertama sys_write tanpa harus beralih mode kembali ke userspace.
PSkocik
3

Dua gotchas utama yang langsung terlintas dalam pikiran adalah:

  • Penanganan kesalahan: setiap syscall dapat diakhiri dengan kesalahan yang perlu diperiksa dan ditangani oleh kode ruang pengguna Anda. Karena itu panggilan batching harus menjalankan kode ruang pengguna setelah setiap panggilan individu sehingga manfaat dari panggilan batch ruang kernel akan dinegasikan. Selain itu, API harus sangat kompleks (jika mungkin dirancang sama sekali) - misalnya bagaimana Anda mengekspresikan logika seperti "jika panggilan ketiga gagal, lakukan sesuatu dan lewati panggilan keempat tetapi lanjutkan dengan yang kelima")?

  • Banyak panggilan "gabungan" yang sebenarnya diimplementasikan menawarkan manfaat tambahan selain dari tidak harus berpindah antara ruang pengguna dan kernel. Misalnya, mereka akan sering menghindari menyalin memori dan menggunakan buffer sama sekali (misalnya mentransfer data langsung dari satu tempat di buffer halaman ke yang lain alih-alih menyalinnya melalui buffer perantara). Tentu saja, ini hanya masuk akal untuk kombinasi panggilan tertentu (mis. Baca-lalu-tulis), bukan untuk kombinasi panggilan batch yang sewenang-wenang.

Michał Kosmulski
sumber
2
Re: penanganan kesalahan. Saya memikirkan hal itu dan itulah sebabnya saya menyarankan argumen flags (BATCH_RET_ON_FIRST_ERR) ... syscall yang berhasil harus mengembalikan ncalls jika semua panggilan selesai tanpa kesalahan atau yang terakhir berhasil jika salah satu gagal. Ini akan memungkinkan Anda untuk memeriksa kesalahan dan mungkin mencoba lagi mulai dari panggilan pertama yang gagal hanya dengan menambah 2 pointer dan mengurangi panggilan dengan nilai kembali jika sumber daya hanya sibuk atau panggilan itu terputus. ... bagian switiching non-konteks berada di luar jangkauan untuk ini, tetapi karena Linux 4.2, splice () dapat membantu mereka juga
technosaurus
2
Kernel dapat secara otomatis mengoptimalkan daftar panggilan untuk menggabungkan berbagai operasi dan menghilangkan pekerjaan yang berlebihan. Kernel mungkin akan melakukan pekerjaan yang lebih baik daripada kebanyakan pengembang individu dengan penghematan besar dalam upaya dengan API yang lebih sederhana.
Aleksandr Dubinsky
@ technosaurus Ini tidak akan kompatibel dengan gagasan technosaurus tentang pengecualian yang mengkomunikasikan operasi mana yang gagal (karena urutan operasi dioptimalkan). Inilah sebabnya mengapa pengecualian biasanya tidak dirancang untuk mengembalikan informasi yang akurat (juga, karena kode menjadi membingungkan dan rapuh). Untungnya, tidak sulit untuk menulis penangan pengecualian generik yang menangani berbagai mode kegagalan.
Aleksandr Dubinsky