เหตุใดจึงไม่มี batching syscall ทั่วไปใน Linux / BSD


17

พื้นหลัง:

ค่าใช้จ่ายในการโทรของระบบมีขนาดใหญ่กว่าค่าใช้จ่ายในการเรียกใช้ฟังก์ชัน (ประมาณช่วงจาก 20-100x) ส่วนใหญ่เกิดจากการสลับบริบทจากพื้นที่ผู้ใช้เป็นพื้นที่เคอร์เนลและด้านหลัง มันเป็นเรื่องธรรมดาที่ฟังก์ชั่นแบบอินไลน์เพื่อประหยัดค่าใช้จ่ายการเรียกใช้ฟังก์ชั่นและการเรียกใช้ฟังก์ชั่นมีราคาถูกกว่า syscalls มาก เหตุผลที่ผู้พัฒนาต้องการหลีกเลี่ยงบางส่วนของการเรียกใช้ระบบโดยดูแลการดำเนินการในเคอร์เนลให้มากที่สุดใน syscall เดียวเท่าที่จะทำได้

ปัญหา:

นี้ได้สร้างจำนวนมาก (ฟุ่มเฟือย?) สายระบบเช่นsendmmsg () , recvmmsg ()เช่นเดียวกับ chdir เปิด lseek และ / หรือการรวมกัน symlink ที่ชอบ: 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()ซึ่งเห็นได้ชัดว่ารวม lseek อ่านและเขียน syscalls มันเป็นเพียงเรื่องของเวลาก่อนที่จะกลายเป็น fcopy_file_range (), lcopy_file_range (), copy_file_rangeat (), fcopy_file_rangeat () และ lcopy_file_rangeat () ... แต่เนื่องจากมี 2 ไฟล์ที่เกี่ยวข้องกับการโทรมากกว่า X อีกสาย มากกว่า. ตกลง Linus และนักพัฒนา BSD หลายคนจะไม่ปล่อยให้มันไปไกลขนาดนั้น แต่ประเด็นของฉันคือถ้ามี syscall แบบแบตช์สิ่งเหล่านี้ทั้งหมด (ส่วนใหญ่?) สามารถนำไปใช้ในพื้นที่ผู้ใช้และลดความซับซ้อนของเคอร์เนล หากค่าใช้จ่ายใด ๆ ในด้าน libc

มีการเสนอวิธีแก้ปัญหาที่ซับซ้อนหลายอย่างซึ่งรวมถึงเธรด syscall แบบพิเศษบางรูปแบบสำหรับ syscalls ที่ไม่ปิดกั้นไปยัง syscalls กระบวนการแบบแบตช์ อย่างไรก็ตามวิธีการเหล่านี้เพิ่มความซับซ้อนที่สำคัญให้กับทั้งเคอร์เนลและพื้นที่ผู้ใช้ในลักษณะเดียวกับ libxcb กับ libX11 (การเรียกแบบอะซิงโครนัสต้องการการตั้งค่าที่มากขึ้น)

วิธีการแก้?:

syscall การแบตช์ทั่วไป สิ่งนี้จะช่วยลดค่าใช้จ่ายที่ใหญ่ที่สุด (สวิตช์หลายโหมด) โดยไม่มีความซับซ้อนที่เกี่ยวข้องกับการมีเคอร์เนลเธรดเฉพาะ (แม้ว่าการทำงานนั้นสามารถเพิ่มในภายหลัง)

โดยพื้นฐานแล้วมีพื้นฐานที่ดีสำหรับต้นแบบใน socketcall () syscall เพียงแค่ขยายจากการใช้อาเรย์ของอาร์กิวเมนต์เพื่อแทนที่อาเรย์ของการคืนค่าตัวชี้ไปยังอาร์เรย์ของอาร์กิวเมนต์ (ซึ่งรวมถึงหมายเลข syscall) จำนวน syscalls และอาร์กิวเมนต์ค่าสถานะ ... สิ่งที่ต้องการ:

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

หนึ่งความแตกต่างที่สำคัญจะเป็นที่ขัดแย้งอาจจะทั้งหมดจะต้องมีตัวชี้สำหรับความเรียบง่ายเพื่อให้ผลของการ syscalls ก่อนที่จะนำมาใช้โดย syscalls ที่ตามมา (เช่นอธิบายไฟล์จากopen()การใช้งานในread()/ write())

ข้อดีบางประการที่เป็นไปได้:

  • พื้นที่ผู้ใช้น้อยกว่า -> พื้นที่เคอร์เนล -> การสลับพื้นที่ผู้ใช้
  • คอมไพเลอร์สวิตช์ที่เป็นไปได้ -fcombine-syscalls เพื่อพยายามแบตช์โดยอัตโนมัติ
  • ตั้งค่าสถานะเสริมสำหรับการทำงานแบบอะซิงโครนัส (คืน fd เพื่อดูทันที)
  • ความสามารถในการใช้ฟังก์ชั่น syscall รวมในอนาคตใน userspace

คำถาม:

เป็นไปได้หรือไม่ที่จะติดตั้ง syscall แบบแบตช์?

  • ฉันขาด gotchas บางอย่างชัดเจนหรือไม่
  • ฉันประเมินค่าผลประโยชน์มากเกินไปหรือไม่

มันคุ้มค่าหรือไม่ที่ฉันจะต้องติดตั้ง syscall แบบแบตช์ (ฉันไม่ได้ทำงานที่ Intel, Google หรือ Redhat)

  • ฉันเคยแพ็ตช์เคอร์เนลของตัวเองมาก่อน แต่กลัวการจัดการกับ LKML
  • ประวัติแสดงให้เห็นว่าแม้ว่าบางสิ่งจะมีประโยชน์อย่างกว้างขวางสำหรับผู้ใช้ "ปกติ" (ผู้ใช้ที่ไม่ใช่องค์กรที่ไม่มีการเข้าถึงการเขียนแบบคอมไพล์) แต่ก็อาจไม่เคยได้รับการยอมรับในขั้นต้น (unionfs, aufs, cryptodev, tuxonice ฯลฯ ... )

อ้างอิง:


4
ปัญหาที่เห็นได้ชัดอย่างหนึ่งที่ฉันเห็นคือเคอร์เนลให้การควบคุมเกี่ยวกับเวลาและพื้นที่ที่จำเป็นสำหรับ syscall รวมถึงความซับซ้อนของการดำเนินงานของ syscall เดียว โดยทั่วไปคุณได้สร้าง syscall ที่สามารถจัดสรรจำนวนหน่วยความจำเคอร์เนลได้ตามอำเภอใจโดยไม่ จำกัด จำนวนรันโดยไม่ จำกัด จำนวนเวลาและสามารถซับซ้อนได้ตามอำเภอใจ ด้วยการซ้อนbatchsyscalls ลงในbatchsyscalls คุณสามารถสร้างทรีเรียกแบบลึกโดยพลการของ syscalls ได้เอง โดยทั่วไปคุณสามารถใส่แอปพลิเคชันทั้งหมดของคุณไว้ใน syscall เดียว
Jörg W Mittag

@ JörgWMittag - ฉันไม่แนะนำให้ทำงานแบบขนานดังนั้นจำนวนหน่วยความจำเคอร์เนลที่ใช้จะไม่เกิน syscall ที่หนักที่สุดในแบตช์และเวลาในเคอร์เนลยังคงถูก จำกัด โดยพารามิเตอร์ ncalls (ซึ่งอาจถูก จำกัด บางค่าโดยพลการ) ของคุณถูกต้องเกี่ยวกับชุด syscall ซ้อนกันเป็นเครื่องมือที่มีประสิทธิภาพอาจจะมากจนควรได้รับการ จำกัด (แม้ว่าฉันจะเห็นว่ามันมีประโยชน์ในสถานการณ์เซิร์ฟเวอร์ไฟล์คงที่ - โดยจงใจภูตลงในห่วงเคอร์เนลโดยใช้ตัวชี้ - โดยทั่วไป กำลังติดตั้งเซิร์ฟเวอร์ TUX เก่า)
technosaurus

1
Syscalls เกี่ยวข้องกับการเปลี่ยนแปลงสิทธิ์ แต่สิ่งนี้ไม่ได้มีลักษณะเหมือนเป็นการสลับบริบทเสมอไป en.wikipedia.org/wiki/…
Erik Eidt

1
อ่านเมื่อวานนี้ซึ่งให้แรงบันดาลใจและพื้นหลังเพิ่มเติม: matildah.github.io/posts/2016-01-30-unikernel-security.html
Tom

@ JörgWMittagไม่อนุญาตการซ้อนเพื่อป้องกันไม่ให้เคอร์เนลล้นล้น มิฉะนั้น syscall แต่ละคนจะเป็นอิสระหลังจากพวกเขาเหมือนปกติ ไม่ควรมีปัญหาเกี่ยวกับการใช้ทรัพยากรนี้ เคอร์เนล 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);

และดูเหมือนว่าจะใช้งานได้ - ฉันสามารถเขียน hello to fd 1 และ world to fd 2 ด้วย 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);
}

โดยทั่วไปฉันใช้:

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

ในฐานะต้นแบบ syscall สากลซึ่งดูเหมือนจะเป็นสิ่งที่ทำงานบน x86_64 ดังนั้น "syscall" super ของฉันคือ:

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

ก็จะส่งกลับจำนวน syscalls พยายาม ( ==Nargsถ้าSUPERSYSCALL__continue_on_failureธงถูกส่งผ่านไปมิฉะนั้น>0 && <=Nargs) และความล้มเหลวในการคัดลอกระหว่างพื้นที่เมล็ดและพื้นที่ของผู้ใช้จะส่งสัญญาณโดย segfaults -EFAULTแทนปกติ

สิ่งที่ฉันไม่รู้ก็คือวิธีนี้จะส่งต่อไปยังสถาปัตยกรรมอื่น ๆ แต่มันจะดีถ้ามีสิ่งนี้ในเคอร์เนล

หากเป็นไปได้สำหรับซุ้มประตูทั้งหมดฉันคิดว่าอาจมี wrapper userspace ที่จะให้ความปลอดภัยประเภทผ่านสหภาพและแมโครบางตัว (มันสามารถเลือกสมาชิกสหภาพตามชื่อ syscall และสหภาพทั้งหมดจะได้รับการแปลงเป็นความยาว 6 หรือสิ่งใดก็ตามที่เทียบเท่ากับความยาวของ 6 สถาปัตยกรรม)


1
ใช้หลักฐานที่ดีของแนวคิด แต่ผมต้องการที่จะเห็นอาร์เรย์ของตัวชี้ไปที่ยาวแทนเพียงอาร์เรย์ของนานเพื่อให้คุณสามารถทำสิ่งที่ต้องการเปิดการเขียนอย่างใกล้ชิดโดยใช้การกลับมาของopenในและwrite closeนั่นจะเพิ่มความซับซ้อนเล็กน้อยเนื่องจากรับ / put_user แต่อาจคุ้มค่า สำหรับความสะดวกในการพกพา IIRC สถาปัตยกรรมบางอย่างอาจปิดบัง syscall register สำหรับ args 5 และ 6 ถ้า 5 หรือ 6 arg syscall เป็นแบตช์ ... การเพิ่ม 2 args เพิ่มเติมสำหรับการใช้ในอนาคตจะแก้ไขได้และสามารถใช้ในอนาคตสำหรับพารามิเตอร์การโทรแบบอะซิงโครนัส ตั้งค่าสถานะ SUPERSYSCALL__async แล้ว
technosaurus

1
ความตั้งใจของฉันคือการเพิ่ม sys_memcpy ด้วย ผู้ใช้สามารถวางไว้ในระหว่าง sys_open และ sys_write เพื่อคัดลอก fd กลับไปยังอาร์กิวเมนต์แรกของ sys_write โดยไม่ต้องสลับโหมดกลับไปที่ userspace
PSkocik

3

สอง gotchas หลักที่นึกได้ทันทีคือ:

  • การจัดการข้อผิดพลาด: syscall แต่ละรายการอาจจบลงด้วยข้อผิดพลาดที่ต้องตรวจสอบและจัดการโดยรหัสพื้นที่ผู้ใช้ของคุณ ดังนั้นการเรียกชุดงานจะต้องเรียกใช้รหัสพื้นที่ผู้ใช้หลังจากการโทรแต่ละครั้งต่อไปดังนั้นประโยชน์ของการเรียกชุดพื้นที่เคอร์เนลจะถูกปฏิเสธ นอกจากนี้ API จะต้องมีความซับซ้อนมาก (ถ้าเป็นไปได้ในการออกแบบเลย) - ตัวอย่างเช่นคุณจะแสดงตรรกะเช่น "ถ้าการโทรสายที่สามล้มเหลวทำอะไรและข้ามสายที่สี่ แต่ทำต่อไปด้วยสายที่ห้า")?

  • การโทรแบบ "รวม" จำนวนมากที่ได้รับการนำไปใช้จริงให้ประโยชน์เพิ่มเติมนอกเหนือจากการไม่ต้องย้ายระหว่างผู้ใช้และพื้นที่เคอร์เนล ตัวอย่างเช่นพวกเขามักจะหลีกเลี่ยงการคัดลอกหน่วยความจำและการใช้บัฟเฟอร์ทั้งหมด (เช่นถ่ายโอนข้อมูลโดยตรงจากที่เดียวในบัฟเฟอร์หน้าไปยังอีกแทนที่การคัดลอกผ่านบัฟเฟอร์กลาง) แน่นอนว่าสิ่งนี้เหมาะสำหรับการโทรแบบผสมเท่านั้น (เช่นอ่านแล้วเขียน) ไม่ใช่สำหรับการโทรแบบรวมกลุ่มโดยพลการ


2
เรื่องการจัดการข้อผิดพลาด ฉันคิดเกี่ยวกับสิ่งนั้นและนั่นเป็นเหตุผลที่ฉันแนะนำอาร์กิวเมนต์ค่าสถานะ (BATCH_RET_ON_FIRST_ERR) ... syscall ที่ประสบความสำเร็จควรส่งคืน ncalls หากการโทรทั้งหมดเสร็จสมบูรณ์โดยไม่มีข้อผิดพลาดหรือประสบความสำเร็จครั้งสุดท้ายหากล้มเหลว สิ่งนี้จะช่วยให้คุณตรวจสอบข้อผิดพลาดและอาจลองอีกครั้งโดยเริ่มจากการโทรที่ไม่สำเร็จครั้งแรกเพียงเพิ่ม 2 พอยน์เตอร์และลดจำนวน ncalls ด้วยค่าส่งคืนหากทรัพยากรไม่ว่างหรือการโทรถูกขัดจังหวะ ... ส่วน switiching ที่ไม่ใช่บริบทอยู่นอกขอบเขตสำหรับสิ่งนี้ แต่เนื่องจาก Linux 4.2, splice () สามารถช่วยพวกนั้นได้เช่นกัน
technosaurus

2
เคอร์เนลสามารถปรับรายการโทรให้เหมาะสมโดยอัตโนมัติเพื่อรวมการดำเนินการต่างๆและกำจัดงานซ้ำซ้อน เคอร์เนลอาจจะทำงานได้ดีกว่านักพัฒนาส่วนใหญ่ที่ประหยัดได้มากด้วย API ที่ง่ายกว่า
Aleksandr Dubinsky

@technosaurus ไม่สามารถทำงานร่วมกับแนวคิดของ technosaurus ในการยกเว้นว่าการสื่อสารใดที่การทำงานล้มเหลว (เพราะลำดับของการดำเนินการได้รับการปรับให้เหมาะสม) นี่คือเหตุผลที่โดยปกติแล้วข้อยกเว้นจะไม่ได้รับการออกแบบมาเพื่อส่งคืนข้อมูลที่แม่นยำดังกล่าว (เช่นกันเนื่องจากรหัสนั้นเกิดความสับสนและเปราะบาง) โชคดีที่มันไม่ยากที่จะเขียนตัวจัดการข้อยกเว้นทั่วไปที่จัดการกับโหมดความล้มเหลวต่างๆ
Aleksandr Dubinsky
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.