kernel - Rewrite umtx_sleep() and umtx_wakeup()
authorMatthew Dillon <dillon@apollo.backplane.com>
Mon, 16 Oct 2017 07:28:11 +0000 (00:28 -0700)
committerMatthew Dillon <dillon@apollo.backplane.com>
Mon, 16 Oct 2017 18:30:24 +0000 (11:30 -0700)
* Rewrite umtx_sleep() and umtx_wakeup() to no longer use
  vm_fault_page_quick().  Calling the VM fault code incurs a huge
  overhead and creates massive contention when many threads are
  using these calls.

  The new code uses fuword(), translate to the physical address via
  PTmap, and has very low overhead and basically zero contention.

* Instead, impose a mandatory timeout for umtx_sleep() and cap it
  at 2 seconds (adjustable via sysctl kern.umtx_timeout_max, set
  in microseconds).  When the memory mapping underpinning a umtx
  changes, userland will not stall for more than 2 seconds.

* The common remapping case caused by fork() is handled by the kernel
  by immediately waking up all sleeping umtx_sleep() calls for the
  related process.

* Any other copy-on-write or remapping cases will stall no more
  than the maximum timeout (2 seconds).  This might include paging
  to/from swap, for example, which can remap the physical page
  underpinning the umtx.  This could also include user application
  snafus or weirdness.

* umtx_sleep() and umtx_wakeup() still translate the user virtual
  address to a physical address for the tsleep() and wakeup() operation.
  This is done via a fault-protected access to the PTmap (the page-table
  self-mapping).

sys/kern/kern_fork.c
sys/kern/kern_synch.c
sys/kern/kern_umtx.c
sys/platform/pc64/x86_64/pmap.c
sys/platform/pc64/x86_64/support.s
sys/sys/systm.h
sys/vm/pmap.h

index d6d5e5a..059e066 100644 (file)
@@ -110,6 +110,8 @@ RB_GENERATE2(lwp_rb_tree, lwp, u.lwp_rbnode, rb_lwp_compare, lwpid_t, lwp_tid);
  * When forking, memory underpinning umtx-supported mutexes may be set
  * COW causing the physical address to change.  We must wakeup any threads
  * blocked on the physical address to allow them to re-resolve their VM.
+ *
+ * (caller is holding p->p_token)
  */
 static void
 wake_umtx_threads(struct proc *p1)
@@ -332,7 +334,8 @@ fork1(struct lwp *lp1, int flags, struct proc **procp)
                }
 
                vm_fork(p1, 0, flags);
-               wake_umtx_threads(p1);
+               if ((flags & RFMEM) == 0)
+                       wake_umtx_threads(p1);
 
                /*
                 * Close all file descriptors.
@@ -655,7 +658,8 @@ fork1(struct lwp *lp1, int flags, struct proc **procp)
        PHOLD(p1);
 
        vm_fork(p1, p2, flags);
-       wake_umtx_threads(p1);
+       if ((flags & RFMEM) == 0)
+               wake_umtx_threads(p1);
 
        /*
         * Create the first lwp associated with the new proc.
index 4a0bb2a..5540be1 100644 (file)
@@ -368,6 +368,18 @@ _tsleep_interlock(globaldata_t gd, const volatile void *ident, int flags)
 
        crit_enter_quick(td);
        if (td->td_flags & TDF_TSLEEPQ) {
+               /*
+                * Shortcut if unchanged
+                */
+               if (td->td_wchan == ident &&
+                   td->td_wdomain == (flags & PDOMAIN_MASK)) {
+                       crit_exit_quick(td);
+                       return;
+               }
+
+               /*
+                * Remove current sleepq
+                */
                cid = LOOKUP(td->td_wchan);
                gid = TCHASHSHIFT(cid);
                qp = &gd->gd_tsleep_hash[gid];
@@ -607,11 +619,13 @@ tsleep(const volatile void *ident, int flags, const char *wmesg, int timo)
        }
 
        /*
-        * If the interlocked flag is set but our cpu bit in the slpqueue
-        * is no longer set, then a wakeup was processed inbetween the
-        * tsleep_interlock() (ours or the callers), and here.  This can
-        * occur under numerous circumstances including when we release the
-        * current process.
+        * For PINTERLOCKED operation, TDF_TSLEEPQ might not be set if
+        * a wakeup() was processed before the thread could go to sleep.
+        *
+        * If TDF_TSLEEPQ is set, make sure the ident matches the recorded
+        * ident.  If it does not then the thread slept inbetween the
+        * caller's initial tsleep_interlock() call and the caller's tsleep()
+        * call.
         *
         * Extreme loads can cause the sending of an IPI (e.g. wakeup()'s)
         * to process incoming IPIs, thus draining incoming wakeups.
@@ -619,6 +633,10 @@ tsleep(const volatile void *ident, int flags, const char *wmesg, int timo)
        if ((td->td_flags & TDF_TSLEEPQ) == 0) {
                logtsleep2(ilockfail, ident);
                goto resume;
+       } else if (td->td_wchan != ident ||
+                  td->td_wdomain != (flags & PDOMAIN_MASK)) {
+               logtsleep2(ilockfail, ident);
+               goto resume;
        }
 
        /*
@@ -745,6 +763,7 @@ resume:
        }
        logtsleep1(tsleep_end);
        crit_exit_quick(td);
+
        return (error);
 }
 
index ec3af2c..cdd1dff 100644 (file)
@@ -1,7 +1,6 @@
 /*
- * (MPSAFE)
- *
- * Copyright (c) 2003,2004,2010 The DragonFly Project.  All rights reserved.
+ * Copyright (c) 2003,2004,2010,2017 The DragonFly Project.
+ * All rights reserved.
  * 
  * This code is derived from software contributed to The DragonFly Project
  * by Matthew Dillon <dillon@backplane.com> and David Xu <davidxu@freebsd.org>
@@ -42,6 +41,7 @@
 
 #include <sys/param.h>
 #include <sys/systm.h>
+#include <sys/cdefs.h>
 #include <sys/kernel.h>
 #include <sys/sysproto.h>
 #include <sys/sysunion.h>
 #include <machine/vmm.h>
 
 /*
- * Improve umtx performance by polling for 4uS before going to sleep.
+ * Improve umtx performance by polling for 4000nS before going to sleep.
  * This can avoid many IPIs in typical pthreads mutex situations.
  */
 #ifdef _RDTSC_SUPPORTED_
-static int umtx_delay = 4000;
-SYSCTL_INT(_kern, OID_AUTO, umtx_delay, CTLFLAG_RW, &umtx_delay, 0, "");
+static int umtx_delay = 4000;          /* nS */
+SYSCTL_INT(_kern, OID_AUTO, umtx_delay, CTLFLAG_RW,
+          &umtx_delay, 0, "");
 #endif
+static int umtx_timeout_max = 2000000; /* microseconds */
+SYSCTL_INT(_kern, OID_AUTO, umtx_timeout_max, CTLFLAG_RW,
+          &umtx_timeout_max, 0, "");
 
 /*
  * If the contents of the userland-supplied pointer matches the specified
@@ -95,69 +99,49 @@ SYSCTL_INT(_kern, OID_AUTO, umtx_delay, CTLFLAG_RW, &umtx_delay, 0, "");
  * safely race against changes in *ptr as long as we are properly interlocked
  * against the umtx_wakeup() call.
  *
- * The VM page associated with the mutex is held in an attempt to keep
- * the mutex's physical address consistent, allowing umtx_sleep() and
- * umtx_wakeup() to use the physical address as their rendezvous.  BUT
- * situations can arise where the physical address may change, particularly
- * if a threaded program fork()'s and the mutex's memory becomes
- * copy-on-write.  We register an event on the VM page to catch COWs.
+ * For performance reasons, we do not try to track the underlying page for
+ * mapping changes.  Instead, the timeout is capped at kern.umtx_timeout_max
+ * (default 1 second) and the caller is expected to retry.  The kernel
+ * will wake all umtx_sleep()s if the process fork()s, but not if it vfork()s.
+ * Other mapping changes must be caught by the timeout.
  *
  * umtx_sleep { const int *ptr, int value, int timeout }
  */
 int
 sys_umtx_sleep(struct umtx_sleep_args *uap)
 {
-    struct lwbuf lwb_cache;
-    struct lwbuf *lwb;
-    vm_page_t m;
     void *waddr;
+    void *uptr;
     int offset;
     int timeout;
     int error;
+    int value;
 
     if (uap->timeout < 0)
        return (EINVAL);
 
     if (curthread->td_vmm) {
        register_t gpa;
-       vmm_vm_get_gpa(curproc, &gpa, (register_t) uap->ptr);
+       vmm_vm_get_gpa(curproc, &gpa, (register_t)uap->ptr);
        uap->ptr = (const int *)gpa;
     }
 
-    if ((vm_offset_t)uap->ptr & (sizeof(int) - 1))
-       return (EFAULT);
+    uptr = __DEQUALIFY(void *, uap->ptr);
+    if ((vm_offset_t)uptr & (sizeof(int) - 1))
+       return EFAULT;
+
+    offset = (vm_offset_t)uptr & PAGE_MASK;
 
     /*
-     * When faulting in the page, force any COW pages to be resolved.
-     * Otherwise the physical page we sleep on my not match the page
-     * being woken up.
-     *
-     * The returned page is held, and this hold count prevents it from
-     * being paged out.  This is important since we are sleeping on what
-     * is essentially a physical address (which avoids all sorts of
-     * collision-space issues).
-     *
-     * WARNING! We can only use vm_fault_page*() for reading data.  We
-     *         cannot use it for writing data because there is no pmap
-     *         interlock to protect against flushes/pageouts.
-     *
-     *         (XXX with recent code work, this may no longer be an issue)
-     *
-     * WARNING! If the user program replaces the mapping of the underlying
-     *         uap->ptr, the physical address may change and the umtx code
-     *         will not be able to match wakeups with tsleeps.
+     * Initial quick check.  If -1 is returned distinguish between
+     * EBUSY and EINVAL.
      */
-    m = vm_fault_page_quick((vm_offset_t)uap->ptr,
-                           VM_PROT_READ | VM_PROT_WRITE, &error, NULL);
-    if (m == NULL) {
-       error = EFAULT;
-       goto done;
-    }
-    lwb = lwbuf_alloc(m, &lwb_cache);
-    offset = (vm_offset_t)uap->ptr & PAGE_MASK;
+    value = fuword32(uptr);
+    if (value == -1 && uservtophys((intptr_t)uap->ptr) == (vm_paddr_t)-1)
+       return EINVAL;
 
     error = EBUSY;
-    if (*(int *)(lwbuf_kva(lwb) + offset) == uap->value) {
+    if (value == uap->value) {
 #ifdef _RDTSC_SUPPORTED_
        /*
         * Poll a little while before sleeping, most mutexes are
@@ -170,7 +154,7 @@ sys_umtx_sleep(struct umtx_sleep_args *uap)
                tsc_target = tsc_get_target(umtx_delay);
                while (tsc_test_target(tsc_target) == 0) {
                        cpu_lfence();
-                       if (*(int *)(lwbuf_kva(lwb) + offset) != uap->value) {
+                       if (fuword32(uptr) != uap->value) {
                                good = 1;
                                break;
                        }
@@ -178,24 +162,30 @@ sys_umtx_sleep(struct umtx_sleep_args *uap)
                }
                if (good) {
                        error = EBUSY;
-                       goto skip;
+                       goto done;
                }
        }
 #endif
        /*
         * Calculate the timeout.  This will be acccurate to within ~2 ticks.
+        * uap->timeout is in microseconds.
         */
-       if ((timeout = uap->timeout) != 0) {
-           timeout = (timeout / 1000000) * hz +
-                     ((timeout % 1000000) * hz + 999999) / 1000000;
-       }
+       timeout = umtx_timeout_max;
+       if (uap->timeout && uap->timeout < timeout)
+               timeout = uap->timeout;
+       timeout = (timeout / 1000000) * hz +
+                 ((timeout % 1000000) * hz + 999999) / 1000000;
 
        /*
         * Calculate the physical address of the mutex.  This gives us
         * good distribution between unrelated processes using the
         * feature.
         */
-       waddr = (void *)((intptr_t)VM_PAGE_TO_PHYS(m) + offset);
+       waddr = (void *)uservtophys((intptr_t)uap->ptr);
+       if (waddr == (void *)(intptr_t)-1) {
+           error = EINVAL;
+           goto done;
+       }
 
        /*
         * Wake us up if the memory location COWs while we are sleeping.
@@ -215,11 +205,10 @@ sys_umtx_sleep(struct umtx_sleep_args *uap)
         */
        tsleep_interlock(waddr, PCATCH | PDOMAIN_UMTX);
        cpu_lfence();
-       if (*(int *)(lwbuf_kva(lwb) + offset) == uap->value) {
+       if (fuword32(uptr) == uap->value) {
                error = tsleep(waddr, PCATCH | PINTERLOCKED | PDOMAIN_UMTX,
                               "umtxsl", timeout);
        } else {
-               tsleep_remove(curthread);
                error = EBUSY;
        }
        crit_exit();
@@ -229,9 +218,6 @@ sys_umtx_sleep(struct umtx_sleep_args *uap)
     } else {
        error = EBUSY;
     }
-skip:
-    lwbuf_free(lwb);
-    vm_page_unhold(m);
 done:
     return(error);
 }
@@ -241,21 +227,17 @@ done:
  *
  * Wakeup the specified number of processes held in umtx_sleep() on the
  * specified user address.  A count of 0 wakes up all waiting processes.
- *
- * XXX assumes that the physical address space does not exceed the virtual
- * address space.
  */
 int
 sys_umtx_wakeup(struct umtx_wakeup_args *uap)
 {
-    vm_page_t m;
     int offset;
     int error;
     void *waddr;
 
     if (curthread->td_vmm) {
        register_t gpa;
-       vmm_vm_get_gpa(curproc, &gpa, (register_t) uap->ptr);
+       vmm_vm_get_gpa(curproc, &gpa, (register_t)uap->ptr);
        uap->ptr = (const int *)gpa;
     }
 
@@ -266,28 +248,20 @@ sys_umtx_wakeup(struct umtx_wakeup_args *uap)
      */
     cpu_mfence();
     if ((vm_offset_t)uap->ptr & (sizeof(int) - 1))
-       return (EFAULT);
-    m = vm_fault_page_quick((vm_offset_t)uap->ptr,
-                           VM_PROT_READ | VM_PROT_WRITE, &error, NULL);
-    if (m == NULL) {
-       error = EFAULT;
-       goto done;
-    }
+       return EFAULT;
+
     offset = (vm_offset_t)uap->ptr & PAGE_MASK;
-    waddr = (void *)((intptr_t)VM_PAGE_TO_PHYS(m) + offset);
+    waddr = (void *)uservtophys((intptr_t)uap->ptr);
+    if (waddr == (void *)(intptr_t)-1)
+       return EINVAL;
 
-#if 1
     if (uap->count == 1) {
        wakeup_domain_one(waddr, PDOMAIN_UMTX);
     } else {
        /* XXX wakes them all up for now */
        wakeup_domain(waddr, PDOMAIN_UMTX);
     }
-#else
-    wakeup_domain(waddr, PDOMAIN_UMTX);
-#endif
-    vm_page_unhold(m);
     error = 0;
-done:
+
     return(error);
 }
index 98cc6cb..a2f99fd 100644 (file)
@@ -701,6 +701,29 @@ vtopte(vm_offset_t va)
        return (PTmap + ((va >> PAGE_SHIFT) & mask));
 }
 
+/*
+ * Returns the physical address translation from va for a user address.
+ * (vm_paddr_t)-1 is returned on failure.
+ */
+vm_paddr_t
+uservtophys(vm_offset_t va)
+{
+       uint64_t mask = ((1ul << (NPTEPGSHIFT + NPDEPGSHIFT +
+                                 NPDPEPGSHIFT + NPML4EPGSHIFT)) - 1);
+       vm_paddr_t pa;
+       pt_entry_t pte;
+       pmap_t pmap;
+
+       pmap = vmspace_pmap(mycpu->gd_curthread->td_lwp->lwp_vmspace);
+       pa = (vm_paddr_t)-1;
+       if (va < VM_MAX_USER_ADDRESS) {
+               pte = kreadmem64(PTmap + ((va >> PAGE_SHIFT) & mask));
+               if (pte & pmap->pmap_bits[PG_V_IDX])
+                       pa = (pte & PG_FRAME) | (va & PAGE_MASK);
+       }
+       return pa;
+}
+
 static uint64_t
 allocpages(vm_paddr_t *firstaddr, long n)
 {
index a0df11e..cbb0bac 100644 (file)
@@ -202,6 +202,30 @@ END(fillw)
  * returns to *curpcb->onfault instead of the function.
  */
 
+/*
+ * uint64_t:%rax kreadmem64(addr:%rdi)
+ *
+ * Read kernel or user memory with fault protection.
+ */
+ENTRY(kreadmem64)
+       movq    PCPU(curthread),%rcx
+       movq    TD_PCB(%rcx), %rcx
+       movq    $kreadmem64fault,PCB_ONFAULT(%rcx)
+       movq    %rsp,PCB_ONFAULT_SP(%rcx)
+
+       movq    (%rdi),%rax
+       movq    $0,PCB_ONFAULT(%rcx)
+       ret
+
+kreadmem64fault:
+       movq    PCPU(curthread),%rcx
+       xorl    %eax,%eax
+       movq    TD_PCB(%rcx),%rcx
+       movq    %rax,PCB_ONFAULT(%rcx)
+       decq    %rax
+       ret
+END(kreadmem64)
+
 /*
  * std_copyout(from_kernel, to_user, len)  - MP SAFE
  *         %rdi,        %rsi,    %rdx
index 27ffcb8..c06bb5f 100644 (file)
@@ -249,6 +249,8 @@ void        bzeront(volatile void *buf, size_t len) __nonnull(1);
 void   *memcpy(void *to, const void *from, size_t len)
            __nonnull(1) __nonnull(2);
 
+long   kreadmem64(const void *addr);
+
 int    copystr (const void *kfaddr, void *kdaddr, size_t len,
                size_t *lencopied) __nonnull(1) __nonnull(2);
 int    copyinstr (const void *udaddr, void *kaddr, size_t len,
index 34c51c9..6310781 100644 (file)
@@ -191,6 +191,7 @@ void                 pmap_object_init_pt (pmap_t pmap, vm_offset_t addr,
 boolean_t       pmap_page_exists_quick (pmap_t pmap, struct vm_page *m);
 void            pmap_page_protect (struct vm_page *m, vm_prot_t prot);
 void            pmap_page_init (struct vm_page *m);
+vm_paddr_t      uservtophys(vm_offset_t va);
 vm_paddr_t      pmap_phys_address (vm_pindex_t);
 void            pmap_pinit (pmap_t);
 void            pmap_puninit (pmap_t);