From: Peter Oskolkov <hidden> Date: 2021-11-22 21:13:39
User Managed Concurrency Groups (UMCG) is an M:N threading
subsystem/toolkit that lets user space application developers implement
in-process user space schedulers.
This v0.9.1 patchset is the same as v0.9, where u32/u64 in
uapi/linux/umcg.h are replaced with __u32/__u64, as test robot/lkp
does not recognize u32/u64 for some reason.
v0.9 is v0.8 rebased on top of the current tip/sched/core,
with a fix in umcg_update_state of an issue reported by Tao Zhou.
Key changes from patchset v0.7:
https://lore.kernel.org/all/20211012232522.714898-1-posk@google.com/:
- added libumcg tools/lib/umcg;
- worker "wakeup" is reworked so that it is now purely a userspace op,
instead of waking the thread in order for it to block on return
to the userspace immediately;
- a couple of minor fixes and refactorings.
These big things remain to be addressed (in no particular order):
- support tracing/debugging
- make context switches faster (see umcg_do_context_switch in umcg.c)
- support other architectures
- cleanup and post selftests in tools/testing/selftests/umcg/
- allow cross-mm wakeups (securely)
Peter Oskolkov (6):
sched/umcg: add WF_CURRENT_CPU and externise ttwu
mm, x86/uaccess: add userspace atomic helpers
sched/umcg: implement UMCG syscalls
sched/umcg, lib/umcg: implement libumcg
sched/umcg: add Documentation/userspace-api/umcg.txt
sched/umcg, lib/umcg: add tools/lib/umcg/libumcg.txt
Documentation/userspace-api/umcg.txt | 598 ++++++++++++
arch/x86/entry/syscalls/syscall_64.tbl | 2 +
arch/x86/include/asm/uaccess_64.h | 93 ++
fs/exec.c | 1 +
include/linux/sched.h | 71 ++
include/linux/syscalls.h | 3 +
include/linux/uaccess.h | 46 +
include/uapi/asm-generic/unistd.h | 7 +-
include/uapi/linux/umcg.h | 137 +++
init/Kconfig | 10 +
kernel/entry/common.c | 4 +-
kernel/exit.c | 5 +
kernel/sched/Makefile | 1 +
kernel/sched/core.c | 12 +-
kernel/sched/fair.c | 4 +
kernel/sched/sched.h | 15 +-
kernel/sched/umcg.c | 949 +++++++++++++++++++
kernel/sys_ni.c | 4 +
mm/maccess.c | 264 ++++++
tools/lib/umcg/.gitignore | 4 +
tools/lib/umcg/Makefile | 11 +
tools/lib/umcg/libumcg.c | 1202 ++++++++++++++++++++++++
tools/lib/umcg/libumcg.h | 299 ++++++
tools/lib/umcg/libumcg.txt | 438 +++++++++
24 files changed, 4168 insertions(+), 12 deletions(-)
create mode 100644 Documentation/userspace-api/umcg.txt
create mode 100644 include/uapi/linux/umcg.h
create mode 100644 kernel/sched/umcg.c
create mode 100644 tools/lib/umcg/.gitignore
create mode 100644 tools/lib/umcg/Makefile
create mode 100644 tools/lib/umcg/libumcg.c
create mode 100644 tools/lib/umcg/libumcg.h
create mode 100644 tools/lib/umcg/libumcg.txt
base-commit: cb0e52b7748737b2cf6481fdd9b920ce7e1ebbdf
--
2.25.1
From: Peter Oskolkov <hidden> Date: 2021-11-22 21:13:45
Add WF_CURRENT_CPU wake flag that advices the scheduler to
move the wakee to the current CPU. This is useful for fast on-CPU
context switching use cases such as UMCG.
In addition, make ttwu external rather than static so that
the flag could be passed to it from outside of sched/core.c.
Signed-off-by: Peter Oskolkov <redacted>
---
kernel/sched/core.c | 3 +--
kernel/sched/fair.c | 4 ++++
kernel/sched/sched.h | 15 +++++++++------
3 files changed, 14 insertions(+), 8 deletions(-)
@@ -2052,13 +2052,14 @@ static inline int task_on_rq_migrating(struct task_struct *p)}/* Wake flags. The first three directly map to some SD flag value */-#define WF_EXEC 0x02 /* Wakeup after exec; maps to SD_BALANCE_EXEC */-#define WF_FORK 0x04 /* Wakeup after fork; maps to SD_BALANCE_FORK */-#define WF_TTWU 0x08 /* Wakeup; maps to SD_BALANCE_WAKE */+#define WF_EXEC 0x02 /* Wakeup after exec; maps to SD_BALANCE_EXEC */+#define WF_FORK 0x04 /* Wakeup after fork; maps to SD_BALANCE_FORK */+#define WF_TTWU 0x08 /* Wakeup; maps to SD_BALANCE_WAKE */-#define WF_SYNC 0x10 /* Waker goes to sleep after wakeup */-#define WF_MIGRATED 0x20 /* Internal use, task got migrated */-#define WF_ON_CPU 0x40 /* Wakee is on_cpu */+#define WF_SYNC 0x10 /* Waker goes to sleep after wakeup */+#define WF_MIGRATED 0x20 /* Internal use, task got migrated */+#define WF_ON_CPU 0x40 /* Wakee is on_cpu */+#define WF_CURRENT_CPU 0x80 /* Prefer to move the wakee to the current CPU. */#ifdef CONFIG_SMPstatic_assert(WF_EXEC==SD_BALANCE_EXEC);
From: Peter Oskolkov <hidden> Date: 2021-11-22 21:13:52
In addition to futexes needing to do atomic operations in the userspace,
a second use case is now in the works (UMCG, see
https://lore.kernel.org/all/20210917180323.278250-1-posk@google.com/),
so a generic facility to perform these operations has been called for
(see https://lore.kernel.org/all/87ilyk9xc0.ffs@tglx/).
Add a set of generic helpers to perform 32/64-bit xchg and cmpxchg
operations in the userspace. Also implement the required
architecture-specific support on x86_64.
Signed-off-by: Peter Oskolkov <redacted>
---
arch/x86/include/asm/uaccess_64.h | 93 +++++++++++
include/linux/uaccess.h | 46 ++++++
mm/maccess.c | 264 ++++++++++++++++++++++++++++++
3 files changed, 403 insertions(+)
From: Peter Oskolkov <hidden> Date: 2021-11-22 21:13:55
Define struct umcg_task and two syscalls: sys_umcg_ctl sys_umcg_wait.
User Managed Concurrency Groups is an M:N threading toolkit that allows
constructing user space schedulers designed to efficiently manage
heterogeneous in-process workloads while maintaining high CPU
utilization (95%+).
In addition, M:N threading and cooperative user space scheduling
enables synchronous coding style and better cache locality when
compared to asynchronous callback/continuation style of programming.
UMCG kernel API is build around the following ideas:
* UMCG server: a task/thread representing "kernel threads", or (v)CPUs;
* UMCG worker: a task/thread representing "application threads", to be
scheduled over servers;
* UMCG task state: (NONE), RUNNING, BLOCKED, IDLE: states a UMCG task (a
server or a worker) can be in;
* UMCG task state flag: LOCKED, PREEMPTED: additional state flags that
can be ORed with the task state to communicate additional information to
the kernel;
* struct umcg_task: a per-task userspace set of data fields, usually
residing in the TLS, that fully reflects the current task's UMCG state
and controls the way the kernel manages the task;
* sys_umcg_ctl(): a syscall used to register the current task/thread as a
server or a worker, or to unregister a UMCG task;
* sys_umcg_wait(): a syscall used to put the current task to sleep and/or
wake another task, pontentially context-switching between the two tasks
on-CPU synchronously.
In short, servers can be thought of as CPUs over which application
threads (workers) are scheduled; at any one time a worker is either:
- RUNNING: has a server and is schedulable by the kernel;
- BLOCKED: blocked in the kernel (e.g. on I/O, or a futex);
- IDLE: is not blocked, but cannot be scheduled by the kernel to
run because it has no server assigned to it (e.g. because all
available servers are busy "running" other workers).
Usually the number of servers in a process is equal to the number of
CPUs available to the kernel if the process is supposed to consume
the whole machine, or less than the number of CPUs available if the
process is sharing the machine with other workloads. The number of
workers in a process can grow very large: tens of thousands is normal;
hundreds of thousands and more (millions) is something that would
be desirable to achieve in the future, as lightweight userspace
threads in Java and Go easily scale to millions, and UMCG workers
are (intended to be) conceptually similar to those.
Detailed use cases and API behavior are provided in
Documentation/userspace-api/umcg.txt (see sibling patches).
Some high-level implementation notes:
UMCG tasks (workers and servers) are "tagged" with struct umcg_task
residing in userspace (usually in TLS) to facilitate kernel/userspace
communication. This makes the kernel-side code much simpler (see e.g.
the implementation of sys_umcg_wait), but also requires some careful
uaccess handling and page pinning (see below).
The main UMCG server/worker interaction looks like:
a. worker W1 is RUNNING, with a server S attached to it sleeping
in IDLE state;
b. worker W1 blocks in the kernel, e.g. on I/O;
c. the kernel marks W1 as BLOCKED, the attached server S
as RUNNING, and wakes S (the "block detection" event);
d. the server now picks another IDLE worker W2 to run: marks
W2 as RUNNING, itself as IDLE, ands calls sys_umcg_wait();
e. when the blocking operation of W1 completes, the worker
is marked by the kernel as IDLE and added to idle workers list
(see struct umcg_task) for the userspace to pick up and
later run (the "wake detection" event).
While there are additional operations such as worker-to-worker
context switch, preemption, workers "yielding", etc., the "workflow"
above is the main worker/server interaction that drives the
implementation.
Specifically:
- most operations are conceptually context switches:
- scheduling a worker: a running server goes to sleep and "runs"
a worker in its place;
- block detection: worker is descheduled, and its server is woken;
- wake detection: woken worker, running in the kernel, is descheduled,
and if there is an idle server, it is woken to process the wake
detection event;
- to faciliate low scheduling latencies and cache locality, most
server/worker interactions described above are performed synchronously
"on CPU" via WF_CURRENT_CPU flag passed to ttwu; while at the moment
the context switches are simulated by putting the switch-out task to
sleep and waking the switch-into task on the same cpu, it is very much
the long-term goal of this project to make the context switch much
lighter, by tweaking runtime accounting and, maybe, even bypassing
__schedule();
- worker blocking is detected in a hook to sched_submit_work; as mentioned
above, the server is to be woken on the same CPU, synchronously;
this code may not pagefault, so to access worker's and server's
userspace memory (struct umcg_task), memory pages containing the worker's
and the server's structs umcg_task are pinned when the worker is
exiting to the userspace, and unpinned when the worker is descheduled;
- worker wakeup is detected in a hook to sched_update_worker, and processed
in the exit to usermode loop (via TIF_NOTIFY_RESUME); workers CAN
pagefault on the wakeup path;
- worker preemption is implemented by the userspace tagging the worker
with UMCG_TF_PREEMPTED state flag and sending a NOOP signal to it;
on the exit to usermode the worker is intercepted and its server is woken
(see Documentation/userspace-api/umcg.txt for more details);
- each state change is tagged with a unique timestamp (of MONOTONIC
variety), so that
- scheduling instrumentation is naturally available;
- racing state changes are easily detected and ABA issues are
avoided;
see umcg_update_state() in umcg.c for implementation details, and
Documentation/userspace-api/umcg.txt for a higher-level
description.
The previous version of the patchset can be found at
https://lore.kernel.org/all/20211012232522.714898-1-posk@google.com/
containing some additional context and links to earlier discussions.
More details are available in Documentation/userspace-api/umcg.txt
in sibling patches, and in doc-comments in the code.
Signed-off-by: Peter Oskolkov <redacted>
---
arch/x86/entry/syscalls/syscall_64.tbl | 2 +
fs/exec.c | 1 +
include/linux/sched.h | 71 ++
include/linux/syscalls.h | 3 +
include/uapi/asm-generic/unistd.h | 7 +-
include/uapi/linux/umcg.h | 137 ++++
init/Kconfig | 10 +
kernel/entry/common.c | 4 +-
kernel/exit.c | 5 +
kernel/sched/Makefile | 1 +
kernel/sched/core.c | 9 +-
kernel/sched/umcg.c | 949 +++++++++++++++++++++++++
kernel/sys_ni.c | 4 +
13 files changed, 1199 insertions(+), 4 deletions(-)
create mode 100644 include/uapi/linux/umcg.h
create mode 100644 kernel/sched/umcg.c
@@ -371,6 +371,8 @@ 447 common memfd_secret sys_memfd_secret 448 common process_mrelease sys_process_mrelease 449 common futex_waitv sys_futex_waitv+450 common umcg_ctl sys_umcg_ctl+451 common umcg_wait sys_umcg_wait # # Due to a historical design error, certain syscalls are numbered differently
@@ -1687,6 +1694,13 @@ extern struct pid *cad_pid;#define PF_KTHREAD 0x00200000 /* I am a kernel thread */#define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */++#ifdef CONFIG_UMCG+#define PF_UMCG_WORKER 0x01000000 /* UMCG worker */+#else+#define PF_UMCG_WORKER 0x00000000+#endif+#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_mask */#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */#define PF_MEMALLOC_PIN 0x10000000 /* Allocation context constrained to zones which allow long term pinning. */
@@ -2287,6 +2301,63 @@ static inline void rseq_execve(struct task_struct *t)#endif+#ifdef CONFIG_UMCG++voidumcg_handle_resuming_worker(void);+voidumcg_handle_exiting_worker(void);+voidumcg_clear_child(structtask_struct*tsk);++/* Called by bprm_execve() in fs/exec.c. */+staticinlinevoidumcg_execve(structtask_struct*tsk)+{+if(tsk->umcg_task)+umcg_clear_child(tsk);+}++/* Called by exit_to_user_mode_loop() in kernel/entry/common.c.*/+staticinlinevoidumcg_handle_notify_resume(void)+{+if(current->flags&PF_UMCG_WORKER)+umcg_handle_resuming_worker();+}++/* Called by do_exit() in kernel/exit.c. */+staticinlinevoidumcg_handle_exit(void)+{+if(current->flags&PF_UMCG_WORKER)+umcg_handle_exiting_worker();+}++/*+*umcg_wq_worker_[sleeping|running]arecalledincore.cby+*sched_submit_work()andsched_update_worker().+*/+voidumcg_wq_worker_sleeping(structtask_struct*tsk);+voidumcg_wq_worker_running(structtask_struct*tsk);++#else /* CONFIG_UMCG */++staticinlinevoidumcg_clear_child(structtask_struct*tsk)+{+}+staticinlinevoidumcg_execve(structtask_struct*tsk)+{+}+staticinlinevoidumcg_handle_notify_resume(void)+{+}+staticinlinevoidumcg_handle_exit(void)+{+}+staticinlinevoidumcg_wq_worker_sleeping(structtask_struct*tsk)+{+}+staticinlinevoidumcg_wq_worker_running(structtask_struct*tsk)+{+}++#endif+#ifdef CONFIG_DEBUG_RSEQvoidrseq_syscall(structpt_regs*regs);
@@ -0,0 +1,137 @@+/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */+#ifndef _UAPI_LINUX_UMCG_H+#define _UAPI_LINUX_UMCG_H++#include<linux/limits.h>+#include<linux/types.h>++/*+*UMCG:UserManagedConcurrencyGroups.+*+*Syscalls(seekernel/sched/umcg.c):+*sys_umcg_ctl()-register/unregisterUMCGtasks;+*sys_umcg_wait()-wait/wake/context-switch.+*+*structumcg_task(below):controlsthestateofUMCGtasks.+*+*SeeDocumentation/userspace-api/umcg.txtfordetals.+*/++/*+*UMCGtaskstates,thefirst6bitsofstructumcg_task.state_ts.+*Thestatesrepresenttheuserspacepointofview.+*/+#define UMCG_TASK_NONE 0ULL+#define UMCG_TASK_RUNNING 1ULL+#define UMCG_TASK_IDLE 2ULL+#define UMCG_TASK_BLOCKED 3ULL++/* UMCG task state flags, bits 7-8 */++/*+*UMCG_TF_LOCKED:lockedbytheuserspaceinpreparationtocallingumcg_wait.+*/+#define UMCG_TF_LOCKED (1ULL << 6)++/*+*UMCG_TF_PREEMPTED:theuserspaceindicatestheworkershouldbepreempted.+*/+#define UMCG_TF_PREEMPTED (1ULL << 7)++/* The first six bits: RUNNING, IDLE, or BLOCKED. */+#define UMCG_TASK_STATE_MASK 0x3fULL++/* The full state mask: the first 18 bits. */+#define UMCG_TASK_STATE_MASK_FULL 0x3ffffULL++/*+*ThenumberofbitsreservedforUMCGstatetimestampin+*structumcg_task.state_ts.+*/+#define UMCG_STATE_TIMESTAMP_BITS 46++/* The number of bits truncated from UMCG state timestamp. */+#define UMCG_STATE_TIMESTAMP_GRANULARITY 4++/**+*structumcg_task-controlsthestateofUMCGtasks.+*+*Thestructisalignedat64bytestoensurethatitfitsinto+*asinglecacheline.+*/+structumcg_task{+/**+*@state_ts:thecurrentstateoftheUMCGtaskdescribedby+*thisstruct,withauniquetimestampindicating+*whenthelaststatechangehappened.+*+*Readable/writablebyboththekernelandtheuserspace.+*+*UMCGtaskstate:+*bits0-5:taskstate;+*bits6-7:stateflags;+*bits8-12:reserved;mustbezeroes;+*bits13-17:foruserspaceuse;+*bits18-63:timestamp(seebelow).+*+*Timestamp:a46-bitCLOCK_MONOTONICtimestamp,at16nsresolution.+*SeeDocumentation/userspace-api/umcg.txtfordetals.+*/+__u64state_ts;/* r/w */++/**+*@next_tid:theTIDoftheUMCGtaskthatshouldbecontext-switched+*intoinsys_umcg_wait().Canbezero.+*+*RunningUMCGworkersmusthavenext_tidsettopointtoIDLE+*UMCGservers.+*+*Read-onlyforthekernel,read/writefortheuserspace.+*/+__u32next_tid;/* r */++__u32flags;/* Reserved; must be zero. */++/**+*@idle_workers_ptr:asingle-linkedlistofidleworkers.CanbeNULL.+*+*Readable/writablebyboththekernelandtheuserspace:the+*kerneladdsitemstothelist,theuserspaceremovesthem.+*/+__u64idle_workers_ptr;/* r/w */++/**+*@idle_server_tid_ptr:apointerpointingtoasingleidleserver.+*Readonly.+*/+__u64idle_server_tid_ptr;/* r */+}__attribute__((packed,aligned(8*sizeof(__u64))));++/**+*enumumcg_ctl_flag-flagstopasstosys_umcg_ctl+*@UMCG_CTL_REGISTER:registerthecurrenttaskasaUMCGtask+*@UMCG_CTL_UNREGISTER:unregisterthecurrenttaskasaUMCGtask+*@UMCG_CTL_WORKER:registerthecurrenttaskasaUMCGworker+*/+enumumcg_ctl_flag{+UMCG_CTL_REGISTER=0x00001,+UMCG_CTL_UNREGISTER=0x00002,+UMCG_CTL_WORKER=0x10000,+};++/**+*enumumcg_wait_flag-flagstopasstosys_umcg_wait+*@UMCG_WAIT_WAKE_ONLY:wake@self->next_tid,don'tput@selftosleep;+*@UMCG_WAIT_WF_CURRENT_CPU:wake@self->next_tidonthecurrentCPU+*(useWF_CURRENT_CPU);@UMCG_WAIT_WAKE_ONLY+*mustbeset.+*/+enumumcg_wait_flag{+UMCG_WAIT_WAKE_ONLY=1,+UMCG_WAIT_WF_CURRENT_CPU=2,+};++/* See Documentation/userspace-api/umcg.txt.*/+#define UMCG_IDLE_NODE_PENDING (1ULL)++#endif /* _UAPI_LINUX_UMCG_H */
@@ -1693,6 +1693,16 @@ config MEMBARRIERIfunsure,sayY.+configUMCG+bool"Enable User Managed Concurrency Groups API"+depends onX86_64+defaultn+help+EnableUserManagedConcurrencyGroupsAPI,whichformthebasis+foranin-processM:Nuserspaceschedulingframework.+Atthemomentthisisanexperimental/RFCfeaturethatisnot+guaranteedtobebackward-compatible.+configKALLSYMSbool"Load all symbols for debugging/ksymoops"ifEXPERTdefaulty
@@ -171,8 +171,10 @@ static unsigned long exit_to_user_mode_loop(struct pt_regs *regs,if(ti_work&(_TIF_SIGPENDING|_TIF_NOTIFY_SIGNAL))handle_signal_work(regs,ti_work);-if(ti_work&_TIF_NOTIFY_RESUME)+if(ti_work&_TIF_NOTIFY_RESUME){+umcg_handle_notify_resume();tracehook_notify_resume(regs);+}/* Architecture specific TIF work */arch_exit_to_user_mode_work(regs,ti_work);
@@ -0,0 +1,949 @@+// SPDX-License-Identifier: GPL-2.0-only++/*+*UserManagedConcurrencyGroups(UMCG).+*+*SeeDocumentation/userspace-api/umcg.txtfordetals.+*/++#include<linux/syscalls.h>+#include<linux/types.h>+#include<linux/uaccess.h>+#include<linux/umcg.h>++#include"sched.h"++/**+*get_user_nofault-getuservaluewithoutsleeping.+*+*get_user()mightsleepandthereforecannotbeusedinpreempt-disabled+*regions.+*/+#define get_user_nofault(out, uaddr) \+({\+intret=-EFAULT;\+\+if(access_ok((uaddr),sizeof(*(uaddr)))){\+pagefault_disable();\+\+if(!__get_user((out),(uaddr)))\+ret=0;\+\+pagefault_enable();\+}\+ret;\+})++/**+*umcg_pin_pages:pinpagescontainingstructumcg_taskofthisworker+*anditsserver.+*+*Thepagesarepinnedwhentheworkerexitstotheuserspaceandunpinned+*whentheworkerisinsched_submit_work(),i.e.whentheworkeris+*abouttoberemovedfromitsrunqueue.ThusatmostNR_CPUSUMCGpages+*arepinnedatanyonetimeacrossthewholesystem.+*+*Thepinningisneededsothatgoing-to-sleepworkerscanaccess+*theirandtheirservers'userspaceumcg_taskstructswithoutpagefaults,+*asthecodepathcanbeexecutedinthecontextofapagefault,with+*mmlockheld.+*/+staticintumcg_pin_pages(u32server_tid)+{+structumcg_task__user*worker_ut=current->umcg_task;+structumcg_task__user*server_ut=NULL;+structtask_struct*tsk;++rcu_read_lock();+tsk=find_task_by_vpid(server_tid);+/* Server/worker interaction is allowed only within the same mm. */+if(tsk&¤t->mm==tsk->mm)+server_ut=READ_ONCE(tsk->umcg_task);+rcu_read_unlock();++if(!server_ut)+return-EINVAL;++tsk=current;++/* worker_ut is stable, don't need to repin */+if(!tsk->pinned_umcg_worker_page)+if(1!=pin_user_pages_fast((unsignedlong)worker_ut,1,0,+&tsk->pinned_umcg_worker_page))+return-EFAULT;++/* server_ut may change, need to repin */+if(tsk->pinned_umcg_server_page){+unpin_user_page(tsk->pinned_umcg_server_page);+tsk->pinned_umcg_server_page=NULL;+}++if(1!=pin_user_pages_fast((unsignedlong)server_ut,1,0,+&tsk->pinned_umcg_server_page))+return-EFAULT;++return0;+}++staticvoidumcg_unpin_pages(void)+{+structtask_struct*tsk=current;++if(tsk->pinned_umcg_worker_page)+unpin_user_page(tsk->pinned_umcg_worker_page);+if(tsk->pinned_umcg_server_page)+unpin_user_page(tsk->pinned_umcg_server_page);++tsk->pinned_umcg_worker_page=NULL;+tsk->pinned_umcg_server_page=NULL;+}++staticvoidumcg_clear_task(structtask_struct*tsk)+{+/*+*Thisiseithercalledforthecurrenttask,orforanewlyforked+*taskthatisnotyetrunning,sowedon'tneedstrictatomicity+*below.+*/+if(tsk->umcg_task){+WRITE_ONCE(tsk->umcg_task,NULL);++/* These can be simple writes - see the commment above. */+tsk->pinned_umcg_worker_page=NULL;+tsk->pinned_umcg_server_page=NULL;+tsk->flags&=~PF_UMCG_WORKER;+}+}++/* Called for a forked or execve-ed child. */+voidumcg_clear_child(structtask_struct*tsk)+{+umcg_clear_task(tsk);+}++/* Called both by normally (unregister) and abnormally exiting workers. */+voidumcg_handle_exiting_worker(void)+{+umcg_unpin_pages();+umcg_clear_task(current);+}++/**+*umcg_update_state:atomicallyupdateumcg_task.state_ts,setnewtimestamp.+*@state_ts-pointstothestate_tsmemberofstructumcg_tasktoupdate;+*@expected-theexpectedvalueofstate_ts,includingthetimestamp;+*@desired-thedesiredvalueofstate_ts,statepartonly;+*@may_fault-whethertousenormalor_nofaultcmpxchg.+*+*Thefunctionisbasicallycmpxchg(state_ts,expected,desired),withextra+*codetosetthetimestampin@desired.+*/+staticintumcg_update_state(u64__user*state_ts,u64*expected,u64desired,+boolmay_fault)+{+u64curr_ts=(*expected)>>(64-UMCG_STATE_TIMESTAMP_BITS);+u64next_ts=ktime_get_ns()>>UMCG_STATE_TIMESTAMP_GRANULARITY;++/* Cut higher order bits. */+next_ts&=(1ULL<<UMCG_STATE_TIMESTAMP_BITS)-1;++if(next_ts==curr_ts)+++next_ts;++/* Remove an old timestamp, if any. */+desired&=UMCG_TASK_STATE_MASK_FULL;++/* Set the new timestamp. */+desired|=(next_ts<<(64-UMCG_STATE_TIMESTAMP_BITS));++if(may_fault)+returncmpxchg_user_64(state_ts,expected,desired);++returncmpxchg_user_64_nofault(state_ts,expected,desired);+}++/**+*sys_umcg_ctl:(un)registerthecurrenttaskasaUMCGtask.+*@flags:ORedvaluesfromenumumcg_ctl_flag;seebelow;+*@self:apointertostructumcg_taskthatdescribesthis+*taskandgovernsthebehaviorofsys_umcg_waitif+*registering;mustbeNULLifunregistering.+*+*@flags&UMCG_CTL_REGISTER:registeraUMCGtask:+*UMCGworkers:+*-@flags&UMCG_CTL_WORKER+*-self->statemustbeUMCG_TASK_BLOCKED+*UMCGservers:+*-!(@flags&UMCG_CTL_WORKER)+*-self->statemustbeUMCG_TASK_RUNNING+*+*Alltasks:+*-self->next_tidmustbezero+*+*Iftheconditionsabovearemet,sys_umcg_ctl()immediatelyreturns+*iftheregisteredtaskisaserver;aworkerwillbeaddedto+*idle_workers_ptr,andtheworkerputtosleep;anidleserver+*fromidle_server_tid_ptrwillbewoken,ifpresent.+*+*@flags==UMCG_CTL_UNREGISTER:unregisteraUMCGtask.Ifthecurrenttask+*isaUMCGworker,theuserspaceisresponsibleforwakingits+*server(beforeoraftercallingsys_umcg_ctl).+*+*Return:+*0-success+*-EFAULT-failedtoread@self+*-EINVAL-someothererroroccurred+*/+SYSCALL_DEFINE2(umcg_ctl,u32,flags,structumcg_task__user*,self)+{+structumcg_taskut;++if(flags==UMCG_CTL_UNREGISTER){+if(self||!current->umcg_task)+return-EINVAL;++if(current->flags&PF_UMCG_WORKER)+umcg_handle_exiting_worker();+else+umcg_clear_task(current);++return0;+}++if(!(flags&UMCG_CTL_REGISTER))+return-EINVAL;++flags&=~UMCG_CTL_REGISTER;+if(flags&&flags!=UMCG_CTL_WORKER)+return-EINVAL;++if(current->umcg_task||!self)+return-EINVAL;++if(copy_from_user(&ut,self,sizeof(ut)))+return-EFAULT;++if(ut.next_tid)+return-EINVAL;++if(flags==UMCG_CTL_WORKER){+if((ut.state_ts&UMCG_TASK_STATE_MASK_FULL)!=UMCG_TASK_BLOCKED)+return-EINVAL;++WRITE_ONCE(current->umcg_task,self);+current->flags|=PF_UMCG_WORKER;++/* Trigger umcg_handle_resuming_worker() */+set_tsk_thread_flag(current,TIF_NOTIFY_RESUME);+}else{+if((ut.state_ts&UMCG_TASK_STATE_MASK_FULL)!=UMCG_TASK_RUNNING)+return-EINVAL;++WRITE_ONCE(current->umcg_task,self);+}++return0;+}++/**+*handle_timedout_worker-makesuretheworkerisaddedtoidle_workers+*upona"clean"timeout.+*/+staticinthandle_timedout_worker(structumcg_task__user*self)+{+u64curr_state,next_state;+intret;++if(get_user(curr_state,&self->state_ts))+return-EFAULT;++if((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE){+/* TODO: should we care here about TF_LOCKED or TF_PREEMPTED? */++next_state=curr_state&~UMCG_TASK_STATE_MASK;+next_state|=UMCG_TASK_BLOCKED;++ret=umcg_update_state(&self->state_ts,&curr_state,next_state,true);+if(ret)+returnret;++return-ETIMEDOUT;+}++return0;/* Not really timed out. */+}++/*+*umcg_should_idle-returntrueiftaskswith@stateshouldblockin+*imcg_idle_loop().+*/+staticboolumcg_should_idle(u64state)+{+switch(state&UMCG_TASK_STATE_MASK){+caseUMCG_TASK_RUNNING:+returnstate&UMCG_TF_LOCKED;+caseUMCG_TASK_IDLE:+return!(state&UMCG_TF_LOCKED);+caseUMCG_TASK_BLOCKED:+returnfalse;+default:+WARN_ONCE(true,"unknown UMCG task state");+returnfalse;+}+}++/**+*umcg_idle_loop-sleepuntil!umcg_should_idle()oratimeoutexpires+*@abs_timeout-absolutetimeoutinnanoseconds;zero=>notimeout+*+*ThefunctionmarksthecurrenttaskasINTERRUPTIBLEandcalls+*freezable_schedule().+*+*Note:becauseUMCGworkersshouldnotberunningWITHOUTattachedservers,+*andbecauseserversshouldnotberunningWITHattachedworkers,+*thefunctionreturnsonlyonfatalsignalpendingandignores/flushes+*allothersignals.+*/+staticintumcg_idle_loop(u64abs_timeout)+{+intret;+structpage*pinned_page=NULL;+structhrtimer_sleepertimeout;+structumcg_task__user*self=current->umcg_task;+constboolworker=current->flags&PF_UMCG_WORKER;++/* Clear PF_UMCG_WORKER to elide workqueue handlers. */+if(worker)+current->flags&=~PF_UMCG_WORKER;++if(abs_timeout){+hrtimer_init_sleeper_on_stack(&timeout,CLOCK_REALTIME,+HRTIMER_MODE_ABS);++hrtimer_set_expires_range_ns(&timeout.timer,(s64)abs_timeout,+current->timer_slack_ns);+}++while(true){+u64umcg_state;++/*+*Weneedtoreadfromuserspace_after_thetaskismarked+*TASK_INTERRUPTIBLE,toproperlyhandleconcurrentwakeups;+*butfaultingisnotallowed;sowetryafastno-faultread,+*andifitfails,pinthepagetemporarily.+*/+retry_once:+set_current_state(TASK_INTERRUPTIBLE);++/* Order set_current_state above with get_user below. */+smp_mb();+ret=-EFAULT;+if(get_user_nofault(umcg_state,&self->state_ts)){+set_current_state(TASK_RUNNING);++if(pinned_page)+gotoout;+elseif(1!=pin_user_pages_fast((unsignedlong)self,+1,0,&pinned_page))+gotoout;++gotoretry_once;+}++if(pinned_page){+unpin_user_page(pinned_page);+pinned_page=NULL;+}++ret=0;+if(!umcg_should_idle(umcg_state)){+set_current_state(TASK_RUNNING);+gotoout;+}++if(abs_timeout)+hrtimer_sleeper_start_expires(&timeout,HRTIMER_MODE_ABS);++if(!abs_timeout||timeout.task)+freezable_schedule();++__set_current_state(TASK_RUNNING);++/*+*Checkfortimeoutbeforecheckingthestate,asworkers+*arenotgoingtoreturnfromfreezable_schedule()unless+*theyareRUNNING.+*/+ret=-ETIMEDOUT;+if(abs_timeout&&!timeout.task)+gotoout;++/* Order set_current_state above with get_user below. */+smp_mb();+ret=-EFAULT;+if(get_user(umcg_state,&self->state_ts))+gotoout;++ret=0;+if(!umcg_should_idle(umcg_state))+gotoout;++ret=-EINTR;+if(fatal_signal_pending(current))+gotoout;++if(signal_pending(current))+flush_signals(current);+}++out:+if(pinned_page){+unpin_user_page(pinned_page);+pinned_page=NULL;+}+if(abs_timeout){+hrtimer_cancel(&timeout.timer);+destroy_hrtimer_on_stack(&timeout.timer);+}+if(worker){+current->flags|=PF_UMCG_WORKER;++if(ret==-ETIMEDOUT)+ret=handle_timedout_worker(self);++/* Workers must go through workqueue handlers upon wakeup. */+set_tsk_thread_flag(current,TIF_NOTIFY_RESUME);+}+returnret;+}++/**+*umcg_wakeup_allowed-checkwhether@currentcanwake@tsk.+*+*Currentlyaplaceholderthatallowswakeupswithinasingleprocess+*only(samemm).Inthefuturetherequirementwillberelaxed(securely).+*/+staticboolumcg_wakeup_allowed(structtask_struct*tsk)+{+WARN_ON_ONCE(!rcu_read_lock_held());++if(tsk->mm&&tsk->mm==current->mm&&READ_ONCE(tsk->umcg_task))+returntrue;++returnfalse;+}++/*+*Trytowakeup.Maybecalledwithpreempt_disableset.Maybecalled+*cross-process.+*+*Note:umcg_ttwusucceedsevenifttwufails:seewait/wakestate+*orderinglogic.+*/+staticintumcg_ttwu(u32next_tid,intwake_flags)+{+structtask_struct*next;++rcu_read_lock();+next=find_task_by_vpid(next_tid);+if(!next||!umcg_wakeup_allowed(next)){+rcu_read_unlock();+return-ESRCH;+}++/* The result of ttwu below is ignored. */+try_to_wake_up(next,TASK_NORMAL,wake_flags);+rcu_read_unlock();++return0;+}++/*+*Atthemoment,umcg_do_context_switchsimplywakesup@nextwith+*WF_CURRENT_CPUandputsthecurrenttasktosleep.+*+*Inthefutureanoptimizationwillbeaddedtoadjustruntimeaccounting+*sothatfromthekernelschedulingperspectivethetwotasksare+*essentiallytreatedasone.Inaddition,thecontextswitchmaybeperformed+*righthereonthefastpath,insteadofgoingthroughthewake/waitpair.+*/+staticintumcg_do_context_switch(u32next_tid,u64abs_timeout)+{+intret;++ret=umcg_ttwu(next_tid,WF_CURRENT_CPU);+if(ret)+returnret;++returnumcg_idle_loop(abs_timeout);+}++/**+*sys_umcg_wait:putthecurrenttasktosleepand/orwakeanothertask.+*@flags:zerooravaluefromenumumcg_wait_flag.+*@abs_timeout:whentowakethetask,innanoseconds;zerofornotimeout.+*+*@self->state_tsmustbeUMCG_TASK_IDLE(where@selfiscurrent->umcg_task)+*if!(@flags&UMCG_WAIT_WAKE_ONLY)(alsoseeumcg_idle_loopand+*umcg_should_idleabove).+*+*If@self->next_tidisnotzero,itmustpointtoanIDLEUMCGtask.+*TheuserspacemusthavechangeditsstatefromIDLEtoRUNNING+*beforecallingsys_umcg_wait()inthecurrenttask.This"next"+*taskwillbewoken(context-switched-toonthefastpath)whenthe+*currenttaskisputtosleep.+*+*SeeDocumentation/userspace-api/umcg.txtfordetals.+*+*Return:+*0-OK;+*-ETIMEDOUT-thetimeoutexpired;+*-EFAULT-failedaccessingstructumcg_task__userofthecurrent+*task;+*-ESRCH-thetasktowakenotfoundornotaUMCGtask;+*-EINVAL-anothererrorhappened(e.g.bad@flags,orthecurrent+*taskisnotaUMCGtask,etc.)+*/+SYSCALL_DEFINE2(umcg_wait,u32,flags,u64,abs_timeout)+{+structumcg_task__user*self=current->umcg_task;+u32next_tid;++if(!self)+return-EINVAL;++if(get_user(next_tid,&self->next_tid))+return-EFAULT;++if(flags&UMCG_WAIT_WAKE_ONLY){+if(!next_tid||abs_timeout)+return-EINVAL;++flags&=~UMCG_WAIT_WAKE_ONLY;+if(flags&~UMCG_WAIT_WF_CURRENT_CPU)+return-EINVAL;++returnumcg_ttwu(next_tid,flags&UMCG_WAIT_WF_CURRENT_CPU?+WF_CURRENT_CPU:0);+}++/* Unlock the worker, if locked. */+if(current->flags&PF_UMCG_WORKER){+u64umcg_state;++if(get_user(umcg_state,&self->state_ts))+return-EFAULT;++if((umcg_state&UMCG_TF_LOCKED)&&umcg_update_state(+&self->state_ts,&umcg_state,+umcg_state&~UMCG_TF_LOCKED,true))+return-EFAULT;+}++if(next_tid)+returnumcg_do_context_switch(next_tid,abs_timeout);++returnumcg_idle_loop(abs_timeout);+}++/*+*NOTE:allcodebelowiscalledfromworkqueuesubmit/update,or+*syscallexittousermodeloop,soallerrorsresultinthe+*terminationofthecurrenttask(viaSIGKILL).+*/++/*+*Wakeidleserver:findthetask,changeitsstateIDLE=>RUNNING,ttwu.+*/+staticintumcg_wake_idle_server_nofault(u32server_tid)+{+structumcg_task__user*ut_server=NULL;+structtask_struct*tsk;+intret=-EINVAL;+u64state;++rcu_read_lock();++tsk=find_task_by_vpid(server_tid);+/* Server/worker interaction is allowed only within the same mm. */+if(tsk&¤t->mm==tsk->mm)+ut_server=READ_ONCE(tsk->umcg_task);++if(!ut_server)+gotoout_rcu;++ret=-EFAULT;+if(get_user_nofault(state,&ut_server->state_ts))+gotoout_rcu;++ret=-EAGAIN;+if((state&UMCG_TASK_STATE_MASK)!=UMCG_TASK_IDLE)+gotoout_rcu;++ret=umcg_update_state(&ut_server->state_ts,&state,+(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING,+false);++if(ret)+gotoout_rcu;++try_to_wake_up(tsk,TASK_NORMAL,WF_CURRENT_CPU);++out_rcu:+rcu_read_unlock();+returnret;+}++/*+*Wakeidleserver:findthetask,changeitsstateIDLE=>RUNNING,ttwu.+*/+staticintumcg_wake_idle_server_may_fault(u32server_tid)+{+structumcg_task__user*ut_server=NULL;+structtask_struct*tsk;+intret=-EINVAL;+u64state;++rcu_read_lock();+tsk=find_task_by_vpid(server_tid);+if(tsk&¤t->mm==tsk->mm)+ut_server=READ_ONCE(tsk->umcg_task);+rcu_read_unlock();++if(!ut_server)+return-EINVAL;++if(get_user(state,&ut_server->state_ts))+return-EFAULT;++if((state&UMCG_TASK_STATE_MASK)!=UMCG_TASK_IDLE)+return-EAGAIN;++ret=umcg_update_state(&ut_server->state_ts,&state,+(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING,+true);+if(ret)+returnret;++/*+*umcg_ttwuwillcallfind_task_by_vpidagain;butwecannot+*elidethis,aswecannotdoget_user()fromanrcu-locked+*codeblock.+*/+returnumcg_ttwu(server_tid,WF_CURRENT_CPU);+}++/*+*Wakeidleserver:findthetask,changeitsstateIDLE=>RUNNING,ttwu.+*/+staticintumcg_wake_idle_server(u32server_tid,boolmay_fault)+{+intret=umcg_wake_idle_server_nofault(server_tid);++if(!ret)+return0;++if(!may_fault||ret!=-EFAULT)+returnret;++returnumcg_wake_idle_server_may_fault(server_tid);+}++/*+*Calledinsched_submit_work()contextforUMCGworkers.Inthecommoncase,+*theworker'sstatechangesRUNNING=>BLOCKED,anditsserver'sstate+*changesIDLE=>RUNNING,andtheserveristtwu-ed.+*+*Undersomeconditions(e.g.theworkeris"locked",see+*/Documentation/userspace-api/umcg.txtformoredetails),the+*functiondoesnothing.+*+*Thefunctioniscalledwithpreemptdisabledtomakesuretheretry_once+*logicbelowworkscorrectly.+*/+staticvoidprocess_sleeping_worker(structtask_struct*tsk,u32*server_tid)+{+structumcg_task__user*ut_worker=tsk->umcg_task;+u64curr_state,next_state;+boolretried=false;+u32tid;+intret;++*server_tid=0;++if(WARN_ONCE((tsk!=current)||!ut_worker,"Invalid UMCG worker."))+return;++/* If the worker has no server, do nothing. */+if(unlikely(!tsk->pinned_umcg_server_page))+return;++if(get_user_nofault(curr_state,&ut_worker->state_ts))+gotodie;++/*+*TheuserspaceisallowedtoconcurrentlychangeaRUNNINGworker's+*stateonlyonceina"short"periodoftime,soweretrystate+*changeatmostonce.Asthisretryblockiswithina+*preempt_disableregion,"short"istrulyshorthere.+*+*SeeDocumentation/userspace-api/umcg.txtfordetails.+*/+retry_once:+if(curr_state&UMCG_TF_LOCKED)+return;++if(WARN_ONCE((curr_state&UMCG_TASK_STATE_MASK)!=UMCG_TASK_RUNNING,+"Unexpected UMCG worker state."))+gotodie;++next_state=curr_state&~UMCG_TASK_STATE_MASK;+next_state|=UMCG_TASK_BLOCKED;++ret=umcg_update_state(&ut_worker->state_ts,&curr_state,next_state,false);+if(ret==-EAGAIN){+if(retried)+gotodie;++retried=true;+gotoretry_once;+}+if(ret)+gotodie;++smp_mb();/* Order state read/write above and getting next_tid below. */+if(get_user_nofault(tid,&ut_worker->next_tid))+gotodie;++*server_tid=tid;+return;++die:+pr_warn("%s: killing task %d\n",__func__,current->pid);+force_sig(SIGKILL);+}++/* Called from sched_submit_work(). Must not fault/sleep. */+voidumcg_wq_worker_sleeping(structtask_struct*tsk)+{+u32server_tid;++/*+*Disablepreemptionsothatretry_onceinprocess_sleeping_worker+*worksproperly.+*/+preempt_disable();+process_sleeping_worker(tsk,&server_tid);+preempt_enable();++if(server_tid){+intret=umcg_wake_idle_server_nofault(server_tid);++if(ret&&ret!=-EAGAIN)+gotodie;+}++gotoout;++die:+pr_warn("%s: killing task %d\n",__func__,current->pid);+force_sig(SIGKILL);+out:+umcg_unpin_pages();+}++/**+*enqueue_idle_worker-pushanidleworkerontoidle_workers_ptrlist/stack.+*+*Returnstrueonsuccess,falseonafatalfailure.+*+*SeeDocumentation/userspace-api/umcg.txtfordetails.+*/+staticboolenqueue_idle_worker(structumcg_task__user*ut_worker)+{+u64__user*node=&ut_worker->idle_workers_ptr;+u64__user*head_ptr;+u64first=(u64)node;+u64head;++if(get_user(head,node)||!head)+returnfalse;++head_ptr=(u64__user*)head;++/* Mark the worker as pending. */+if(put_user(UMCG_IDLE_NODE_PENDING,node))+returnfalse;++/* Make the head point to the worker. */+if(xchg_user_64(head_ptr,&first))+returnfalse;++/* Make the worker point to the previous head. */+if(put_user(first,node))+returnfalse;++returntrue;+}++/**+*get_idle_server-retrieveanidleserver,ifpresent.+*+*Returnstrueonsuccess,falseonafatalfailure.+*/+staticboolget_idle_server(structumcg_task__user*ut_worker,u32*server_tid)+{+u64server_tid_ptr;+u32tid;++/* Empty result is OK. */+*server_tid=0;++if(get_user(server_tid_ptr,&ut_worker->idle_server_tid_ptr))+returnfalse;++if(!server_tid_ptr)+returnfalse;++tid=0;+if(xchg_user_32((u32__user*)server_tid_ptr,&tid))+returnfalse;++*server_tid=tid;+returntrue;+}++/*+*Returnstruetowaitfortheuserspacetoschedulethisworker,false+*toreturntotheuserspace.+*+*Inthecommoncase,aBLOCKEDworkerismarkedIDLEandenqueued+*toidle_workers_ptrlist.Theidleserveriswoken(ifpresent).+*+*IfaRUNNINGworkerispreempted,thisfunctionwilltrigger,inwhich+*casetheworkerismovedtoIDLEstateanditsserveriswoken.+*+*Sets@server_tidtopointtotheservertobewokeniftheworker+*isgoingtosleep;sets@server_tidtopointtotheserverassigned+*tothisRUNNINGworkeriftheworkeristoreturntotheuserspace.+*/+staticboolprocess_waking_worker(structtask_struct*tsk,u32*server_tid)+{+structumcg_task__user*ut_worker=tsk->umcg_task;+u64curr_state,next_state;++*server_tid=0;++if(WARN_ONCE((tsk!=current)||!ut_worker,"Invalid umcg worker"))+returnfalse;++if(fatal_signal_pending(tsk))+returnfalse;++if(get_user(curr_state,&ut_worker->state_ts))+gotodie;++if((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_RUNNING){+u32tid;++/* Wakeup: wait but don't enqueue. */+if(curr_state&UMCG_TF_LOCKED)+returntrue;++smp_mb();/* Order getting state and getting server_tid */+if(get_user(tid,&ut_worker->next_tid))+gotodie;++if(!tid)+/* RUNNING workers must have servers. */+gotodie;++*server_tid=tid;++/* pass-through: RUNNING with a server. */+if(!(curr_state&UMCG_TF_PREEMPTED))+returnfalse;++/*+*FallthroughtomarktheworkerIDLE:theworkeris+*PREEMPTED.+*/+}elseif(unlikely((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE&&+(curr_state&UMCG_TF_LOCKED)))+/* The worker prepares to sleep or to unregister. */+returnfalse;++if(unlikely((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE))+gotodie;++next_state=curr_state&~UMCG_TASK_STATE_MASK;+next_state|=UMCG_TASK_IDLE;++if(umcg_update_state(&ut_worker->state_ts,&curr_state,+next_state,true))+gotodie;++if(!enqueue_idle_worker(ut_worker))+gotodie;++smp_mb();/* Order enqueuing the worker with getting the server. */+if(!(*server_tid)&&!get_idle_server(ut_worker,server_tid))+gotodie;++returntrue;++die:+pr_warn("umcg_process_waking_worker: killing task %d\n",current->pid);+force_sig(SIGKILL);+returnfalse;+}++/*+*Calledfromsched_update_worker():deferallworkuntillater,as+*sched_update_worker()maybecalledwithin-kernellocksheld.+*/+voidumcg_wq_worker_running(structtask_struct*tsk)+{+set_tsk_thread_flag(tsk,TIF_NOTIFY_RESUME);+}++/* Called via TIF_NOTIFY_RESUME flag from exit_to_user_mode_loop. */+voidumcg_handle_resuming_worker(void)+{+u32server_tid;++/* Avoid recursion by removing PF_UMCG_WORKER */+current->flags&=~PF_UMCG_WORKER;++do{+boolshould_wait;++should_wait=process_waking_worker(current,&server_tid);+if(!should_wait)+break;++if(server_tid){+intret=umcg_wake_idle_server(server_tid,true);++if(ret&&ret!=-EAGAIN)+gotodie;+}++umcg_idle_loop(0);+}while(true);++if(!server_tid)+/* No server => no reason to pin pages. */+umcg_unpin_pages();+elseif(umcg_pin_pages(server_tid))+gotodie;++gotoout;++die:+pr_warn("%s: killing task %d\n",__func__,current->pid);+force_sig(SIGKILL);+out:+current->flags|=PF_UMCG_WORKER;+}
From: Peter Oskolkov <hidden> Date: 2021-11-22 21:14:01
Implement libumcg in tools/lib/umcg. Define higher-level UMCG
API that hides kernel-level UMCG API intricacies.
As a higher-level API, libumcg makes subtle changes to server/worker
interactions, compared to the kernel UMCG API, and introduces
the following new concepts:
- UMCG Group: a collection of servers and workers in a process
that can interact with each other; UMCG groups are useful to
partition servers and workers within a process in order to, for
example, affine work to specific NUMA nodes;
- UMCG basic tasks: these are UMCG servers, from the kernel point
of view; they do not interact with UMCG workers and thus
do not need in UMCG groups; used for cooperative wait/wake/swap
operations.
The main difference of server/worker interaction in libumcg
vs the kernel-side UMCG API is that a wakeup can be queued:
if umcg_wake() is called on a RUNNING UMCG task, the fact is
recorded (in the userspace), and when the task calls umcg_wait()
or umcg_swap(), the wakeup is consumed and the task is not
marked IDLE.
Libumcg exports the following API:
umcg_enabled()
umcg_get_utid()
umcg_set_task_tag()
umcg_get_task_tag()
umcg_create_group()
umcg_destroy_group()
umcg_register_basic_task()
umcg_register_worker()
umcg_register_server()
umcg_unregister_task()
umcg_wait()
umcg_wake()
umcg_swap()
umcg_get_idle_worker()
umcg_run_worker()
umcg_preempt_worker()
umcg_get_time_ns()
See tools/lib/umcg/libumcg.txt for details.
Notes:
- this is still somewhat work-in-progress: while the kernel side
code has been more or less stable over the last couple of months,
the userspace side of things is less so;
- while libumcg is intended to be the main/primary/only direct user
of the kernel UMCG API, at the moment the implementation is more
geared more towards testing and correctness than live production
usage, with a lot of asserts and similar development helpers;
- I have a number of umcg selftests that I plan to clean up and
post shortly.
Signed-off-by: Peter Oskolkov <redacted>
---
tools/lib/umcg/.gitignore | 4 +
tools/lib/umcg/Makefile | 11 +
tools/lib/umcg/libumcg.c | 1202 +++++++++++++++++++++++++++++++++++++
tools/lib/umcg/libumcg.h | 299 +++++++++
4 files changed, 1516 insertions(+)
create mode 100644 tools/lib/umcg/.gitignore
create mode 100644 tools/lib/umcg/Makefile
create mode 100644 tools/lib/umcg/libumcg.c
create mode 100644 tools/lib/umcg/libumcg.h
@@ -0,0 +1,1202 @@+// SPDX-License-Identifier: GPL-2.0+#define _GNU_SOURCE+#include"libumcg.h"++#include<assert.h>+#include<errno.h>+#include<pthread.h>+#include<signal.h>+#include<stdatomic.h>+#include<stdbool.h>+#include<stdio.h>+#include<stdlib.h>+#include<string.h>+#include<threads.h>+#include<time.h>++#include<linux/kernel.h>++staticintsys_umcg_ctl(uint32_tflags,structumcg_task*umcg_task)+{+returnsyscall(__NR_umcg_ctl,flags,umcg_task);+}++staticintsys_umcg_wait(uint32_tflags,uint64_tabs_timeout)+{+returnsyscall(__NR_umcg_wait,flags,abs_timeout);+}++boolumcg_enabled(void)+{+intret=sys_umcg_ctl(UMCG_CTL_REGISTER,NULL);++if(ret&&errno==EINVAL)+returntrue;++returnfalse;+}++uint64_tumcg_get_time_ns(void)+{+structtimespects;++if(clock_gettime(CLOCK_REALTIME,&ts)){+fprintf(stderr,"clock_gettime failed\n");+abort();+}++returnts.tv_sec*NSEC_PER_SEC+ts.tv_nsec;+}++structumcg_task_tls;++/**+*structumcg_group-describesUMCGgroup.+*+*Seetools/lib/umcg/libumcg.txtfordetals.+*/+structumcg_group{+/**+*@idle_workers_head:pointstothekernel-sidelistofidle+*workers,i.e.theaddressofthisfield+*ispassedtothekernelin+*structumcg_task.idle_workers_ptr.+*/+uint64_tidle_workers_head;++/**+*@nr_tasks:thenumberoftasks(serversandworkers)registered+*inthisgroup.+*/+uint64_tnr_tasks;++/**+*@idle_worker_lock:protects@idle_workersbelow.+*/+pthread_spinlock_tidle_worker_lock;++/**+*@idle_server_lock:protects@idle_serversbelow.+*/+pthread_spinlock_tidle_server_lock;++/**+*@idle_workers:pointstotheuserspace-sidelistofidleworkers.+*+*Whenaserverpollsforanidleworkerviaumcg_poll_worker(),+*theserverfirstconsults@idle_workers;ifthelistisempty,+*thevalueofthevariableisswappedwith@idle_workers_head.+*/+uint64_t*idle_workers;++/**+*@idle_servers:pointstotheuserspace-sidelistofidleservers.+*+*Whenaserverpollsforanidleworkerviaumcg_poll_worker(),+*andnoneisavailable,theserverisaddedtothelistandblocks+*viasys_umcg_wait().+*/+structumcg_task_tls*idle_servers;++/**+*@idle_server_tid:theTIDofoneoftheidleservers.+*+*Theaddressofthisfieldispassedtothekernelin+*structumct_task.idle_server_tid_ptr.+*/+uint32_tidle_server_tid;+}__attribute((aligned(8)));++/**+*structumcg_task_tls-perthreadstructusedtoidentify/manageUMCGtasks+*+*EachUMCGtaskrequiresaninstanceofstructumcg_taskpassedto+*sys_umcg_ctl.Thisstructcontainsit,aswellasseveraladditional+*fieldsusefulfortheuserspaceUMCGAPI.+*+*Thealignmentisdrivenbythealignmentofstructumcg_task.+*/+structumcg_task_tls{+structumcg_taskumcg_task;+structumcg_group*group;/* read only */+umcg_tidpeer;/* server or worker or UMCG_NONE */+umcg_tidself;/* read only */+intptr_ttag;+pid_ttid;/* read only */+boolworker;/* read only */++structumcg_task_tls*next;/* used in group->idle_servers */+}__attribute((aligned(8*sizeof(uint64_t))));++staticthread_localstructumcg_task_tls*umcg_task_tls;++umcg_tidumcg_get_utid(void)+{+return(umcg_tid)&umcg_task_tls;+}++staticstructumcg_task_tls*utid_to_utls(umcg_tidutid)+{+assert(utid!=UMCG_NONE);+return*(structumcg_task_tls**)utid;+}++uint64_tumcg_get_task_state(umcg_tidtask)+{+structumcg_task_tls*utls=utid_to_utls(task);+uint64_tstate;++if(!utls)+returnUMCG_TASK_NONE;++state=atomic_load_explicit(&utls->umcg_task.state_ts,memory_order_acquire);+returnstate&UMCG_TASK_STATE_MASK_FULL;+}++/* Update the state variable, set new timestamp. */+staticboolumcg_update_state(uint64_t*state,uint64_t*prev,uint64_tnext)+{+uint64_tprev_ts=(*prev)>>(64-UMCG_STATE_TIMESTAMP_BITS);+structtimespecnow;+uint64_tnext_ts;+intres;++/*+*clock_gettime(CLOCK_MONOTONIC,...)takeslessthan20nsona+*typicalIntelprocessoronaverage,evenwhenrunconcurrently,+*sotheoverheadislowenoughformostapplications.+*+*Ifthisisstilltoohigh,`next_ts=prev_ts+1`shouldwork+*aswell.Theonlyrealrequirementisthatthe"timestamps"are+*uniqueueperthreadwithinareasonabletimeframe.+*/+res=clock_gettime(CLOCK_MONOTONIC,&now);+assert(!res);+next_ts=(now.tv_sec*NSEC_PER_SEC+now.tv_nsec)>>+UMCG_STATE_TIMESTAMP_GRANULARITY;++/* Cut higher order bits. */+next_ts&=((1ULL<<UMCG_STATE_TIMESTAMP_BITS)-1);++if(next_ts==prev_ts)+++next_ts;++#ifndef NDEBUG+if(prev_ts>next_ts){+fprintf(stderr,"%s: time goes back: prev_ts: %lu "+"next_ts: %lu diff: %lu\n",__func__,+prev_ts,next_ts,prev_ts-next_ts);+}+#endif++/* Remove old timestamp, if any. */+next&=((1ULL<<(64-UMCG_STATE_TIMESTAMP_BITS))-1);++/* Set the new timestamp. */+next|=(next_ts<<(64-UMCG_STATE_TIMESTAMP_BITS));++/*+*TODO:reviewwhethermemoryorderbelowcanbeweakenedto+*memory_order_acq_relforsuccessandmemory_order_acquirefor+*failure.+*/+returnatomic_compare_exchange_strong_explicit(state,prev,next,+memory_order_seq_cst,memory_order_seq_cst);+}++staticboolumcg_worker_in_idle_queue(umcg_tidworker)+{+structumcg_task_tls*worker_utls=utid_to_utls(worker);+structumcg_task*worker_ut=&worker_utls->umcg_task;++assert(worker_utls->worker);++return(uint64_t)&worker_utls->group->idle_workers_head!=+atomic_load_explicit(&worker_ut->idle_workers_ptr,+memory_order_acquire);+}++voidumcg_set_task_tag(umcg_tidutid,intptr_ttag)+{+utid_to_utls(utid)->tag=tag;+}++intptr_tumcg_get_task_tag(umcg_tidutid)+{+returnutid_to_utls(utid)->tag;+}++staticbooltry_task_lock(structumcg_task_tls*task,uint64_texpected_state,+uint64_tnew_state)+{+uint64_tnext;+uint64_tprev=atomic_load_explicit(&task->umcg_task.state_ts,+memory_order_acquire);++if(prev&UMCG_TF_LOCKED)+returnfalse;++if((prev&UMCG_TASK_STATE_MASK)!=expected_state)+returnfalse;++next=(prev&~UMCG_TASK_STATE_MASK)|new_state|UMCG_TF_LOCKED;+returnumcg_update_state((uint64_t*)&task->umcg_task.state_ts,&prev,next);+}++staticvoidtask_lock(structumcg_task_tls*task,uint64_texpected_state,+uint64_tnew_state)+{+intloop_counter=0;++while(!try_task_lock(task,expected_state,new_state))+assert(++loop_counter<1000*1000*100);+}++staticvoidtask_unlock(structumcg_task_tls*task,uint64_texpected_state,+uint64_tnew_state)+{+boolok;+uint64_tnext;+uint64_tprev=atomic_load_explicit((uint64_t*)&task->umcg_task.state_ts,+memory_order_acquire);++next=((prev&~UMCG_TASK_STATE_MASK_FULL)|new_state)&~UMCG_TF_LOCKED;+assert(next!=prev);+assert((prev&UMCG_TASK_STATE_MASK_FULL&~UMCG_TF_LOCKED)==expected_state);++ok=umcg_update_state((uint64_t*)&task->umcg_task.state_ts,&prev,next);+assert(ok);+}++umcg_tidumcg_register_basic_task(intptr_ttag)+{+intret;++if(umcg_task_tls!=NULL){+errno=EINVAL;+returnUMCG_NONE;+}++umcg_task_tls=malloc(sizeof(structumcg_task_tls));+if(!umcg_task_tls){+errno=ENOMEM;+returnUMCG_NONE;+}+memset(umcg_task_tls,0,sizeof(structumcg_task_tls));++umcg_task_tls->umcg_task.state_ts=UMCG_TASK_RUNNING;+umcg_task_tls->self=(umcg_tid)&umcg_task_tls;+umcg_task_tls->tag=tag;+umcg_task_tls->tid=gettid();++ret=sys_umcg_ctl(UMCG_CTL_REGISTER,&umcg_task_tls->umcg_task);+if(ret){+free(umcg_task_tls);+umcg_task_tls=NULL;+errno=ret;+returnUMCG_NONE;+}++returnumcg_task_tls->self;+}++staticumcg_tidumcg_register_task_in_group(umcg_tgroup_id,intptr_ttag,+boolserver)+{+intret;+uint32_tself_tid;+structumcg_group*group;+structumcg_task_tls*curr;++if(group_id==UMCG_NONE){+errno=EINVAL;+returnUMCG_NONE;+}++if(umcg_task_tls!=NULL){+errno=EINVAL;+returnUMCG_NONE;+}++group=(structumcg_group*)group_id;++curr=malloc(sizeof(structumcg_task_tls));+if(!curr){+errno=ENOMEM;+returnUMCG_NONE;+}+memset(curr,0,sizeof(structumcg_task_tls));++self_tid=gettid();+curr->umcg_task.state_ts=server?UMCG_TASK_RUNNING:UMCG_TASK_BLOCKED;+curr->umcg_task.idle_server_tid_ptr=server?0UL:+(uint64_t)&group->idle_server_tid;+curr->umcg_task.idle_workers_ptr=+(uint64_t)&group->idle_workers_head;+curr->group=group;+curr->tag=tag;+curr->tid=self_tid;+curr->self=(umcg_tid)&umcg_task_tls;+curr->worker=!server;++/*+*Needtosetumcg_task_tlsbeforeregistering,asaserver+*maypickupthisworkerimmediately,anduse@self.+*/+atomic_store_explicit(&umcg_task_tls,curr,memory_order_release);++ret=sys_umcg_ctl(server?UMCG_CTL_REGISTER:+UMCG_CTL_REGISTER|UMCG_CTL_WORKER,+&curr->umcg_task);+if(ret){+free(curr);+errno=ret;+atomic_store_explicit(&umcg_task_tls,NULL,memory_order_release);+returnUMCG_NONE;+}++atomic_fetch_add_explicit(&group->nr_tasks,1,memory_order_relaxed);++returnumcg_task_tls->self;+}++umcg_tidumcg_register_worker(umcg_tgroup_id,intptr_ttag)+{+returnumcg_register_task_in_group(group_id,tag,false);+}++umcg_tidumcg_register_server(umcg_tgroup_id,intptr_ttag)+{+returnumcg_register_task_in_group(group_id,tag,true);+}++intumcg_unregister_task(void)+{+intret;++if(!umcg_task_tls){+errno=EINVAL;+return-1;+}++/* If this is a worker, wake the server. */+if(umcg_task_tls->worker){+structumcg_task_tls*curr=umcg_task_tls;+structumcg_task_tls*utls_server;++task_lock(curr,UMCG_TASK_RUNNING,UMCG_TASK_IDLE);+utls_server=utid_to_utls(curr->peer);+assert(utls_server->tid==atomic_load_explicit(+&curr->umcg_task.next_tid,+memory_order_acquire));+curr->peer=UMCG_NONE;+atomic_store_explicit(&curr->umcg_task.next_tid,0,+memory_order_release);++utls_server->peer=UMCG_NONE;+atomic_store_explicit(&utls_server->umcg_task.next_tid,0,+memory_order_release);++/* Keep the worker locked to avoid needing the server. */+if(utls_server){+curr->worker=false;/* umcg_wake tries to lock */+ret=umcg_wake(utls_server->self,false);+assert(!ret||errno==ESRCH);+}+}++ret=sys_umcg_ctl(UMCG_CTL_UNREGISTER,NULL);+if(ret){+errno=ret;+return-1;+}++if(umcg_task_tls->group)+atomic_fetch_sub_explicit(&umcg_task_tls->group->nr_tasks,1,+memory_order_relaxed);++free(umcg_task_tls);+atomic_store_explicit(&umcg_task_tls,NULL,memory_order_release);+return0;+}++/* Helper return codes. */+enumumcg_prepare_op_result{+UMCG_OP_DONE,+UMCG_OP_SYS,+UMCG_OP_AGAIN,+UMCG_OP_ERROR+};++staticenumumcg_prepare_op_resultumcg_prepare_wait_may_lock(void)+{+structumcg_task*ut;+uint64_tprev_state,next_state;++if(!umcg_task_tls){+errno=EINVAL;+returnUMCG_OP_ERROR;+}++ut=&umcg_task_tls->umcg_task;++prev_state=atomic_load_explicit(&ut->state_ts,memory_order_acquire);+next_state=umcg_task_tls->worker?+UMCG_TASK_IDLE|UMCG_TF_LOCKED|UMCG_UTF_WORKER_IN_WAIT:+UMCG_TASK_IDLE;+if(((prev_state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_RUNNING)&&+umcg_update_state((uint64_t*)&ut->state_ts,&prev_state,next_state))+returnUMCG_OP_SYS;++if((prev_state&UMCG_TASK_STATE_MASK_FULL)!=+(UMCG_TASK_RUNNING|UMCG_UTF_WAKEUP_QUEUED)){+#ifndef NDEBUG+fprintf(stderr,"libumcg: unexpected state before wait: %lu\n",+prev_state);+assert(false);+#endif+errno=EINVAL;+returnUMCG_OP_ERROR;+}++if(umcg_update_state((uint64_t*)&ut->state_ts,&prev_state,UMCG_TASK_RUNNING))+returnUMCG_OP_DONE;++#ifndef NDEBUG+/* Raced with another wait/wake? This is not supported. */+fprintf(stderr,"libumcg: failed to remove the wakeup flag: %lu\n",+prev_state);+assert(false);+#endif+errno=EINVAL;+returnUMCG_OP_ERROR;+}++/* Always return -1 because the user needs to see ETIMEDOUT in errno */+staticinthandle_timedout(void)+{+structumcg_task*ut=&umcg_task_tls->umcg_task;+uint64_tumcg_state;++retry:+/* Restore RUNNING state if the task is still IDLE. */+umcg_state=atomic_load_explicit(&ut->state_ts,+memory_order_acquire);+if((umcg_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_RUNNING)+return-1;++assert((umcg_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE);++if(umcg_update_state((uint64_t*)&ut->state_ts,&umcg_state,UMCG_TASK_RUNNING))+return-1;++/* A wakeup could have been queued. */+gotoretry;+}++staticintumcg_do_wait(uint64_ttimeout)+{+structumcg_task*ut=&umcg_task_tls->umcg_task;+uint32_tflags=0;++/* If this is a worker, need to change the state of the server. */+if(umcg_task_tls->worker&&+atomic_load_explicit(&ut->next_tid,memory_order_acquire)){+boolok;+structumcg_task*server_ut=+&utid_to_utls(umcg_task_tls->peer)->umcg_task;+uint64_tserver_state=atomic_load_explicit(&server_ut->state_ts,+memory_order_acquire);++assert((server_state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_IDLE);+ok=umcg_update_state((uint64_t*)&server_ut->state_ts,+&server_state,UMCG_TASK_RUNNING);+assert(ok);+}elseif(!umcg_task_tls->worker)+atomic_store_explicit(&ut->next_tid,0,memory_order_release);++do{+uint64_tumcg_state;+intret;++ret=sys_umcg_wait(flags,timeout);+if(!ret)+return0;++if(ret&&errno==EINTR){+umcg_state=atomic_load_explicit(&ut->state_ts,+memory_order_acquire)&UMCG_TASK_STATE_MASK;+if(umcg_state==UMCG_TASK_RUNNING)+return0;+continue;+}++if(errno==ETIMEDOUT)+returnhandle_timedout();++return-1;+}while(true);+}++intumcg_wait(uint64_ttimeout)+{+switch(umcg_prepare_wait_may_lock()){+caseUMCG_OP_DONE:+return0;+caseUMCG_OP_SYS:+break;+caseUMCG_OP_ERROR:+return-1;+default:+assert(false);+return-1;+}++returnumcg_do_wait(timeout);+}++staticvoidenqueue_idle_worker(structumcg_task_tls*utls)+{+structumcg_task*ut=&utls->umcg_task;+uint64_t*node=(uint64_t*)&ut->idle_workers_ptr;+uint64_thead=*node;+uint64_t*head_ptr=(uint64_t*)head;+uint64_tfirst=(uint64_t)node;++assert(utls->worker);+assert(&utls->group->idle_workers_head==head_ptr);++/* Mark the worker as pending. */+atomic_store_explicit(node,UMCG_IDLE_NODE_PENDING,memory_order_release);++/* Make the head point to the worker. */+first=atomic_exchange_explicit(head_ptr,first,memory_order_acq_rel);++/* Make the worker point to the previous head. */+atomic_store_explicit(node,first,memory_order_release);+}++staticenumumcg_prepare_op_resultumcg_prepare_wake_may_lock(+structumcg_task_tls*next_utls,boolfor_swap)+{+structumcg_task*next_ut=&next_utls->umcg_task;+uint64_tcurr_state,next_state;+enumumcg_prepare_op_resultresult=UMCG_OP_DONE;+boolenqueue_worker=false;++curr_state=atomic_load_explicit(&next_ut->state_ts,memory_order_acquire);++if(curr_state&(UMCG_TF_LOCKED|UMCG_UTF_WAKEUP_QUEUED))+returnUMCG_OP_AGAIN;++/* Start with RUNNING tasks. */+if((curr_state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_RUNNING)+next_state=UMCG_TASK_RUNNING|UMCG_UTF_WAKEUP_QUEUED;+elseif(curr_state&UMCG_UTF_WORKER_IN_WAIT){+/* Next, check workers in wait. */+assert(next_utls->worker);+assert((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE);++if(for_swap){+next_state=UMCG_TASK_RUNNING;+result=UMCG_OP_SYS;+}else{+next_state=UMCG_TASK_IDLE;+enqueue_worker=true;+}+}elseif((curr_state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_IDLE){+/* Next, check IDLE tasks. */+if(next_utls->worker){+if(for_swap){+next_state=UMCG_TASK_RUNNING|UMCG_TF_LOCKED;+result=UMCG_OP_SYS;+}else{+returnUMCG_OP_AGAIN;+}+}else{+atomic_store_explicit(&next_utls->umcg_task.next_tid,+0,memory_order_release);+next_state=UMCG_TASK_RUNNING;+result=UMCG_OP_SYS;+}+}else{+/* Finally, deal with BLOCKED workers. */+assert((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_BLOCKED);+assert(next_utls->worker);++returnUMCG_OP_AGAIN;+}++if(umcg_update_state((uint64_t*)&next_ut->state_ts,&curr_state,next_state)){+if(enqueue_worker)+enqueue_idle_worker(next_utls);+returnresult;+}++returnUMCG_OP_AGAIN;+}++staticintumcg_do_wake_or_swap(uint32_tnext_tid,boolshould_wait,+uint64_ttimeout,boolwf_current_cpu,+structumcg_task_tls*next_utls)+{+structumcg_task*ut;+uint32_tflags=0;+uint32_tserver_tid=0;+intret;++/* wf_current_cpu is possible in wake-only scenarios. */+assert(!should_wait||!wf_current_cpu);+assert(umcg_task_tls!=NULL);++ut=&umcg_task_tls->umcg_task;++/*+*Thisisaworkerwakinganothertask:lockitsothatnext_tid+*isnotinterpretedasaserverifthisworkerpagefaults.+*/+if(umcg_task_tls->worker&&!should_wait){+server_tid=atomic_load_explicit(&ut->next_tid,+memory_order_acquire);+assert(server_tid);+assert(utid_to_utls(umcg_task_tls->peer)->tid==server_tid);+task_lock(umcg_task_tls,UMCG_TASK_RUNNING,UMCG_TASK_IDLE);+}++atomic_store_explicit(&ut->next_tid,next_tid,memory_order_release);++if(!should_wait)+flags|=UMCG_WAIT_WAKE_ONLY;+if(wf_current_cpu)+flags|=UMCG_WAIT_WF_CURRENT_CPU;++if(next_utls&&next_utls->worker)+task_unlock(next_utls,UMCG_TASK_RUNNING,UMCG_TASK_RUNNING);+ret=sys_umcg_wait(flags,should_wait?timeout:0);++/* If we locked this worker, unlock it. */+if(server_tid){+atomic_store_explicit(&ut->next_tid,server_tid,+memory_order_release);+task_unlock(umcg_task_tls,UMCG_TASK_IDLE,UMCG_TASK_RUNNING);+}++if(ret&&errno==ETIMEDOUT)+returnhandle_timedout();++returnret;+}++intumcg_wake(umcg_tidnext,boolwf_current_cpu)+{+structumcg_task_tls*utls=utid_to_utls(next);+uint64_tloop_counter=0;++if(!utls){+errno=EINVAL;+return-1;+}++again:+assert(++loop_counter<(1ULL<<31));+switch(umcg_prepare_wake_may_lock(utls,false/* for_swap */)){+caseUMCG_OP_DONE:+return0;+caseUMCG_OP_SYS:+break;+caseUMCG_OP_ERROR:+return-1;+caseUMCG_OP_AGAIN:+gotoagain;+default:+assert(false);+return-1;+}++returnumcg_do_wake_or_swap(utls->tid,false,0,wf_current_cpu,utls);+}++staticvoidtransfer_server_locked(structumcg_task_tls*next)+{+structumcg_task_tls*curr=umcg_task_tls;+structumcg_task_tls*server=utid_to_utls(curr->peer);++atomic_thread_fence(memory_order_acquire);+assert(curr->worker);+assert(next->worker);+assert(curr->peer!=UMCG_NONE);+assert(next->peer==UMCG_NONE);++next->peer=curr->peer;+curr->peer=UMCG_NONE;+next->umcg_task.next_tid=curr->umcg_task.next_tid;+curr->umcg_task.next_tid=0;++server->peer=next->self;+server->umcg_task.next_tid=next->tid;+atomic_thread_fence(memory_order_release);+}++intumcg_swap(umcg_tidnext,uint64_ttimeout)+{+structumcg_task_tls*utls=utid_to_utls(next);+boolshould_wake,should_wait;+uint64_tloop_counter=0;++assert(umcg_task_tls);++again:+assert(++loop_counter<(1ULL<<31));+switch(umcg_prepare_wake_may_lock(utls,true/* for_swap */)){+caseUMCG_OP_DONE:+should_wake=false;+break;+caseUMCG_OP_SYS:+should_wake=true;+break;+caseUMCG_OP_ERROR:+return-1;+caseUMCG_OP_AGAIN:+gotoagain;+default:+assert(false);+}++switch(umcg_prepare_wait_may_lock()){+caseUMCG_OP_DONE:+should_wait=false;+break;+caseUMCG_OP_SYS:+should_wait=true;+break;+caseUMCG_OP_ERROR:+return-1;+default:+assert(false);+}++if(should_wait&&should_wake&&umcg_task_tls->worker)+transfer_server_locked(utls);++if(should_wake)+returnumcg_do_wake_or_swap(utls->tid,should_wait,timeout,+false,utls);++if(should_wait)+returnumcg_do_wait(timeout);++return0;+}++/* A noop SIGUSR1 handler, used in worker preemption. */+staticvoidsigusr_handler(intsignum)+{+}++umcg_tumcg_create_group(uint32_tflags)+{+structumcg_group*group;+intres;++if(flags&&flags!=UMCG_GROUP_ENABLE_PREEMPTION){+errno=EINVAL;+returnUMCG_NONE;+}++group=malloc(sizeof(structumcg_group));+if(!group){+errno=ENOMEM;+returnUMCG_NONE;+}++memset(group,0,sizeof(*group));++res=pthread_spin_init(&group->idle_worker_lock,PTHREAD_PROCESS_PRIVATE);+if(res){+errno=res;+gotoerror;+}++res=pthread_spin_init(&group->idle_server_lock,PTHREAD_PROCESS_PRIVATE);+if(res){+errno=res;+res=pthread_spin_destroy(&group->idle_worker_lock);+assert(!res);+gotoerror;+}++if(flags&UMCG_GROUP_ENABLE_PREEMPTION){+if(SIG_ERR==signal(SIGUSR1,sigusr_handler)){+res=pthread_spin_destroy(&group->idle_worker_lock);+assert(!res);+res=pthread_spin_destroy(&group->idle_server_lock);+assert(!res);+gotoerror;+}+}++return(intptr_t)group;++error:+free(group);+returnUMCG_NONE;+}++intumcg_destroy_group(umcg_tumcg)+{+intres;+structumcg_group*group=(structumcg_group*)umcg;++if(atomic_load_explicit(&group->nr_tasks,memory_order_acquire)){+errno=EBUSY;+return-1;+}++res=pthread_spin_destroy(&group->idle_worker_lock);+assert(!res);+res=pthread_spin_destroy(&group->idle_server_lock);+assert(!res);++free(group);+return0;+}++staticvoiddetach_worker(void)+{+structumcg_task_tls*server_utls=umcg_task_tls;+structumcg_task_tls*worker_utls;++assert(server_utls->group!=NULL);++atomic_thread_fence(memory_order_acquire);+if(!server_utls->peer)+return;++worker_utls=utid_to_utls(server_utls->peer);+assert(server_utls->peer==worker_utls->self);+assert(worker_utls->peer==server_utls->self);++umcg_task_tls->umcg_task.next_tid=0;+worker_utls->umcg_task.next_tid=0;+worker_utls->peer=UMCG_NONE;+server_utls->peer=UMCG_NONE;++atomic_thread_fence(memory_order_release);+}++umcg_tidumcg_run_worker(umcg_tidworker)+{+structumcg_task_tls*worker_utls=utid_to_utls(worker);+structumcg_task_tls*server_utls=umcg_task_tls;+structumcg_task*server_ut=&umcg_task_tls->umcg_task;+structumcg_task*worker_ut;+uint64_tcurr_state,next_state;+intret;+boolok;++assert(server_utls->group!=NULL);+assert(server_utls->group==worker_utls->group);+assert(worker_utls->worker);++atomic_thread_fence(memory_order_acquire);+assert(server_utls->peer==UMCG_NONE);+assert(worker_utls->peer==UMCG_NONE);++worker_ut=&worker_utls->umcg_task;++assert(!umcg_worker_in_idle_queue(worker));++/*+*MarktheserverIDLEbeforemarkingtheworkerRUNNING:preemption+*canhappenimmediatelyaftertheworkerismarkedRUNNING.+*/+curr_state=atomic_load_explicit((uint64_t*)&server_ut->state_ts,+memory_order_acquire);+assert((curr_state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_RUNNING);+ok=umcg_update_state((uint64_t*)&server_ut->state_ts,&curr_state,+UMCG_TASK_IDLE);+assert(ok);++/* Lock the worker in preparation to run it. */+curr_state=atomic_load_explicit((uint64_t*)&worker_ut->state_ts,+memory_order_acquire);+assert((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE);+assert(!(curr_state&UMCG_TF_LOCKED));+next_state=curr_state&UMCG_UTF_WAKEUP_QUEUED?+UMCG_TASK_RUNNING|UMCG_UTF_WAKEUP_QUEUED:+UMCG_TASK_RUNNING;+ok=umcg_update_state((uint64_t*)&worker_ut->state_ts,&curr_state,+next_state|UMCG_TF_LOCKED);++assert(ok);++/* Attach the server to the worker. */+atomic_thread_fence(memory_order_acquire);+server_ut->next_tid=worker_utls->tid;+worker_ut->next_tid=server_utls->tid;+worker_utls->peer=server_utls->self;+server_utls->peer=worker;++atomic_thread_fence(memory_order_release);+task_unlock(worker_utls,next_state,next_state);++ret=sys_umcg_wait(0,0);++atomic_thread_fence(memory_order_acquire);+if(!server_utls->peer){+assert(server_ut->next_tid==0);+/*+*Theworkerwokeearlyduetoumcg_statechange+*andunregistered/exited.+*/+assert(!ret||errno==ESRCH);+errno=0;+returnUMCG_NONE;+}++assert(!ret);++/* Detach the server from the worker. */+worker_utls=utid_to_utls(server_utls->peer);+detach_worker();++returnworker_utls->self;+}++intumcg_preempt_worker(umcg_tidworker)+{+structumcg_task_tls*worker_utls=utid_to_utls(worker);+structumcg_task*worker_ut=&worker_utls->umcg_task;+uint32_tworker_tid=worker_utls->tid;+uint64_tcurr_state;+intret;++curr_state=atomic_load_explicit(&worker_ut->state_ts,+memory_order_acquire);+if((curr_state&UMCG_TASK_STATE_MASK_FULL)!=UMCG_TASK_RUNNING){+errno=EAGAIN;+return-1;+}++if(!umcg_update_state((uint64_t*)&worker_ut->state_ts,&curr_state,+UMCG_TASK_RUNNING|UMCG_TF_PREEMPTED)){+errno=EAGAIN;+return-1;+}++/*+*Itispossiblethatthisthreadisdescheduledhere,theworker+*pagefaults,wakesup,andthenexits;inthiscasetgkill()below+*willfailwitherrno==ESRCH.+*/+ret=tgkill(getpid(),worker_tid,SIGUSR1);+assert(!ret||errno==ESRCH);+return0;+}++staticvoidwake_idle_server(void)+{+structumcg_group*group=umcg_task_tls->group;+intres;++res=pthread_spin_lock(&group->idle_server_lock);+assert(!res);++if(group->idle_servers){+structumcg_task_tls*server=group->idle_servers;++group->idle_servers=server->next;+server->next=NULL;++assert((atomic_load_explicit(&server->umcg_task.state_ts,+memory_order_acquire)&+UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_IDLE);++res=umcg_wake(server->self,false);+assert(!res);+}++res=pthread_spin_unlock(&group->idle_server_lock);+assert(!res);+}++staticumcg_tidget_idle_worker(void)+{+structumcg_group*group=umcg_task_tls->group;+umcg_tidresult=UMCG_NONE;+uint64_t*head;+intres;++res=pthread_spin_lock(&group->idle_worker_lock);+assert(!res);++head=group->idle_workers;++once_again:+/* First, check the userspace idle worker list. */+if(head){+uint64_tnext;+structumcg_task*worker;+structumcg_task_tls*worker_utls;++worker=container_of((__u64*)head,structumcg_task,idle_workers_ptr);+worker_utls=container_of(worker,structumcg_task_tls,umcg_task);++/* Spin while the worker is pending. */+do{+next=atomic_load_explicit(head,memory_order_acquire);+}while(next==UMCG_IDLE_NODE_PENDING);++/* Wait for the worker's server to detach in umcg_run_worker(). */+while(atomic_load_explicit(&worker_utls->peer,+memory_order_relaxed))+;++/* Pull the worker out of the idle worker list. */+group->idle_workers=(uint64_t*)next;+atomic_store_explicit(&worker->idle_workers_ptr,+(uint64_t)&group->idle_workers_head,+memory_order_release);++if(next)+wake_idle_server();++result=worker_utls->self;+gotoout;+}++/*+*Getthekernel'sidleworkerlist.+*+*TODO:reviewwhethermemoryorderbelowcanbeweakenedto+*memory_order_acq_rel.+*/+head=(uint64_t*)atomic_exchange_explicit(&group->idle_workers_head,+0ULL,memory_order_seq_cst);++if(!head)+gotoout;++group->idle_workers=head;+gotoonce_again;++out:+res=pthread_spin_unlock(&group->idle_worker_lock);+assert(!res);++returnresult;+}++staticvoidenqueue_idle_server(void)+{+structumcg_task_tls*server=umcg_task_tls;+structumcg_group*group=server->group;+intres;++res=pthread_spin_lock(&group->idle_server_lock);+assert(!res);++assert(server->next==NULL);+assert((atomic_load_explicit(&server->umcg_task.state_ts,+memory_order_acquire)&+UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_IDLE);++server->next=group->idle_servers;+group->idle_servers=server;++res=pthread_spin_unlock(&group->idle_server_lock);+assert(!res);+}++staticumcg_tididle_server_wait(void)+{+structumcg_task_tls*server_utls=umcg_task_tls;+structumcg_task*server_ut=&umcg_task_tls->umcg_task;+uint32_tserver_tid=server_utls->tid;+structumcg_group*group=umcg_task_tls->group;+umcg_tidworker;+uint32_tprev=0ULL;+uint64_tstate;+boolok;++state=atomic_load_explicit((uint64_t*)&server_ut->state_ts,memory_order_acquire);+assert((state&UMCG_TASK_STATE_MASK_FULL)==UMCG_TASK_RUNNING);+ok=umcg_update_state((uint64_t*)&server_ut->state_ts,&state,UMCG_TASK_IDLE);+assert(ok);++/*+*TrytobecomeTHEidleserverthatthekernelwillwake.+*+*TODO:reviewwhethermemoryorderbelowcanbeweakenedto+*memory_order_acq_relforsuccessandmemory_order_acquire+*forfailure.+*/+ok=atomic_compare_exchange_strong_explicit(&group->idle_server_tid,+&prev,server_tid,+memory_order_seq_cst,memory_order_seq_cst);++if(!ok){+assert(prev!=server_tid);+enqueue_idle_server();+umcg_do_wait(0);+assert(server_utls->next==NULL);++returnUMCG_NONE;+}++/* We need to ensure no idle workers enqueued before going to sleep. */+worker=get_idle_worker();++if(worker){+state=atomic_load_explicit(&server_ut->state_ts,+memory_order_acquire);+if((state&UMCG_TASK_STATE_MASK_FULL)!=UMCG_TASK_RUNNING){+ok=umcg_update_state((uint64_t*)&server_ut->state_ts,+&state,UMCG_TASK_RUNNING);+assert(ok||((state&UMCG_TASK_STATE_MASK_FULL)==+UMCG_TASK_RUNNING));+}+}else+umcg_do_wait(0);++/*+*Iftheservercallsumcg_get_idle_worker()inaloop,theworker+*thatpulledtheserveratstepN(andthuszeroedidle_server_tid)+*maywaketheserveratstepN+1withoutcleaningidle_server_tid,+*sotheserverneedstocleanidle_server_tidincasethishappens.+*+*TODO:reviewwhethermemoryorderbelowcanbeweakenedto+*memory_order_acq_relforsuccessandmemory_order_acquire+*forfailure.+*/+prev=server_tid;+ok=atomic_compare_exchange_strong_explicit(+&group->idle_server_tid,&prev,0UL,+memory_order_seq_cst,memory_order_seq_cst);+assert(ok||(prev!=server_tid));+returnworker;+}++umcg_tidumcg_get_idle_worker(boolwait)+{+umcg_tidresult=UMCG_NONE;++assert(umcg_task_tls->peer==UMCG_NONE);+assert((atomic_load_explicit(&umcg_task_tls->umcg_task.state_ts,+memory_order_acquire)&UMCG_TASK_STATE_MASK_FULL)==+UMCG_TASK_RUNNING);++do{+result=get_idle_worker();++if(result||!wait)+break;++result=idle_server_wait();+}while(!result);++assert((atomic_load_explicit(&umcg_task_tls->umcg_task.state_ts,+memory_order_acquire)&UMCG_TASK_STATE_MASK_FULL)==+UMCG_TASK_RUNNING);+returnresult;+}
From: Peter Oskolkov <hidden> Date: 2021-11-22 21:14:11
Document User Managed Concurrency Groups syscalls, data structures,
state transitions, etc. in UMGG kernel API.
Signed-off-by: Peter Oskolkov <redacted>
---
Documentation/userspace-api/umcg.txt | 598 +++++++++++++++++++++++++++
1 file changed, 598 insertions(+)
create mode 100644 Documentation/userspace-api/umcg.txt
@@ -0,0 +1,598 @@+UMCG API (KERNEL)++User Managed Concurrency Groups (UMCG) is an M:N threading+subsystem/toolkit that lets user space application developers implement+in-process user space schedulers.++See tools/lib/umcg/umcg.txt for LIBUMCG API, as opposed to UMCG API (kernel)+described here. The first three subsections are the same in both documents.+++CONTENTS++ WHY? HETEROGENEOUS IN-PROCESS WORKLOADS+ REQUIREMENTS+ WHY TWO APIS: UMCG (KERNEL) AND LIBUMCG (USERSPACE)?+ UMCG API (KERNEL)+ SERVERS+ WORKERS+ UMCG TASK STATES+ STRUCT UMCG_TASK+ SYS_UMCG_CTL()+ SYS_UMCG_WAIT()+ STATE TRANSITIONS+ SERVER-ONLY USE CASES+++WHY? HETEROGENEOUS IN-PROCESS WORKLOADS++Linux kernel's CFS scheduler is designed for the "common" use case, with+efficiency/throughput in mind. Work isolation and workloads of different+"urgency" are addressed by tools such as cgroups, CPU affinity, priorities,+etc., which are difficult or impossible to efficiently use in-process.++For example, a single DBMS process may receive tens of thousands requests+per second; some of these requests may have strong response latency+requirements as they serve live user requests (e.g. login authentication);+some of these requests may not care much about latency but must be served+within a certain time period (e.g. an hourly aggregate usage report); some+of these requests are to be served only on a best-effort basis and can be+NACKed under high load (e.g. an exploratory research/hypothesis testing+workload).++Beyond different work item latency/throughput requirements as outlined+above, the DBMS may need to provide certain guarantees to different users;+for example, user A may "reserve" 1 CPU for their high-priority/low-latency+requests, 2 CPUs for mid-level throughput workloads, and be allowed to send+as many best-effort requests as possible, which may or may not be served,+depending on the DBMS load. Besides, the best-effort work, started when the+load was low, may need to be delayed if suddenly a large amount of+higher-priority work arrives. With hundreds or thousands of users like+this, it is very difficult to guarantee the application's responsiveness+using standard Linux tools while maintaining high CPU utilization.++Gaming is another use case: some in-process work must be completed before a+certain deadline dictated by frame rendering schedule, while other work+items can be delayed; some work may need to be cancelled/discarded because+the deadline has passed; etc.++User Managed Concurrency Groups is an M:N threading toolkit that allows+constructing user space schedulers designed to efficiently manage+heterogeneous in-process workloads described above while maintaining high+CPU utilization (95%+).+++REQUIREMENTS++One relatively established way to design high-efficiency, low-latency+systems is to split all work into small on-cpu work items, with+asynchronous I/O and continuations, all executed on a thread pool with the+number of threads not exceeding the number of available CPUs. Although this+approach works, it is quite difficult to develop and maintain such a+system, as, for example, small continuations are difficult to piece+together when debugging. Besides, such asynchronous callback-based systems+tend to be somewhat cache-inefficient, as continuations can get scheduled+on any CPU regardless of cache locality.++M:N threading and cooperative user space scheduling enables controlled CPU+usage (minimal OS preemption), synchronous coding style, and better cache+locality.++Specifically:++* a variable/fluctuating number M of "application" threads should be+ "scheduled over" a relatively fixed number N of "kernel" threads, where+ N is less than or equal to the number of CPUs available;+* only those application threads that are attached to kernel threads are+ scheduled "on CPU";+* application threads should be able to cooperatively yield to each other;+* when an application thread blocks in kernel (e.g. in I/O), this becomes+ a scheduling event ("block") that the userspace scheduler should be able+ to efficiently detect, and reassign a waiting application thread to the+ freeded "kernel" thread;+* when a blocked application thread wakes (e.g. its I/O operation+ completes), this event ("wake") should also be detectable by the+ userspace scheduler, which should be able to either quickly dispatch the+ newly woken thread to an idle "kernel" thread or, if all "kernel"+ threads are busy, put it in the waiting queue;+* in addition to the above, it would be extremely useful for a separate+ in-process "watchdog" facility to be able to monitor the state of each+ of the M+N threads, and to intervene in case of runaway workloads+ (interrupt/preempt).+++WHY THE TWO APIS: UMCG (KERNEL) AND LIBUMCG (USERSPACE)?++UMCG syscalls, sys_umcg_ctl() and sys_umcg_wait(), are designed to make+the kernel-side UMCG implementation as lightweight as possible. LIBUMCG,+on the other hand, is designed to expose the key abstractions to users+in a much more usable, higher-level way.++See tools/lib/umcg/libumcg.txt for more details on LIBUMCG API.+++UMCG API (KERNEL)++Based on the requrements above, UMCG API (kernel) is build around the+following ideas:++* UMCG server: a task/thread representing "kernel threads", or CPUs from+ the requirements above;+* UMCG worker: a task/thread representing "application threads", to be+ scheduled over servers;+* UMCG task state: (NONE), RUNNING, BLOCKED, IDLE: states a UMCG task (a+ server or a worker) can be in;+* UMCG task state flag: LOCKED, PREEMPTED: additional state flags that+ can be ORed with the task state to communicate additional information to+ the kernel;+* struct umcg_task: a per-task userspace set of data fields, usually+ residing in the TLS, that fully reflects the current task's UMCG state+ and controls the way the kernel manages the task;+* sys_umcg_ctl(): a syscall used to register the current task/thread as a+ server or a worker, or to unregister a UMCG task;+* sys_umcg_wait(): a syscall used to put the current task to sleep and/or+ wake another task, pontentially context-switching between the two tasks+ on-CPU synchronously.+++SERVERS++When a task/thread is registered as a server, it is in RUNNING state and+behaves like any other normal task/thread. In addition, servers can+interact with other UMCG tasks via sys_umcg_wait():++* servers can voluntarily suspend their execution (wait), becoming IDLE;+* servers can wake other IDLE servers;+* servers can context-switch between each other.++Note that if a server blocks in the kernel not via sys_umcg_wait(), it+still retains its RUNNING state.+++WORKERS++A worker cannot be RUNNING without having a server associated with it, so+when a task is first registered as a worker, it enters the IDLE state.++* a worker becomes RUNNING when a server calls sys_umcg_wait to+ context-switch into it; the server goes IDLE, and the worker becomes+ RUNNING in its place;+* when a RUNNING worker blocks in the kernel, it becomes BLOCKED, its+ associated server becomes RUNNING and the server's sys_umcg_wait() call+ from the bullet above returns; this transition is sometimes called+ "block detection";+* when the syscall on which a BLOCKED worker completes, the worker+ becomes IDLE and is added to the list of idle workers; if there is an+ idle server waiting, the kernel wakes it; this transition is sometimes+ called "wake detection";+* RUNNING workers can voluntarily suspend their execution (wait),+ becoming IDLE; their associated servers are woken;+* a RUNNING worker can context-switch with an IDLE worker; the server of+ the switched-out worker is transferred to the switched-in worker;+* any UMCG task can "wake" an IDLE worker via sys_umcg_wait(); unless+ this is a server running the worker as described in the first bullet in+ this list, the worker remain IDLE but is added to the idle workers list;+ this "wake" operation exists for completeness, to make sure+ wait/wake/context-switch operations are available for all UMCG tasks;+* the userspace can preempt a RUNNING worker by marking it+ RUNNING|PREEMPTED and sending a signal to it; the userspace should have+ installed a NOP signal handler for the signal; the kernel will then+ transition the worker into IDLE|PREEMPTED state and wake its associated+ server.+++UMCG TASK STATES++Important: all state transitions described below involve at least two+steps: the change of the state field in struct umcg_task, for example+RUNNING to IDLE, and the corresponding change in struct task_struct state,+for example a transition between the task running on CPU and being+descheduled and removed from the kernel runqueue. The key principle of UMCG+API design is that the party initiating the state transition modifies the+state variable.++For example, a task going IDLE first changes its state from RUNNING to IDLE+in the userpace and then calls sys_umcg_wait(), which completes the+transition.++Note on documentation: in include/uapi/linux/umcg.h, task states have the+form UMCG_TASK_RUNNING, UMCG_TASK_BLOCKED, etc. In this document these are+usually referred to simply RUNNING and BLOCKED, unless it creates+ambiguity. Task state flags, e.g. UMCG_TF_PREEMPTED, are treated similarly.++UMCG task states reflect the view from the userspace, rather than from the+kernel. There are three fundamental task states:++* RUNNING: indicates that the task is schedulable by the kernel; applies+ to both servers and workers;+* IDLE: indicates that the task is not schedulable by the kernel (see+ umcg_idle_loop() in kernel/sched/umcg.c); applies to both servers and+ workers;+* BLOCKED: indicates that the worker is blocked in the kernel; does not+ apply to servers.++In addition to the three states above, two state flags help with state+transitions:++* LOCKED: the userspace is preparing the worker for a state transition+ and "locks" the worker until the worker is ready for the kernel to act+ on the state transition; used similarly to preempt_disable or+ irq_disable in the kernel; applies only to workers in RUNNING or IDLE+ state; RUNNING|LOCKED means "this worker is about to become RUNNING,+ while IDLE|LOCKED means "this worker is about to become IDLE or+ unregister;+* PREEMPTED: the userspace indicates it wants the worker to be preempted;+ there are no situations when both LOCKED and PREEMPTED flags are set at+ the same time.+++STRUCT UMCG_TASK++From include/uapi/linux/umcg.h:++struct umcg_task {+ uint64_t state_ts; /* r/w */+ uint32_t next_tid; /* r */+ uint32_t flags; /* reserved */+ uint64_t idle_workers_ptr; /* r/w */+ uint64_t idle_server_tid_ptr; /* r* */+};++Each UMCG task is identified by struct umcg_task, which is provided to the+kernel when the task is registered via sys_umcg_ctl().++* uint64_t state_ts: the current state of the task this struct+ identifies, as described in the previous section, combined with a+ unique timestamp indicating when the last state change happened.++ Readable/writable by both the kernel and the userspace.++ bits 0 - 5: task state (RUNNING, IDLE, BLOCKED);+ bits 6 - 7: state flags (LOCKED, PREEMPTED);+ bits 8 - 12: reserved; must be zeroes;+ bits 13 - 17: for userspace use;+ bits 18 - 63: timestamp.++ Timestamp: a 46-bit CLOCK_MONOTONIC timestamp, at 16ns resolution.++ It is highly benefitical to tag each state change with a unique+ timestamp:++ - timestamps will naturally provide instrumentation to measure+ scheduling delays, both in the kernel and in the userspace;+ - uniqueness of timestamps (module overflow) guarantees that state+ change races, especially ABA races, are easily detected and avoided.++ Each timestamp represents the moment in time the state change happened,+ in nanoseconds, with the lower 4 bits and the upper 16 bits stripped.++ In this document 'umcg_task.state' is often used to talk about+ 'umcg_task.state_ts' field, as timestamps do not carry semantic+ meaning at the moment.++ This is how umcg_task.state_ts is updated in the kernel:++ /* kernel side */+ /**+ * umcg_update_state: atomically update umcg_task.state_ts, set new timestamp.+ * @state_ts - points to the state_ts member of struct umcg_task to update;+ * @expected - the expected value of state_ts, including the timestamp;+ * @desired - the desired value of state_ts, state part only;+ * @may_fault - whether to use normal or _nofault cmpxchg.+ *+ * The function is basically cmpxchg(state_ts, expected, desired), with extra+ * code to set the timestamp in @desired.+ */+ static int umcg_update_state(u64 __user *state_ts, u64 *expected, u64 desired,+ bool may_fault)+ {+ u64 curr_ts = (*expected) >> (64 - UMCG_STATE_TIMESTAMP_BITS);+ u64 next_ts = ktime_get_ns() >> UMCG_STATE_TIMESTAMP_GRANULARITY;++ /* Cut higher order bits. */+ next_ts &= ((1ULL << UMCG_STATE_TIMESTAMP_BITS) - 1);++ if (next_ts == curr_ts)+ ++next_ts;++ /* Remove an old timestamp, if any. */+ desired &= ((1ULL << (64 - UMCG_STATE_TIMESTAMP_BITS)) - 1);++ /* Set the new timestamp. */+ desired |= (next_ts << (64 - UMCG_STATE_TIMESTAMP_BITS));++ if (may_fault)+ return cmpxchg_user_64(state_ts, expected, desired);++ return cmpxchg_user_64_nofault(state_ts, expected, desired);+ }++* uint32_t next_tid: contains the TID of the task to context-switch-into+ in sys_umcg_wait(); can be zero; writable by the userspace, readable by+ the kernel; if this is a RUNNING worker, this field contains the TID of+ the server that should be woken when this worker blocks; see+ sys_umcg_wait() for more details;++* uint32_t flags: reserved; must be zero.++* uint64_t idle_workers_ptr: this field forms a single-linked list of+ idle workers: all RUNNING workers have this field set to point to the+ head of the list (a pointer variable in the userspace).++ When a worker's blocking operation in the kernel completes, the kernel+ changes the worker's state from BLOCKED to IDLE and adds the worker to+ the top of the list of idle workers using this logic:++ /* kernel side */+ /**+ * enqueue_idle_worker - push an idle worker onto idle_workers_ptr+ * list/stack.+ *+ * Returns true on success, false on a fatal failure.+ */+ static bool enqueue_idle_worker(struct umcg_task __user *ut_worker)+ {+ u64 __user *node = &ut_worker->idle_workers_ptr;+ u64 __user *head_ptr;+ u64 first = (u64)node;+ u64 head;++ if (get_user_nosleep(head, node) || !head)+ return false;++ head_ptr = (u64 __user *)head;++ if (put_user_nosleep(UMCG_IDLE_NODE_PENDING, node))+ return false;++ if (xchg_user_64(head_ptr, &first))+ return false;++ if (put_user_nosleep(first, node))+ return false;++ return true;+ }++ In the userspace the list is cleared atomically using this logic:++ /* userspace side */+ uint64_t *idle_workers = (uint64_t *)*head;++ atomic_exchange(&idle_workers, NULL);++ The userspace re-points workers' idle_workers_ptr to the list head+ variable before the worker is allowed to become RUNNING again.++ When processing the idle workers list, the userspace should wait for+ workers marked as UMCG_IDLE_NODE_PENDING to have the flag cleared (see+ enqueue_idle_worker() above).++* uint64_t idle_server_tid_ptr: points to a variable in the userspace+ that points to an idle server, i.e. a server in IDLE state waiting in+ sys_umcg_wait(); read-only; workers must have this field set; not used+ in servers.++ When a worker's blocking operation in the kernel completes, the kernel+ changes the worker's state from BLOCKED to IDLE, adds the worker to the+ list of idle workers, and wakes the idle server if present; the kernel+ atomically exchanges (*idle_server_tid_ptr) with 0, thus waking the idle+ server, if present, only once. See State transitions below for more+ details.+++SYS_UMCG_CTL()++int sys_umcg_ctl(uint32_t flags, struct umcg_task *self) is used to+register or unregister the current task as a worker or server. Flags can be+one of the following:++ UMCG_CTL_REGISTER: register a server;+ UMCG_CTL_REGISTER | UMCG_CTL_WORKER: register a worker;+ UMCG_CTL_UNREGISTER: unregister the current server or worker.++When registering a task, self must point to struct umcg_task describing+this server or worker; the pointer must remain valid until the task is+unregistered.++When registering a server, self->state must be RUNNING; all other fields in+self must be zeroes.++When registering a worker, self->state must be BLOCKED;+self->idle_server_tid_ptr and self->idle_workers_ptr must be valid pointers+as described in struct umcg_task; self->next_tid must be zero.++When unregistering a task, self must be NULL.+++SYS_UMCG_WAIT()++int sys_umcg_wait(uint32_t flags, uint64_t abs_timeout) operates on+registered UMCG servers and workers: struct umcg_task *self provided to+sys_umcg_ctl() when registering the current task is consulted in addition+to flags and abs_timeout parameters.++The function can be used to perform one of the three operations:++* wait: if self->next_tid is zero, sys_umcg_wait() puts the current+ task to sleep;+* wake: if self->next_tid is not zero, and flags & UMCG_WAIT_WAKE_ONLY,+ the task identified by next_tid is woken;+* context switch: if self->next_tid is not zero, and !(flags &+ UMCG_WAIT_WAKE_ONLY), the current task is put to sleep and the next task+ is woken, synchronously switching between the tasks on the current CPU+ on the fast path.++Flags can be zero or a combination of the following values:++* UMCG_WAIT_WAKE_ONLY: wake the next task, don't put the current task to+ sleep;+* UMCG_WAIT_WF_CURRENT_CPU: wake the next task on the curent CPU; this+ flag has an effect only if UMCG_WAIT_WAKE_ONLY is set: context switching+ is always attempted to happen on the curent CPU.++The section below provides more details on how servers and workers interact+via sys_umcg_wait(), during worker block/wake events, and during worker+preemption.+++STATE TRANSITIONS++As mentioned above, the key principle of UMCG state transitions is that the+party initiating the state transition modifies the state of affected tasks.++Below, "TASK:STATE" indicates a task T, where T can be either W for worker+or S for server, in state S, where S can be one of the three states,+potentially ORed with a state flag. Each individual state transition is an+atomic operation (cmpxchg) unless indicated otherwise. Also note that the+order of state transitions is important and is part of the contract between+the userspace and the kernel. The kernel is free to kill the task (SIGKILL)+if the contract is broken.++Some worker state transitions below include adding LOCKED flag to worker+state. This is done to indicate to the kernel that the worker is+transitioning state and should not participate in the block/wake detection+routines, which can happen due to interrupts/pagefaults/signals.++IDLE|LOCKED means that a running worker is preparing to sleep, so+interrupts should not lead to server wakeup; RUNNING|LOCKED means that an+idle worker is going to be "scheduled to run", but may not yet have its+server set up properly.++The key invariant: a RUNNING worker (not LOCKED) must have a server+assigned to it.++Key state transitions:++* server to worker context switch ("schedule a worker to run"):+ S:RUNNING+W:IDLE => S:IDLE+W:RUNNING:+ in the userspace, in the context of the server S running:+ S:RUNNING => S:IDLE (mark self as idle)+ W:IDLE => W:RUNNING|LOCKED (mark the worker as running)+ W.next_tid := S.tid; S.next_tid := W.tid (link the server with+ the worker)+ W:RUNNING|LOCKED => W:RUNNING (unlock the worker)+ S: sys_umcg_wait() (make the syscall)+ the kernel context switches from the server to the worker; the+ server sleeps until it becomes RUNNING during one of the+ transitions below;++* worker to server context switch (worker "yields"): S:IDLE+W:RUNNING =>+S:RUNNING+W:IDLE:+ in the userspace, in the context of the worker W running (note that+ a running worker has its next_tid set to point to its server):+ W:RUNNING => W:IDLE|LOCKED (mark self as idle)+ S:IDLE => S:RUNNING (mark the server as running)+ W: sys_umcg_wait() (make the syscall)+ the kernel removes the LOCKED flag from the worker's state and+ context switches from the worker to the server; the worker sleeps+ until it becomes RUNNING;++* worker to worker context switch: W1:RUNNING+W2:IDLE =>+ W1:IDLE+W2:RUNNING:+ in the userspace, in the context of W1 running:+ W2:IDLE => W2:RUNNING|LOCKED (mark W2 as running)+ W1:RUNNING => W1:IDLE|LOCKED (mark self as idle)+ W2.next_tid := W1.next_tid; S.next_tid := W2.tid (transfer the+ server W1 => W2)+ W1:next_tid := W2.tid (indicate that W1 should context-switch+ into W2)+ W2:RUNNING|LOCKED => W2:RUNNING (unlock W2)+ W1: sys_umcg_wait() (make the syscall)+ same as above, the kernel removes the LOCKED flag from the W1's+ state and context switches to next_tid;++* worker wakeup: W:IDLE => W:IDLE, W queued into the idle worker list:+ in the userspace, a server S can wake a worker W sleeping in+ sys_umcg_wait() without "running" it. This is a purely+ userspace operation that adds the worker to the idle worker list.++* block detection: worker blocks in the kernel: S:IDLE+W:RUNNING =>+ S:RUNNING+W:BLOCKED:+ when a worker blocks in the kernel in RUNNING state (not LOCKED),+ before descheduling the task from the CPU the kernel performs+ these operations:+ W:RUNNING => W:BLOCKED+ S := W.next_tid+ S:IDLE => S:RUNNING+ try_to_wake_up(S)+ if any of the first three operations above fail, the worker is+ killed via SIGKILL. Note that ttwu(S) is not required to succeed,+ as the server may still be transitioning to sleep in+ sys_umcg_wait(); before actually putting the server to sleep its+ UMCG state is checked and, if it is RUNNING, sys_umcg_wait()+ returns to the userspace;+ if the worker has its LOCKED flag set, block detection does not+ trigger, as the worker is assumed to be in the userspace+ scheduling code.++* wake detection: worker wakes in the kernel: W:BLOCKED => W:IDLE:+ all workers' returns to the userspace are intercepted:+ start: (a label)+ if W:RUNNING & W.next_tid != 0: let the worker exit to the+ userspace, as this is a RUNNING worker with a server;+ W:* => W:IDLE (previously blocked or woken without servers+ workers are not allowed to return to the userspace);+ the worker is appended to W.idle_workers_ptr idle workers list;+ S := *W.idle_server_tid_ptr; if (S != 0) S:IDLE => S.RUNNING;+ ttwu(S)+ idle_loop(W): this is the same idle loop that sys_umcg_wait()+ uses: it breaks only when the worker becomes RUNNING; when+ the idle loop exits, it is assumed that the userspace has+ properly removed the worker from the idle workers list+ before marking it RUNNING;+ goto start; (repeat from the beginning).++ the logic above is a bit more complicated in the presence of+ LOCKED or PREEMPTED flags, but the main invariants+ stay the same:+ only RUNNING workers with servers assigned are allowed to run+ in the userspace (unless LOCKED);+ newly IDLE workers are added to the idle workers list; any+ user-initiated state change assumes the userspace+ properly removed the worker from the list;+ as with wake detection, any "breach of contract" by the+ userspace will result in the task termination via SIGKILL.++* worker preemption: S:IDLE+W:RUNNING => S:RUNNING+W:IDLE|PREEMPTED:+ when the userspace wants to preempt a RUNNING worker, it changes it+ state, atomically, RUNNING => RUNNING|PREEMPTED and sends a+ signal to the worker via tgkill(); the signal handler, previously+ set up by the userspace, can be a NOP (note that only RUNNING+ workers can be preempted);++ if the worker, at the moment the signal arrived, continued to be+ running on-CPU in the userspace, the "wake detection" code will be+ triggered that, in addition to what was described above, will+ check if the worker is in RUNNING|PREEMPTED state:+ W:RUNNING|PREEMPTED => W:IDLE|PREEMPTED+ S := W.next_tid+ S:IDLE => S:RUNNING+ try_to_wakeup(S)++ if the signal arrives after the worker blocks in the kernel,+ the "block detection" happened as described above, with the+ following change:+ W:RUNNING|PREEMPTED => W:BLOCKED|PREEMPTED+ S := W.next_tid+ S:IDLE => S:RUNNING+ try_to_wake_up(S)++ in any case, the worker's server is woken, with its attached+ worker (S.next_tid) either in BLOCKED|PREEMPTED or IDLE|PREEMPTED+ state.+++SERVER-ONLY USE CASES++Some workloads/applications may benefit from fast and synchronous on-CPU+user-initiated context switches without the need for full userspace+scheduling (block/wake detection). These applications can use "standalone"+UMCG servers to wait/wake/context-switch. At the moment only in-process+operations are allowed. In the future this restriction will be lifted,+and wait/wake/context-switch operations between servers in related processes+be permitted (when it is safe to do so, e.g. if the processes belong+to the same user and/or cgroup).++These "worker-less" operations involve trivial RUNNING <==> IDLE state+changes, not discussed here for brevity.--
@@ -0,0 +1,438 @@+LIBUMCG API (USERSPACE)++User Managed Concurrency Groups (UMCG) is an M:N threading+subsystem/toolkit that lets user space application developers implement+in-process user space schedulers.++See Documentation/userspace-api/umcg.txt for UMCG API (kernel), as opposed+to LIBUMCG API described here. The first three subsections are the+same in both documents.+++CONTENTS++ WHY? HETEROGENEOUS IN-PROCESS WORKLOADS+ REQUIREMENTS+ WHY THE TWO APIS: UMCG (KERNEL) AND LIBUMCG (USERSPACE)?+ LIBUMCG API (USERSPACE)+ SERVERS+ WORKERS+ BASIC UMCG TASKS+ LIBUMCG API+ umcg_t+ umcg_tid+ UMCG_NONE+ umcg_enabled()+ umcg_get_utid()+ umcg_set_task_tag()+ umcg_get_task_tag()+ umcg_create_group()+ umcg_destroy_group()+ umcg_register_basic_task()+ umcg_register_worker()+ umcg_register_server()+ umcg_unregister_task()+ umcg_wait()+ umcg_wake()+ umcg_swap()+ umcg_get_idle_worker()+ umcg_run_worker()+ umcg_preempt_worker()+ umcg_get_time_ns()+++WHY? HETEROGENEOUS IN-PROCESS WORKLOADS++Linux kernel's CFS scheduler is designed for the "common" use case, with+efficiency/throughput in mind. Work isolation and workloads of different+"urgency" are addressed by tools such as cgroups, CPU affinity, priorities,+etc., which are difficult or impossible to efficiently use in-process.++For example, a single DBMS process may receive tens of thousands requests+per second; some of these requests may have strong response latency+requirements as they serve live user requests (e.g. login authentication);+some of these requests may not care much about latency but must be served+within a certain time period (e.g. an hourly aggregate usage report); some+of these requests are to be served only on a best-effort basis and can be+NACKed under high load (e.g. an exploratory research/hypothesis testing+workload).++Beyond different work item latency/throughput requirements as outlined+above, the DBMS may need to provide certain guarantees to different users;+for example, user A may "reserve" 1 CPU for their high-priority/low-latency+requests, 2 CPUs for mid-level throughput workloads, and be allowed to send+as many best-effort requests as possible, which may or may not be served,+depending on the DBMS load. Besides, the best-effort work, started when the+load was low, may need to be delayed if suddenly a large amount of+higher-priority work arrives. With hundreds or thousands of users like+this, it is very difficult to guarantee the application's responsiveness+using standard Linux tools while maintaining high CPU utilization.++Gaming is another use case: some in-process work must be completed before a+certain deadline dictated by frame rendering schedule, while other work+items can be delayed; some work may need to be cancelled/discarded because+the deadline has passed; etc.++User Managed Concurrency Groups is an M:N threading toolkit that allows+constructing user space schedulers designed to efficiently manage+heterogeneous in-process workloads described above while maintaining high+CPU utilization (95%+).+++REQUIREMENTS++One relatively established way to design high-efficiency, low-latency+systems is to split all work into small on-cpu work items, with+asynchronous I/O and continuations, all executed on a thread pool with the+number of threads not exceeding the number of available CPUs. Although this+approach works, it is quite difficult to develop and maintain such a+system, as, for example, small continuations are difficult to piece+together when debugging. Besides, such asynchronous callback-based systems+tend to be somewhat cache-inefficient, as continuations can get scheduled+on any CPU regardless of cache locality.++M:N threading and cooperative user space scheduling enables controlled CPU+usage (minimal OS preemption), synchronous coding style, and better cache+locality.++Specifically:++* a variable/fluctuating number M of "application" threads should be+ "scheduled over" a relatively fixed number N of "kernel" threads, where+ N is less than or equal to the number of CPUs available;+* only those application threads that are attached to kernel threads are+ scheduled "on CPU";+* application threads should be able to cooperatively yield to each other;+* when an application thread blocks in kernel (e.g. in I/O), this becomes+ a scheduling event ("block") that the userspace scheduler should be able+ to efficiently detect, and reassign a waiting application thread to the+ freeded "kernel" thread;+* when a blocked application thread wakes (e.g. its I/O operation+ completes), this event ("wake") should also be detectable by the+ userspace scheduler, which should be able to either quickly dispatch the+ newly woken thread to an idle "kernel" thread or, if all "kernel"+ threads are busy, put it in the waiting queue;+* in addition to the above, it would be extremely useful for a separate+ in-process "watchdog" facility to be able to monitor the state of each+ of the M+N threads, and to intervene in case of runaway workloads+ (interrupt/preempt).+++WHY THE TWO APIS: UMCG (KERNEL) AND LIBUMCG (USERSPACE)?++UMCG syscalls, sys_umcg_ctl() and sys_umcg_wait(), are designed to make+the kernel-side UMCG implementation as lightweight as possible. LIBUMCG,+on the other hand, is designed to expose the key abstractions to users+in a much more usable, higher-level way.++See Documentation/userspace-api/umcg.txt for more details on+UMCG API (kernel).++Please note that LIBUMCG API is itself a rather low-level API intended+to be used to construct higher-level userspace schedulers.++Note: to avoid confusion, in this document "UMCG servers/workers" refer+UMCG tasks when considered in the context of the kernel UMCG API (syscalls),+while "LIBUMCG servers/workers" refer to the same tasks when considered+in the context of the userspace LIBUMC API outlined below. When the+distinction is not important, "UMCG servers/workers" is used generically.+++LIBUMCG API (USERSPACE)++Based on the requrements above, LIBUMCG API (userspace) is build around the+following ideas:++* UMCG server: a thread representing "kernel threads", or CPUs from+ the requirements above;+* UMCG worker: a thread representing "application threads", to be+ scheduled over servers;+* UMCG group: a collection of servers and workers that can interact with+ each other; a single process may contain several UMCG groups (e.g. a+ group per NUMA node);+* a set of functions (API) that allows workers to be "scheduled" over+ servers and to interact with one another cooperatively.+++LIBUMCG SERVERS++When a thread is registered as a server, it behaves like any other normal+thread.++Servers can interact with other servers in the same UMCG group:++* servers can voluntarily suspend their execution by calling umcg_wait();+* servers can wake other servers by calling umcg_wake();+* servers can context-switch between each other by calling umcg_swap().++Servers can also interact with workers in their UMCG group:++* servers can schedule ("run") workers in their place by calling+ umcg_run_worker(); when the worker blocks, the function returns;+* servers can query for workers that finished their blocking operations+ by calling umcg_get_idle_worker();+* servers can force running workers into idle state and have the+ servers running those workers to wakeup by calling umcg_preempt_worker().+++LIBUMCG WORKERS++A worker cannot be running without having a server associated with it, so+when a task is first registered as a worker, it is blocked until a server+"runs" it (new workers are added to the idle worker list).++Workers can interact with other workers in their UMCG group:++* workers can voluntarily suspend their execution by calling umcg_wait();+* workers can wake other workers by calling umcg_wake();+* workers can context-switch between each other by calling umcg_swap().+++LIBUMCG BASIC UMCG TASKS++If the application is only interested in server-to-server interactions,+it does not need to create a UMCG group and may register a server as a+"basic UMCG task". Same umcg_[wait|wake|swap] functions are available.+++LIBUMCG API: umcg_t++umcg_t is an opaque pointer indicating a UMCG group.+++LIBUMCG API: umcg_tid++umcg_tid is an opaque pointer indicating a UMCG task (a basic task,+a server, or a worker).+++LIBUMCG API: UMCG_NONE++UMCG_NONE holds a NULL value for variables of type umcg_t or umcg_tid.+++LIBUMCG API: umcg_enabled()++bool umcg_enabled(void) - returns true if the running kernel exposes+ UMCG kernel API (sys_umcg_ctl and sys_umcg_wait).+++LIBUMCG API: umcg_get_utid++umcg_tid umcg_get_utid(void) - returns the umcg_tid value identifying+ the current thread as a UMCG task. The value+ is guaranteed to be stable over the life+ of the thread, but may be reused between+ different threads (it is a pointer to a TLS+ variable).+++LIBUMCG API: umg_set_task_tag()++void umcg_set_task_tag(umcg_tid utid, intptr_t tag) - a helper function+ used to associate an arbitrary user-provided value+ with a umcg task/thread.+++LIBUMCG API: umcg_get_task_tag()++intptr_t umcg_get_task_tag(umcg_tid utid) - returns a previously set+ task tag, or zero.+++LIBUMCG API: umcg_create_group()++umcg_t umcg_create_group(uint32_t flags) - create a UMCG group.++ UMCG servers and workers at LIBUMCG level must belong to the same UMCG+ group to interact; note that this is different from UMCG kernel API,+ where servers and workers can all interact within the same process.++ UMCG groups are used to partition UMCG tasks (servers and workers) within+ a process, e.g. to allow NUMA-aware scheduling.+++LIBUMCG API: umcg_destroy_group()++int umcg_destroy_group(umcg_t umcg) - destroy a UMCG group. The group must+ be empty, i.e. all its servers and workers must have+ unregistered.+++LIBUMCG API: umcg_register_basic_task()++umcg_tid umcg_register_basic_task(intptr_t tag) - register the current+ thread as a basic LIBUMCG task.++ Basic LIBUMCG tasks do not belong to UMCG groups, and thus cannot+ interact with LIBUMCG workers.++ At the kernel level basic LIBUMCG tasks are servers.+++LIBUMCG API: umcg_register_worker()++umcg_tid umcg_register_worker(umcg_t group_id, intptr_t tag) - register+ the current thread as a LIBUMCG worker in a group.++ LIBUMCG workers, once registered, can forget about being UMCG workers,+ as the only difference vs "normal" threads is that now workers are+ scheduled not by the kernel, but by servers in their UMCG group, which+ happens "transparently" to UMCG workers.++ LIBUMCG workers may call umcg_[wait|wake|swap] to cooperatively share+ workload with other LIBUMCG workers in the same group.++ Note: at the moment UMCG workers, once registered, cannnot receive+ non-fatal signals.+++LIBUMCG API: umcg_register_server()++umcg_tid umcg_register_server(umcg_t group_id, intptr_t tag) - register+ the current thread as a LIBUMCG server in a group.++ LIBUMCG servers schedule LIBUMCG workers in the same group via+ umcg_get_idle_worker(), umcg_run_worker(), and umcg_preempt_worker().+ See descriptions of these functions below for more details.+++LIBUMCG API: umcg_unregister_task()++int umcg_unregister_task(void) - unregister the current thread as a UMCG task.++ A thread can be only one type of a UMCG task at a time. Once unregistered,+ a thread can register again as a different UMCG task type, in the same+ or a different group.+++LIBUMCG API: umcg_wait()++int umcg_wait(uint64_t timeout) - block the current UMCG task until+ the timeout expires or it is woken via umcg_wake() or umcg_swap().++ All UMCG task types can call umcg_wait().+++LIBUMCG API: umcg_wake()++int umcg_wake(umcg_tid next, bool wf_current_cpu) - wake a UMCG task.++ Wake a umcg task if it is blocked as a result of calling umcg_wait() or+ umcg_swap(). If @next is NOT blocked in umcg_wait() or umcg_swap(),+ it will be marked as "wakeup queued"; when @next calls umcg_wait() or+ umcg_swap() later, the "wakeup queued" flag will be removed and+ the function will not block.++ Only one wakeup can be queued per task, so calling umcg_wake for+ a task with a wakeup queued will spin (in the userspace) until the+ wakeup flag is cleared.++ If @next is a worker blocked in umcg_wait(), the worker is added+ to the idle workers list so that it will the be picked up by+ umcg_get_idle_worker().++ Note that "wakeup queued" is a purely LIBUMCG (userspace) concept:+ the kernel (UMCG kernel API) is unaware of it.+++LIBUMCG API: umcg_swap()++int umcg_swap(umcg_tid next, u64 timeout) - block the current task; wake @next.++ umcg_swap() can be used for server-to-server or worker-to-worker+ context switches, but NOT server-to-worker or worker-to-server.+ If a server wants to context switch with a worker, the server+ should call umcg_run_worker(). If a worker wants to context switch+ to its server, it should call umcg_wait().++ In server-to-server context switches, the switching-out server+ is RUNNING; the switching-in server can either be IDLE or RUNNING.+ If the switching-in server is running, it will have a wakeup queued.+ If the switching-out server has a wakeup queued, umcg_swap() will+ consume the wakeup.++ In worker-to-worker context switches, the "normal" behavior is that+ the RUNNING switching-out worker becomes IDLE, its server is+ transferred to the IDLE switching-in worker, and the switching-in+ worker becomes RUNNING.++ The switching-in worker MUST NOT be in the idle worker list: it+ can either be in umcg_wait(), or pulled out of the idle worker list+ previously.++ Same wakeup-queued rules apply to swapping workers as they apply+ to swapping servers.++ Note that while with servers umcg_swap() is technically equivalent+ to { umcg_wake(); umcg_wait(); }, with possible on-cpu optimizations,+ with workers there is a difference, as umcg_swap() will RUN an+ idle worker, while umcg_wake() will add it to the idle worker list.++ This difference, however, is transparent for workers: workers+ engaging is cooperative scheduling via wait/wake/swap will observe+ exactly the same behavior as if they were servers or basic UMCG tasks.+++LIBUMCG API: umcg_get_idle_worker()++umcg_tid umcg_get_idle_worker(bool wait) - get an idle worker from+ the idle worker list.++ Servers can query for unblocked workers by calling umcg_get_idle_worker().++ There are two idle worker lists per UMCG group; the kernel-side list,+ as described in Documentation/userspace-api/umcg.txt, and the+ userspace-side list. umcg_get_idle_worker() first checks the userspace+ list and, if it is not empty, returns the first available idle worker,+ removing it from the list.++ If the userspace list is empty, the function swaps it with the kernel-side+ list of empty workers, and then checks the userspace list again.++ If @wait is true, the function blocks if there are no idle workers+ available. The server will be added to the group's idle server list.+ The server may also be pointed at by struct umcg_task.idle_server_tid_ptr.++ It is safe to call umcg_get_idle_worker() concurrently.+++LIBUMCG API: umcg_run_worker()++umcg_tid umcg_run_worker(umcg_tid worker) - run the worker.++ Servers "run" workers by calling umcg_run_worker(). @worker must be+ IDLE, and must NOT be in the idle worker list. I.e. a server may+ run a worker that is blocked in umcg_wait() either if umcg_wake()+ has NOT been called on the @worker, or if umcg_wake() HAD been+ called, and the worker then was returned via umcg_get_idle_worker().++ umcg_run_worker() will block; if the worker the server is running+ swaps with another worker, the server will get reassigned to that+ new worker.++ When the worker the server is running blocks, umcg_run_worker returns+ the worker's umcg_tid, or UMCG_NONE if the worker unregisters.+++LIBUMCG API: umcg_preempt_worker()++int umcg_preempt_worker(umcg_tid worker) - preempt a RUNNING worker.++ The function interrupgs a RUNNING worker and wakes its server. If+ the worker is not RUNNING (i.e. BLOCKED or IDLE), the function+ returns an error with errno set to EAGAIN.++ The group the worker belongs to must be created with+ UMCG_GROUP_ENABLE_PREEMPTION flag set.++ The function may be called from any thread in the process that+ @worker belongs to.+++LIBUMCG API: umcg_get_time_ns()++uint64_t umcg_get_time_ns(void) - return the current absolute time.++ This function can be used to calculate the absolute timeouts passed+ to umcg_wait() and umcg_swap().--
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 14:40:59
On Mon, Nov 22, 2021 at 01:13:21PM -0800, Peter Oskolkov wrote:
User Managed Concurrency Groups (UMCG) is an M:N threading
subsystem/toolkit that lets user space application developers implement
in-process user space schedulers.
This v0.9.1 patchset is the same as v0.9, where u32/u64 in
uapi/linux/umcg.h are replaced with __u32/__u64, as test robot/lkp
does not recognize u32/u64 for some reason.
v0.9 is v0.8 rebased on top of the current tip/sched/core,
with a fix in umcg_update_state of an issue reported by Tao Zhou.
Key changes from patchset v0.7:
https://lore.kernel.org/all/20211012232522.714898-1-posk@google.com/:
- added libumcg tools/lib/umcg;
- worker "wakeup" is reworked so that it is now purely a userspace op,
instead of waking the thread in order for it to block on return
to the userspace immediately;
- a couple of minor fixes and refactorings.
These big things remain to be addressed (in no particular order):
- support tracing/debugging
- make context switches faster (see umcg_do_context_switch in umcg.c)
- support other architectures
- cleanup and post selftests in tools/testing/selftests/umcg/
- allow cross-mm wakeups (securely)
*groan*... so these patches do *NOT* support the very thing this all
started with, namely block + wakeup notifications. I'm really not sure
how that happened, as that was the sole purpose of the exercise.
Aside of that, the whole uaccess stuff is horrific :-( I'll reply to
that email separately, but the alternative is also included in the
random hackery below.
I'm still trying to make sense of it all, but I'm really not seeing how
any of this satisfies the initial goals, also it is once again 100% new
code :/
---
arch/x86/Kconfig | 1
arch/x86/include/asm/uaccess.h | 106 +++++++++++++++
arch/x86/include/asm/uaccess_64.h | 93 -------------
include/linux/entry-common.h | 2
include/linux/sched.h | 29 ++--
include/linux/thread_info.h | 2
include/linux/uaccess.h | 46 ------
init/Kconfig | 7 -
kernel/entry/common.c | 11 +
kernel/sched/umcg.c | 231 ++++++++++++++++++++-------------
mm/maccess.c | 264 --------------------------------------
11 files changed, 278 insertions(+), 514 deletions(-)
Index: linux-2.6/arch/x86/include/asm/uaccess_64.h
===================================================================
@@ -67,10 +71,12 @@ static int umcg_pin_pages(u32 server_tidtsk=current;/* worker_ut is stable, don't need to repin */-if(!tsk->pinned_umcg_worker_page)-if(1!=pin_user_pages_fast((unsignedlong)worker_ut,1,0,-&tsk->pinned_umcg_worker_page))+// XXX explain, this should never be so+if(!tsk->pinned_umcg_worker_page){+if(pin_user_pages_fast((unsignedlong)worker_ut,1,0,+&tsk->pinned_umcg_worker_page)!=1)return-EFAULT;+}/* server_ut may change, need to repin */if(tsk->pinned_umcg_server_page){
@@ -78,8 +84,8 @@ static int umcg_pin_pages(u32 server_tidtsk->pinned_umcg_server_page=NULL;}-if(1!=pin_user_pages_fast((unsignedlong)server_ut,1,0,-&tsk->pinned_umcg_server_page))+if(pin_user_pages_fast((unsignedlong)server_ut,1,0,+&tsk->pinned_umcg_server_page)!=1)return-EFAULT;return0;
@@ -156,10 +169,17 @@ static int umcg_update_state(u64 __user/* Set the new timestamp. */desired|=(next_ts<<(64-UMCG_STATE_TIMESTAMP_BITS));-if(may_fault)-returncmpxchg_user_64(state_ts,expected,desired);+if(!user_access_begin(state_ts,sizeof(*state_ts)))+return-EFAULT;++success=__try_cmpxchg_user((u64*)state_ts,expected,desired,Efault);+user_access_end();++returnsuccess?0:-EAGAIN;-returncmpxchg_user_64_nofault(state_ts,expected,desired);+Efault:+user_access_end();+return-EFAULT;}/**
@@ -263,7 +282,7 @@ static int handle_timedout_worker(structnext_state=curr_state&~UMCG_TASK_STATE_MASK;next_state|=UMCG_TASK_BLOCKED;-ret=umcg_update_state(&self->state_ts,&curr_state,next_state,true);+ret=umcg_update_state(&self->state_ts,&curr_state,next_state);if(ret)returnret;
@@ -324,7 +343,7 @@ static int umcg_idle_loop(u64 abs_timeoucurrent->timer_slack_ns);}-while(true){+for(;;){u64umcg_state;/*
@@ -333,22 +352,18 @@ static int umcg_idle_loop(u64 abs_timeou*butfaultingisnotallowed;sowetryafastno-faultread,*andifitfails,pinthepagetemporarily.*/-retry_once:set_current_state(TASK_INTERRUPTIBLE);-/* Order set_current_state above with get_user below. */-smp_mb();ret=-EFAULT;if(get_user_nofault(umcg_state,&self->state_ts)){-set_current_state(TASK_RUNNING);-if(pinned_page)-gotoout;-elseif(1!=pin_user_pages_fast((unsignedlong)self,-1,0,&pinned_page))-gotoout;+break;-gotoretry_once;+if(pin_user_pages_fast((unsignedlong)self,+1,0,&pinned_page)!=1)+break;++continue;}if(pinned_page){
@@ -377,27 +389,26 @@ retry_once:*/ret=-ETIMEDOUT;if(abs_timeout&&!timeout.task)-gotoout;+break;-/* Order set_current_state above with get_user below. */-smp_mb();ret=-EFAULT;if(get_user(umcg_state,&self->state_ts))-gotoout;+break;ret=0;if(!umcg_should_idle(umcg_state))-gotoout;+break;ret=-EINTR;if(fatal_signal_pending(current))-gotoout;+break;+// XXX this *cannot* be right, a process can loose signals this way.if(signal_pending(current))flush_signals(current);}+__set_current_state(TASK_RUNNING);-out:if(pinned_page){unpin_user_page(pinned_page);pinned_page=NULL;
@@ -459,26 +467,6 @@ static int umcg_ttwu(u32 next_tid, int wreturn0;}-/*-*Atthemoment,umcg_do_context_switchsimplywakesup@nextwith-*WF_CURRENT_CPUandputsthecurrenttasktosleep.-*-*Inthefutureanoptimizationwillbeaddedtoadjustruntimeaccounting-*sothatfromthekernelschedulingperspectivethetwotasksare-*essentiallytreatedasone.Inaddition,thecontextswitchmaybeperformed-*righthereonthefastpath,insteadofgoingthroughthewake/waitpair.-*/-staticintumcg_do_context_switch(u32next_tid,u64abs_timeout)-{-intret;--ret=umcg_ttwu(next_tid,WF_CURRENT_CPU);-if(ret)-returnret;--returnumcg_idle_loop(abs_timeout);-}-/***sys_umcg_wait:putthecurrenttasktosleepand/orwakeanothertask.*@flags:zerooravaluefromenumumcg_wait_flag.
@@ -581,9 +573,10 @@ static int umcg_wake_idle_server_nofaultif((state&UMCG_TASK_STATE_MASK)!=UMCG_TASK_IDLE)gotoout_rcu;+pagefault_disable();ret=umcg_update_state(&ut_server->state_ts,&state,-(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING,-false);+(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING);+pagefault_enable();if(ret)gotoout_rcu;
@@ -621,8 +614,7 @@ static int umcg_wake_idle_server_may_faureturn-EAGAIN;ret=umcg_update_state(&ut_server->state_ts,&state,-(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING,-true);+(state&~UMCG_TASK_STATE_MASK)|UMCG_TASK_RUNNING);if(ret)returnret;
@@ -690,6 +682,11 @@ static void process_sleeping_worker(stru**SeeDocumentation/userspace-api/umcg.txtfordetails.*/++// XXX this seems like a super gross hack, please explain more.+// XXX ideally we kill this LOCKED but entirely, that just smells+// XXX worse than fish gone bad.+retry_once:if(curr_state&UMCG_TF_LOCKED)return;
@@ -712,6 +711,8 @@ retry_once:if(ret)gotodie;+// XXX write a real ordering comment, see ttwu() for examples+// XXX idem for all other barriers in this file.smp_mb();/* Order state read/write above and getting next_tid below. */if(get_user_nofault(tid,&ut_worker->next_tid))gotodie;
@@ -777,14 +777,25 @@ static bool enqueue_idle_worker(struct ureturnfalse;/* Make the head point to the worker. */-if(xchg_user_64(head_ptr,&first))+if(!user_access_begin(head_ptr,sizeof(*head_ptr)))returnfalse;+first=__xchg_user(head_ptr,(u64)node,Efault);+user_access_end();++// XXX vCPU goes on a holiday here and userspace is left+// XXX with a broken list, cmpxchg based list-add is safer+// XXX that way+/* Make the worker point to the previous head. */if(put_user(first,node))returnfalse;returntrue;++Efault:+user_access_end();+returnfalse;}/**
@@ -870,9 +888,10 @@ static bool process_waking_worker(struct*PREEMPTED.*/}elseif(unlikely((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE&&-(curr_state&UMCG_TF_LOCKED)))+(curr_state&UMCG_TF_LOCKED))){/* The worker prepares to sleep or to unregister. */returnfalse;+}if(unlikely((curr_state&UMCG_TASK_STATE_MASK)==UMCG_TASK_IDLE))gotodie;
@@ -905,18 +923,53 @@ die:*/voidumcg_wq_worker_running(structtask_struct*tsk){-set_tsk_thread_flag(tsk,TIF_NOTIFY_RESUME);+// XXX this cannot be right, userspace needs to know we're blocked+// XXX also, this was exactly what we had those pins for!++addselftoblockedlist();+changestate();+possiblywakenext_tid();++umcg_unpin_pages();++// and then we go sleep.... the umcg_sys_exit() handler will then+// notify userspace we've woken up again and, if available, kick some+// idle thread to pick us up.}-/* Called via TIF_NOTIFY_RESUME flag from exit_to_user_mode_loop. */-voidumcg_handle_resuming_worker(void)+voidumcg_sys_enter(structpt_regs*regs){u32server_tid;/* Avoid recursion by removing PF_UMCG_WORKER */current->flags&=~PF_UMCG_WORKER;-do{+// XXX wth did umcg_task::server_tid go?++if(!server_tid)+umcg_unpin_pages();+elseif(umcg_pin_pages(server_tid))+gotodie;++gotoout;++die:+pr_warn("%s: killing task %d\n",__func__,current->pid);+force_sig(SIGKILL);+out:+current->flags|=PF_UMCG_WORKER;+}++voidumcg_sys_exit(structpt_regs*regs)+{+u32server_tid;++umcg_unpin_pages();++/* Avoid recursion by removing PF_UMCG_WORKER */+current->flags&=~PF_UMCG_WORKER;++for(;;){boolshould_wait;should_wait=process_waking_worker(current,&server_tid);
@@ -931,13 +984,7 @@ void umcg_handle_resuming_worker(void)}umcg_idle_loop(0);-}while(true);--if(!server_tid)-/* No server => no reason to pin pages. */-umcg_unpin_pages();-elseif(umcg_pin_pages(server_tid))-gotodie;+}gotoout;
@@ -2303,9 +2303,10 @@ static inline void rseq_execve(struct ta#ifdef CONFIG_UMCG-voidumcg_handle_resuming_worker(void);-voidumcg_handle_exiting_worker(void);-voidumcg_clear_child(structtask_struct*tsk);+externvoidumcg_sys_enter(structpt_regs*regs);+externvoidumcg_sys_exit(structpt_regs*regs);+externvoidumcg_handle_exiting_worker(void);+externvoidumcg_clear_child(structtask_struct*tsk);/* Called by bprm_execve() in fs/exec.c. */staticinlinevoidumcg_execve(structtask_struct*tsk)
@@ -2314,13 +2315,6 @@ static inline void umcg_execve(struct taumcg_clear_child(tsk);}-/* Called by exit_to_user_mode_loop() in kernel/entry/common.c.*/-staticinlinevoidumcg_handle_notify_resume(void)-{-if(current->flags&PF_UMCG_WORKER)-umcg_handle_resuming_worker();-}-/* Called by do_exit() in kernel/exit.c. */staticinlinevoidumcg_handle_exit(void){
@@ -76,6 +77,9 @@ static long syscall_trace_enter(struct pif(unlikely(work&SYSCALL_WORK_SYSCALL_TRACEPOINT))trace_sys_enter(regs,syscall);+if(work&SYSCALL_WORK_SYSCALL_UMCG)+umcg_sys_enter(regs);+syscall_enter_audit(regs,syscall);returnret?:syscall;
@@ -171,10 +175,8 @@ static unsigned long exit_to_user_mode_lif(ti_work&(_TIF_SIGPENDING|_TIF_NOTIFY_SIGNAL))handle_signal_work(regs,ti_work);-if(ti_work&_TIF_NOTIFY_RESUME){-umcg_handle_notify_resume();+if(ti_work&_TIF_NOTIFY_RESUME)tracehook_notify_resume(regs);-}/* Architecture specific TIF work */arch_exit_to_user_mode_work(regs,ti_work);
Please, just read what you wrote. This scored *really* high on the
WTF'o'meter.
That is aside of:
- that user_access_begin() includes access_ok().
- the fact that having SMAP *inside* a cmpxchg loop is ridiculous.
- that you write cmpxchg inside a loop, but it isn't actually a cmpxchg-loop.
No the real problem is:
- you *DISABLE* pagefaults
- you force the exception handler
- you manually fix up the fault
while you could've just done the op and let the fault handler do it's
thing, that whole function is pointless.
So as a penance for not having looked at this before I wrote you the
replacement. The asm-goto-output variant isn't actually compile tested,
but the old complicated thing is. Also, I'm >.< close to merging the
series that kills .fixup for x86, but the fixup (pun intended) should be
trivial.
Usage can be gleaned from the bigger patch I send you in reply to 0/ but
TL;DR:
if (!user_access_begin(uptr, sizeof(u64)))
return -EFAULT;
unsafe_get_user(old, uptr, Efault);
do {
new = func(old);
} while (!__try_cmpxchg_user(uptr, &old, new, Efault));
user_access_end();
return 0;
Efault:
user_access_end();
return -EFAULT;
Then if called within pagefault_disable(), it'll get -EFAULT more, if
called without it, it'll just take the fault and try to fix it up if at
all possible.
---
From: Peter Oskolkov <hidden> Date: 2021-11-24 16:29:02
On Wed, Nov 24, 2021 at 6:06 AM Peter Zijlstra [off-list ref] wrote:
On Mon, Nov 22, 2021 at 01:13:21PM -0800, Peter Oskolkov wrote:
quoted
User Managed Concurrency Groups (UMCG) is an M:N threading
subsystem/toolkit that lets user space application developers implement
in-process user space schedulers.
This v0.9.1 patchset is the same as v0.9, where u32/u64 in
uapi/linux/umcg.h are replaced with __u32/__u64, as test robot/lkp
does not recognize u32/u64 for some reason.
v0.9 is v0.8 rebased on top of the current tip/sched/core,
with a fix in umcg_update_state of an issue reported by Tao Zhou.
Key changes from patchset v0.7:
https://lore.kernel.org/all/20211012232522.714898-1-posk@google.com/:
- added libumcg tools/lib/umcg;
- worker "wakeup" is reworked so that it is now purely a userspace op,
instead of waking the thread in order for it to block on return
to the userspace immediately;
- a couple of minor fixes and refactorings.
These big things remain to be addressed (in no particular order):
- support tracing/debugging
- make context switches faster (see umcg_do_context_switch in umcg.c)
- support other architectures
- cleanup and post selftests in tools/testing/selftests/umcg/
- allow cross-mm wakeups (securely)
*groan*... so these patches do *NOT* support the very thing this all
started with, namely block + wakeup notifications. I'm really not sure
how that happened, as that was the sole purpose of the exercise.
I'm not sure why you say this - in-process block/wakeup is very much
supported - please see the third patch. Cross-process (cross-mm)
wakeups are not supported at the moment, as the security story has to
be fleshed out.
Aside of that, the whole uaccess stuff is horrific :-( I'll reply to
that email separately, but the alternative is also included in the
random hackery below.
Thanks - I'll try to make uaccess more to your liking, unless you say
the whole thing is a no-go.
I'm still trying to make sense of it all, but I'm really not seeing how
any of this satisfies the initial goals, also it is once again 100% new
code :/
I believe the initial goals of in-process block/wakeup detection,
on-cpu context switching, etc. are all achieved here. Re: new code:
the code in the third patch evolved into what it is today based on
feedback/discussions in this list.
[...]
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 17:20:57
On Wed, Nov 24, 2021 at 08:28:43AM -0800, Peter Oskolkov wrote:
On Wed, Nov 24, 2021 at 6:06 AM Peter Zijlstra [off-list ref] wrote:
quoted
On Mon, Nov 22, 2021 at 01:13:21PM -0800, Peter Oskolkov wrote:
quoted
User Managed Concurrency Groups (UMCG) is an M:N threading
subsystem/toolkit that lets user space application developers implement
in-process user space schedulers.
This v0.9.1 patchset is the same as v0.9, where u32/u64 in
uapi/linux/umcg.h are replaced with __u32/__u64, as test robot/lkp
does not recognize u32/u64 for some reason.
v0.9 is v0.8 rebased on top of the current tip/sched/core,
with a fix in umcg_update_state of an issue reported by Tao Zhou.
Key changes from patchset v0.7:
https://lore.kernel.org/all/20211012232522.714898-1-posk@google.com/:
- added libumcg tools/lib/umcg;
- worker "wakeup" is reworked so that it is now purely a userspace op,
instead of waking the thread in order for it to block on return
to the userspace immediately;
- a couple of minor fixes and refactorings.
These big things remain to be addressed (in no particular order):
- support tracing/debugging
- make context switches faster (see umcg_do_context_switch in umcg.c)
- support other architectures
- cleanup and post selftests in tools/testing/selftests/umcg/
- allow cross-mm wakeups (securely)
*groan*... so these patches do *NOT* support the very thing this all
started with, namely block + wakeup notifications. I'm really not sure
how that happened, as that was the sole purpose of the exercise.
I'm not sure why you say this - in-process block/wakeup is very much
supported - please see the third patch. Cross-process (cross-mm)
wakeups are not supported at the moment, as the security story has to
be fleshed out.
I seem to have gotten submit and update work confused. I'll go stare
more. For some reason I find it very hard to read this stuff.
From: kernel test robot <hidden> Date: 2021-11-24 18:37:59
Hi Peter,
Thank you for the patch! Perhaps something to improve:
[auto build test WARNING on cb0e52b7748737b2cf6481fdd9b920ce7e1ebbdf]
url: https://github.com/0day-ci/linux/commits/Peter-Oskolkov/sched-mm-x86-uaccess-implement-User-Managed-Concurrency-Groups/20211123-051525
base: cb0e52b7748737b2cf6481fdd9b920ce7e1ebbdf
config: arm64-randconfig-r031-20211124 (https://download.01.org/0day-ci/archive/20211125/202111250209.9dBNZjdP-lkp@intel.com/config)
compiler: clang version 14.0.0 (https://github.com/llvm/llvm-project 67a1c45def8a75061203461ab0060c75c864df1c)
reproduce (this is a W=1 build):
wget https://raw.githubusercontent.com/intel/lkp-tests/master/sbin/make.cross -O ~/bin/make.cross
chmod +x ~/bin/make.cross
# install arm64 cross compiling tool for clang build
# apt-get install binutils-aarch64-linux-gnu
# https://github.com/0day-ci/linux/commit/942655474fa2cd59ea3d11a1cc03775dd79a508e
git remote add linux-review https://github.com/0day-ci/linux
git fetch --no-tags linux-review Peter-Oskolkov/sched-mm-x86-uaccess-implement-User-Managed-Concurrency-Groups/20211123-051525
git checkout 942655474fa2cd59ea3d11a1cc03775dd79a508e
# save the config file to linux build tree
COMPILER_INSTALL_PATH=$HOME/0day COMPILER=clang make.cross W=1 ARCH=arm64
If you fix the issue, kindly add following tag as appropriate
Reported-by: kernel test robot <redacted>
All warnings (new ones prefixed by >>):
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:34:1: note: expanded from here
__arm64_sys_recvmsg
^
kernel/sys_ni.c:257:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:263:1: warning: no previous prototype for function '__arm64_sys_mremap' [-Wmissing-prototypes]
COND_SYSCALL(mremap);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:39:1: note: expanded from here
__arm64_sys_mremap
^
kernel/sys_ni.c:263:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:266:1: warning: no previous prototype for function '__arm64_sys_add_key' [-Wmissing-prototypes]
COND_SYSCALL(add_key);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:40:1: note: expanded from here
__arm64_sys_add_key
^
kernel/sys_ni.c:266:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:267:1: warning: no previous prototype for function '__arm64_sys_request_key' [-Wmissing-prototypes]
COND_SYSCALL(request_key);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:41:1: note: expanded from here
__arm64_sys_request_key
^
kernel/sys_ni.c:267:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:268:1: warning: no previous prototype for function '__arm64_sys_keyctl' [-Wmissing-prototypes]
COND_SYSCALL(keyctl);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:42:1: note: expanded from here
__arm64_sys_keyctl
^
kernel/sys_ni.c:268:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:272:1: warning: no previous prototype for function '__arm64_sys_landlock_create_ruleset' [-Wmissing-prototypes]
COND_SYSCALL(landlock_create_ruleset);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:47:1: note: expanded from here
__arm64_sys_landlock_create_ruleset
^
kernel/sys_ni.c:272:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:273:1: warning: no previous prototype for function '__arm64_sys_landlock_add_rule' [-Wmissing-prototypes]
COND_SYSCALL(landlock_add_rule);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:48:1: note: expanded from here
__arm64_sys_landlock_add_rule
^
kernel/sys_ni.c:273:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:274:1: warning: no previous prototype for function '__arm64_sys_landlock_restrict_self' [-Wmissing-prototypes]
COND_SYSCALL(landlock_restrict_self);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:49:1: note: expanded from here
__arm64_sys_landlock_restrict_self
^
kernel/sys_ni.c:274:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
quoted
kernel/sys_ni.c:277:1: warning: no previous prototype for function '__arm64_sys_umcg_ctl' [-Wmissing-prototypes]
COND_SYSCALL(umcg_ctl);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:50:1: note: expanded from here
__arm64_sys_umcg_ctl
^
kernel/sys_ni.c:277:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
quoted
kernel/sys_ni.c:278:1: warning: no previous prototype for function '__arm64_sys_umcg_wait' [-Wmissing-prototypes]
COND_SYSCALL(umcg_wait);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:51:1: note: expanded from here
__arm64_sys_umcg_wait
^
kernel/sys_ni.c:278:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:283:1: warning: no previous prototype for function '__arm64_sys_fadvise64_64' [-Wmissing-prototypes]
COND_SYSCALL(fadvise64_64);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:52:1: note: expanded from here
__arm64_sys_fadvise64_64
^
kernel/sys_ni.c:283:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:286:1: warning: no previous prototype for function '__arm64_sys_swapon' [-Wmissing-prototypes]
COND_SYSCALL(swapon);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:53:1: note: expanded from here
__arm64_sys_swapon
^
kernel/sys_ni.c:286:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:287:1: warning: no previous prototype for function '__arm64_sys_swapoff' [-Wmissing-prototypes]
COND_SYSCALL(swapoff);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:54:1: note: expanded from here
__arm64_sys_swapoff
^
kernel/sys_ni.c:287:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:288:1: warning: no previous prototype for function '__arm64_sys_mprotect' [-Wmissing-prototypes]
COND_SYSCALL(mprotect);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:55:1: note: expanded from here
__arm64_sys_mprotect
^
kernel/sys_ni.c:288:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:289:1: warning: no previous prototype for function '__arm64_sys_msync' [-Wmissing-prototypes]
COND_SYSCALL(msync);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:56:1: note: expanded from here
__arm64_sys_msync
^
kernel/sys_ni.c:289:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:290:1: warning: no previous prototype for function '__arm64_sys_mlock' [-Wmissing-prototypes]
COND_SYSCALL(mlock);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:57:1: note: expanded from here
__arm64_sys_mlock
^
kernel/sys_ni.c:290:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
arch/arm64/include/asm/syscall_wrapper.h:76:13: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
kernel/sys_ni.c:291:1: warning: no previous prototype for function '__arm64_sys_munlock' [-Wmissing-prototypes]
COND_SYSCALL(munlock);
^
arch/arm64/include/asm/syscall_wrapper.h:76:25: note: expanded from macro 'COND_SYSCALL'
asmlinkage long __weak __arm64_sys_##name(const struct pt_regs *regs) \
^
<scratch space>:58:1: note: expanded from here
__arm64_sys_munlock
^
kernel/sys_ni.c:291:1: note: declare 'static' if the function is not intended to be used outside of this translation unit
vim +/__arm64_sys_umcg_ctl +277 kernel/sys_ni.c
275
276 /* kernel/sched/umcg.c */
> 277 COND_SYSCALL(umcg_ctl);
> 278 COND_SYSCALL(umcg_wait);
279
---
0-DAY CI Kernel Test Service, Intel Corporation
https://lists.01.org/hyperkitty/list/kbuild-all@lists.01.org
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 20:08:42
On Mon, Nov 22, 2021 at 01:13:24PM -0800, Peter Oskolkov wrote:
+/**
+ * struct umcg_task - controls the state of UMCG tasks.
+ *
+ * The struct is aligned at 64 bytes to ensure that it fits into
+ * a single cache line.
+ */
+struct umcg_task {
+ /**
+ * @state_ts: the current state of the UMCG task described by
+ * this struct, with a unique timestamp indicating
+ * when the last state change happened.
+ *
+ * Readable/writable by both the kernel and the userspace.
+ *
+ * UMCG task state:
+ * bits 0 - 5: task state;
+ * bits 6 - 7: state flags;
+ * bits 8 - 12: reserved; must be zeroes;
+ * bits 13 - 17: for userspace use;
+ * bits 18 - 63: timestamp (see below).
+ *
+ * Timestamp: a 46-bit CLOCK_MONOTONIC timestamp, at 16ns resolution.
+ * See Documentation/userspace-api/umcg.txt for detals.
+ */
+ __u64 state_ts; /* r/w */
+
+ /**
+ * @next_tid: the TID of the UMCG task that should be context-switched
+ * into in sys_umcg_wait(). Can be zero.
+ *
+ * Running UMCG workers must have next_tid set to point to IDLE
+ * UMCG servers.
+ *
+ * Read-only for the kernel, read/write for the userspace.
+ */
+ __u32 next_tid; /* r */
+
+ __u32 flags; /* Reserved; must be zero. */
+
+ /**
+ * @idle_workers_ptr: a single-linked list of idle workers. Can be NULL.
+ *
+ * Readable/writable by both the kernel and the userspace: the
+ * kernel adds items to the list, the userspace removes them.
+ */
+ __u64 idle_workers_ptr; /* r/w */
+
+ /**
+ * @idle_server_tid_ptr: a pointer pointing to a single idle server.
+ * Readonly.
+ */
+ __u64 idle_server_tid_ptr; /* r */
+} __attribute__((packed, aligned(8 * sizeof(__u64))));
The thing is; I really don't see how this is supposed to be used. Where
did the blocked and runnable list go ?
I also don't see why the kernel cares about idle workers at all; that
seems something userspace can sort itself just fine.
The whole next_tid thing seems confused too, how can it be the next task
when it must be the server? Also, what if there isn't an idle server?
This just all isn't making any sense to me.
I'm still very hesitant to use ktime (fear the HPET); but I suppose it
makes sense to use a time base that's accessible to userspace. Was
MONOTONIC_RAW considered?
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 21:32:42
On Wed, Nov 24, 2021 at 09:08:23PM +0100, Peter Zijlstra wrote:
On Mon, Nov 22, 2021 at 01:13:24PM -0800, Peter Oskolkov wrote:
quoted
+/**
+ * struct umcg_task - controls the state of UMCG tasks.
+ *
+ * The struct is aligned at 64 bytes to ensure that it fits into
+ * a single cache line.
+ */
+struct umcg_task {
+ /**
+ * @state_ts: the current state of the UMCG task described by
+ * this struct, with a unique timestamp indicating
+ * when the last state change happened.
+ *
+ * Readable/writable by both the kernel and the userspace.
+ *
+ * UMCG task state:
+ * bits 0 - 5: task state;
+ * bits 6 - 7: state flags;
+ * bits 8 - 12: reserved; must be zeroes;
+ * bits 13 - 17: for userspace use;
+ * bits 18 - 63: timestamp (see below).
+ *
+ * Timestamp: a 46-bit CLOCK_MONOTONIC timestamp, at 16ns resolution.
+ * See Documentation/userspace-api/umcg.txt for detals.
+ */
+ __u64 state_ts; /* r/w */
+
+ /**
+ * @next_tid: the TID of the UMCG task that should be context-switched
+ * into in sys_umcg_wait(). Can be zero.
+ *
+ * Running UMCG workers must have next_tid set to point to IDLE
+ * UMCG servers.
+ *
+ * Read-only for the kernel, read/write for the userspace.
+ */
+ __u32 next_tid; /* r */
+
+ __u32 flags; /* Reserved; must be zero. */
+
+ /**
+ * @idle_workers_ptr: a single-linked list of idle workers. Can be NULL.
+ *
+ * Readable/writable by both the kernel and the userspace: the
+ * kernel adds items to the list, the userspace removes them.
+ */
+ __u64 idle_workers_ptr; /* r/w */
+
+ /**
+ * @idle_server_tid_ptr: a pointer pointing to a single idle server.
+ * Readonly.
+ */
+ __u64 idle_server_tid_ptr; /* r */
+} __attribute__((packed, aligned(8 * sizeof(__u64))));
The thing is; I really don't see how this is supposed to be used. Where
did the blocked and runnable list go ?
I also don't see why the kernel cares about idle workers at all; that
seems something userspace can sort itself just fine.
The whole next_tid thing seems confused too, how can it be the next task
when it must be the server? Also, what if there isn't an idle server?
This just all isn't making any sense to me.
Oooh, someone made things super confusing by doing s/runnable/idle/ on
the whole thing :-( That only took me most of the day to figure out.
Naming is important, don't mess about with stuff like this.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 21:42:07
On Mon, Nov 22, 2021 at 01:13:24PM -0800, Peter Oskolkov wrote:
+ while (true) {
(you have 2 inf. loops in umcg and you chose a different expression for each)
+ u64 umcg_state;
+
+ /*
+ * We need to read from userspace _after_ the task is marked
+ * TASK_INTERRUPTIBLE, to properly handle concurrent wakeups;
+ * but faulting is not allowed; so we try a fast no-fault read,
+ * and if it fails, pin the page temporarily.
+ */
That comment is misleading! Faulting *is* allowed, but it can scribble
__state. If faulting would not be allowed, you wouldn't be able to call
pin_user_pages_fast().
+retry_once:
+ set_current_state(TASK_INTERRUPTIBLE);
+
+ /* Order set_current_state above with get_user below. */
+ smp_mb();
And just in case you hadn't yet seen, that smp_mb() is implied by
set_current_state().
+ ret = -EFAULT;
+ if (get_user_nofault(umcg_state, &self->state_ts)) {
+ set_current_state(TASK_RUNNING);
+
+ if (pinned_page)
+ goto out;
+ else if (1 != pin_user_pages_fast((unsigned long)self,
+ 1, 0, &pinned_page))
That else is pointless, and that '1 != foo' coding style is evil.
+ goto out;
+
+ goto retry_once;
+ }
And, as you could've seen from the big patch, all that goto isn't
actually needed here, break / continue seem to be sufficient.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-24 21:58:46
On Mon, Nov 22, 2021 at 01:13:24PM -0800, Peter Oskolkov wrote:
+ if (abs_timeout) {
+ hrtimer_init_sleeper_on_stack(&timeout, CLOCK_REALTIME,
+ HRTIMER_MODE_ABS);
Using CLOCK_REALTIME timers while the rest of the thing runs off of
CLOCK_MONOTONIC doesn't seem to make sense to me. Why would you want to
have timeouts subject to DST shifts and crap like that?
That pr_warn() might need to be pr_warn_ratelimited() in order to no be
a system log DoS.
Because, AFAICT, you can craft userspace to trigger this arbitrarily
often, just spawn a worker and make it misbehave.
From: Peter Oskolkov <hidden> Date: 2021-11-25 17:43:31
Thanks, Peter, for the review!
Some of your comments, like ratelimiting pr_warn and removing gotos,
are obvious in how to address them, so I'll just do that and won't
mention them here. Some comments are less clear re: what should be
done about them, so I have them below with my own comments/questions.
At a higher level, I get that the uaccess patch is bad and needs
serious changes. But based on your comments on this main patch so far,
it looks like the overall approach did not raise many objections - is
it so? Have you finished reviewing the patch?
Please also look at my questions/comments below.
Thanks,
Peter
[...]
quoted
+struct umcg_task {
[...]
The thing is; I really don't see how this is supposed to be used. Where
did the blocked and runnable list go ?
I also don't see why the kernel cares about idle workers at all; that
seems something userspace can sort itself just fine.
The whole next_tid thing seems confused too, how can it be the next task
when it must be the server? Also, what if there isn't an idle server?
This just all isn't making any sense to me.
Based on your later comments I assume it is clearer now. The doc patch
5 has a lot of extra explanations and examples. Please let me know if
something is still unclear here.
I'm still very hesitant to use ktime (fear the HPET); but I suppose it
makes sense to use a time base that's accessible to userspace. Was
MONOTONIC_RAW considered?
I believe it was considered. I'll re-consider it, and add a comment if
the new consideration arrives at the same conclusion.
Using CLOCK_REALTIME timers while the rest of the thing runs off of
CLOCK_MONOTONIC doesn't seem to make sense to me. Why would you want to
have timeouts subject to DST shifts and crap like that?
Yes, these should be the same if at all possible. I'll definitely
reconsider what clock to use in both timeouts and state timestamps.
Oooh, someone made things super confusing by doing s/runnable/idle/ on
the whole thing :-( That only took me most of the day to figure out.
Naming is important, don't mess about with stuff like this.
I clearly remember I had four states: blocked, pending, runnable,
running (I still believe that four states better reflect what is going
on here). The current blocked/idle/running is the result of an early
discussion. Something along the lines of:
<start of a recollection>
pending workers (=unblocked workers that the userspace still thinks
are blocked) are better named as idle; also the kernel does not really
care about what userspace thinks, so idle workers and runnable workers
are the same from the kernel point of view, so let's have one state
for these workers, not two.
<end of the recollection>
Please let me know if you want me to change anything here. I'll gladly
name workers on the idle worker list as idle (or whatever you prefer),
and workers that the userspace took out of the list as "runnable".
Just as a FYI, workers blocked in umcg_wait() will also be called
"runnable" then, as they are sitting in umcg_idle_loop() and can be
woken or swapped into.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-26 17:32:36
On Thu, Nov 25, 2021 at 09:28:49AM -0800, Peter Oskolkov wrote:
it looks like the overall approach did not raise many objections - is
it so? Have you finished reviewing the patch?
I've been trying to make sense of it, and while doing so deleted a bunch
of things and rewrote the rest.
Things that went *poof*:
- wait_wake_only
- server_tid_ptr (now: server_tid)
- state_ts (now: state,blocked_ts,runnable_ts)
I've also changed next_tid to only be used as a context switch target,
never to find the server to enqueue the runnable tasks on.
All xchg() users seem to have disappeared.
Signals should now be handled, after which it'll go back to waiting on
RUNNING.
The code could fairly easily be changed to work on 32bit, big-endian is
the tricky bit, for now 64bit only.
Anyway, I only *think* the below code will work (it compiles with gcc-10
and gcc-11) but I've not yet come around to writing/updating the
userspace part, so it might explode on first contact -- I'll try that
next week if you don't beat me to it.
That said, the below code seems somewhat sensible to me (I would say,
having written it :), but I'm fairly sure I killed some capabilities the
other thing had (notably the first two items above).
If you want either of them restored, can you please give a use-case for
them? Because I cannot seem to think of any sane cases for either
wait_wake_only or server_tid_ptr.
Anyway, in large order it's very like what you did, but it's different
in pretty much all details.
Of note, it now has 5 hooks: sys_enter, pre-schedule, post-schedule
(still nop), sys_exit and notify_resume.
---
Subject: sched: User Mode Concurency Groups
From: Peter Zijlstra <peterz@infradead.org>
Date: Fri Nov 26 17:24:27 CET 2021
XXX split and changelog
Originally-by: Peter Oskolkov [off-list ref]
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
---
@@ -371,6 +371,8 @@447commonmemfd_secretsys_memfd_secret448commonprocess_mreleasesys_process_mrelease449commonfutex_waitvsys_futex_waitv+450commonumcg_ctlsys_umcg_ctl+451commonumcg_waitsys_umcg_wait## Due to a historical design error, certain syscalls are numbered differently---a/arch/x86/include/asm/thread_info.h+++b/arch/x86/include/asm/thread_info.h
@@ -83,6 +83,7 @@ struct thread_info {#define TIF_NEED_RESCHED 3 /* rescheduling necessary */#define TIF_SINGLESTEP 4 /* reenable singlestep on user return*/#define TIF_SSBD 5 /* Speculative store bypass disable */+#define TIF_UMCG 6 /* UMCG return to user hook */#define TIF_SPEC_IB 9 /* Indirect branch speculation mitigation */#define TIF_SPEC_L1D_FLUSH 10 /* Flush L1D on mm switches (processes) */#define TIF_USER_RETURN_NOTIFY 11 /* notify kernel of userspace return */
@@ -1687,6 +1697,13 @@ extern struct pid *cad_pid;#define PF_KTHREAD 0x00200000 /* I am a kernel thread */#define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */++#ifdef CONFIG_UMCG+#define PF_UMCG_WORKER 0x01000000 /* UMCG worker */+#else+#define PF_UMCG_WORKER 0x00000000+#endif+#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_mask */#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */#define PF_MEMALLOC_PIN 0x10000000 /* Allocation context constrained to zones which allow long term pinning. */
@@ -1693,6 +1693,21 @@ config MEMBARRIERIfunsure,sayY.+configHAVE_UMCG+bool++configUMCG+bool"Enable User Managed Concurrency Groups API"+depends on64BIT+depends onGENERIC_ENTRY+depends onHAVE_UMCG+defaultn+help+EnableUserManagedConcurrencyGroupsAPI,whichformthebasis+foranin-processM:Nuserspaceschedulingframework.+Atthemomentthisisanexperimental/RFCfeaturethatisnot+guaranteedtobebackward-compatible.+configKALLSYMSbool"Load all symbols for debugging/ksymoops"ifEXPERTdefaulty---a/kernel/entry/common.c+++b/kernel/entry/common.c
I'm still very hesitant to use ktime (fear the HPET); but I suppose it
makes sense to use a time base that's accessible to userspace. Was
MONOTONIC_RAW considered?
MONOTONIC_RAW is not really useful as you can't sleep on it and it won't
solve the HPET crap either.
Thanks,
tglx
From: Thomas Gleixner <hidden> Date: 2021-11-26 21:16:29
On Fri, Nov 26 2021 at 18:09, Peter Zijlstra wrote:
+
+ if (timo) {
+ hrtimer_init_sleeper_on_stack(&timeout, tsk->umcg_clock,
+ HRTIMER_MODE_ABS);
+ hrtimer_set_expires_range_ns(&timeout.timer, (s64)timo,
+ tsk->timer_slack_ns);
+ }
+
+ for (;;) {
+ set_current_state(TASK_INTERRUPTIBLE);
+
+ ret = -EINTR;
+ if (signal_pending(current))
+ break;
+
+ /*
+ * Faults can block and scribble our wait state.
+ */
+ pagefault_disable();
+ if (get_user(state, &self->state)) {
+ pagefault_enable();
+
+ ret = -EFAULT;
+ if (page) {
+ unpin_user_page(page);
+ page = NULL;
+ break;
+ }
+
+ if (pin_user_pages_fast((unsigned long)self, 1, 0, &page) != 1) {
+ page = NULL;
+ break;
+ }
+
+ continue;
+ }
+
+ if (page) {
+ unpin_user_page(page);
+ page = NULL;
+ }
+ pagefault_enable();
+
+ state &= UMCG_TASK_MASK;
+ if (state != UMCG_TASK_RUNNABLE) {
+ ret = 0;
+ if (state == UMCG_TASK_RUNNING)
+ break;
+
+ ret = -EINVAL;
+ break;
+ }
+
+ if (timo)
+ hrtimer_sleeper_start_expires(&timeout, HRTIMER_MODE_ABS);
+
+ freezable_schedule();
You can replace the whole hrtimer foo with
if (!schedule_hrtimeout_range_clock(timo ? &timo : NULL,
tsk->timer_slack_ns,
HRTIMER_MODE_ABS,
tsk->umcg_clock)) {
ret = -ETIMEOUT;
break;
}
Thanks,
tglx
I'm still very hesitant to use ktime (fear the HPET); but I suppose it
makes sense to use a time base that's accessible to userspace. Was
MONOTONIC_RAW considered?
MONOTONIC_RAW is not really useful as you can't sleep on it and it won't
solve the HPET crap either.
But it's ns are of equal size to sched_clock(), if both share TSC IIRC.
Whereas MONOTONIC, being subject to ntp rate stuff, has differently
sized ns.
The only time that's relevant though is when you're going to mix these
timestamps with CLOCK_THREAD_CPUTIME_ID, which might just be
interesting.
But yeah, not being able to sleep on it ruins the party.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-26 22:02:56
On Fri, Nov 26, 2021 at 10:08:14PM +0100, Thomas Gleixner wrote:
On Fri, Nov 26 2021 at 18:09, Peter Zijlstra wrote:
quoted
+
+ if (timo) {
+ hrtimer_init_sleeper_on_stack(&timeout, tsk->umcg_clock,
+ HRTIMER_MODE_ABS);
+ hrtimer_set_expires_range_ns(&timeout.timer, (s64)timo,
+ tsk->timer_slack_ns);
+ }
+
+ for (;;) {
+ set_current_state(TASK_INTERRUPTIBLE);
+
+ ret = -EINTR;
+ if (signal_pending(current))
+ break;
+
+ /*
+ * Faults can block and scribble our wait state.
+ */
+ pagefault_disable();
+ if (get_user(state, &self->state)) {
+ pagefault_enable();
+
+ ret = -EFAULT;
+ if (page) {
+ unpin_user_page(page);
+ page = NULL;
+ break;
+ }
+
+ if (pin_user_pages_fast((unsigned long)self, 1, 0, &page) != 1) {
+ page = NULL;
+ break;
+ }
+
+ continue;
+ }
+
+ if (page) {
+ unpin_user_page(page);
+ page = NULL;
+ }
+ pagefault_enable();
+
+ state &= UMCG_TASK_MASK;
+ if (state != UMCG_TASK_RUNNABLE) {
+ ret = 0;
+ if (state == UMCG_TASK_RUNNING)
+ break;
+
+ ret = -EINVAL;
+ break;
+ }
+
+ if (timo)
+ hrtimer_sleeper_start_expires(&timeout, HRTIMER_MODE_ABS);
+
+ freezable_schedule();
You can replace the whole hrtimer foo with
if (!schedule_hrtimeout_range_clock(timo ? &timo : NULL,
tsk->timer_slack_ns,
HRTIMER_MODE_ABS,
tsk->umcg_clock)) {
ret = -ETIMEOUT;
break;
}
That seems to loose the freezable crud.. then again, since we're
interruptible, that shouldn't matter. Lemme go do that.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-26 22:19:02
On Fri, Nov 26, 2021 at 06:09:10PM +0100, Peter Zijlstra wrote:
quoted hunk
@@ -155,8 +159,7 @@ static unsigned long exit_to_user_mode_l * Before returning to user space ensure that all pending work * items have been completed. */- while (ti_work & EXIT_TO_USER_MODE_WORK) {-+ do { local_irq_enable_exit_to_user(ti_work); if (ti_work & _TIF_NEED_RESCHED)
@@ -168,6 +171,10 @@ static unsigned long exit_to_user_mode_l if (ti_work & _TIF_PATCH_PENDING) klp_update_patch_state(current);+ /* must be before handle_signal_work(); terminates on sigpending */+ if (ti_work & _TIF_UMCG)+ umcg_notify_resume(regs);+ if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL)) handle_signal_work(regs, ti_work);
@@ -188,7 +195,7 @@ static unsigned long exit_to_user_mode_l tick_nohz_user_enter_prepare(); ti_work = READ_ONCE(current_thread_info()->flags);- }+ } while (ti_work & EXIT_TO_USER_MODE_WORK); /* Return the latest work state for arch_exit_to_user_mode() */ return ti_work;
@@ -203,7 +210,7 @@ static void exit_to_user_mode_prepare(st /* Flush pending rcuog wakeup before the last need_resched() check */ tick_nohz_user_enter_prepare();- if (unlikely(ti_work & EXIT_TO_USER_MODE_WORK))+ if (unlikely(ti_work & (EXIT_TO_USER_MODE_WORK | _TIF_UMCG))) ti_work = exit_to_user_mode_loop(regs, ti_work); arch_exit_to_user_mode_prepare(regs, ti_work);
Thomas, since you're looking at this. I'm not quite sure I got this
right. The intent is that when _TIF_UMCG is set (and it is never cleared
until the task unregisters) it is called at least once.
The thinking is that if umcg_wait() gets interrupted, we'll drop out,
handle the signal and then resume the wait, which can obviously happen
any number of times.
It's just that I'm never quite sure where signal crud happens; I'm
assuming handle_signal_work() simply mucks about with regs (sets sp and
ip etc.. to the signal stack) and drops out of kernel mode, and on
re-entry we do this whole merry cycle once again. But I never actually
dug that deep.
From: Thomas Gleixner <hidden> Date: 2021-11-27 00:47:24
On Fri, Nov 26 2021 at 22:59, Peter Zijlstra wrote:
On Fri, Nov 26, 2021 at 10:08:14PM +0100, Thomas Gleixner wrote:
quoted
quoted
+ if (timo)
+ hrtimer_sleeper_start_expires(&timeout, HRTIMER_MODE_ABS);
+
+ freezable_schedule();
You can replace the whole hrtimer foo with
if (!schedule_hrtimeout_range_clock(timo ? &timo : NULL,
tsk->timer_slack_ns,
HRTIMER_MODE_ABS,
tsk->umcg_clock)) {
ret = -ETIMEOUT;
break;
}
That seems to loose the freezable crud.. then again, since we're
interruptible, that shouldn't matter. Lemme go do that.
We could add a freezable wrapper for that if necessary.
Thanks,
tglx
From: Thomas Gleixner <hidden> Date: 2021-11-27 01:18:49
On Fri, Nov 26 2021 at 23:16, Peter Zijlstra wrote:
On Fri, Nov 26, 2021 at 06:09:10PM +0100, Peter Zijlstra wrote:
quoted
- if (unlikely(ti_work & EXIT_TO_USER_MODE_WORK))
+ if (unlikely(ti_work & (EXIT_TO_USER_MODE_WORK | _TIF_UMCG)))
ti_work = exit_to_user_mode_loop(regs, ti_work);
arch_exit_to_user_mode_prepare(regs, ti_work);
Thomas, since you're looking at this. I'm not quite sure I got this
right. The intent is that when _TIF_UMCG is set (and it is never cleared
until the task unregisters) it is called at least once.
Right.
The thinking is that if umcg_wait() gets interrupted, we'll drop out,
handle the signal and then resume the wait, which can obviously happen
any number of times.
Right.
It's just that I'm never quite sure where signal crud happens; I'm
assuming handle_signal_work() simply mucks about with regs (sets sp and
ip etc.. to the signal stack) and drops out of kernel mode, and on
re-entry we do this whole merry cycle once again. But I never actually
dug that deep.
Yes. It sets up the signal frame and once the loop is left because there
are no more TIF flags to handle it drops back to user space into the
signal handler. That returns to the kernel via sys_[rt_]sigreturn()
which undoes the regs damage either by restoring the previous state or
fiddling it to restart the syscall instead of dropping back to user
space.
So yes, this should work, but I hate the sticky nature of TIF_UMCG. I
have no real good idea how to avoid that yet, but let me think about it
some more.
Thanks,
tglx
From: Peter Oskolkov <hidden> Date: 2021-11-29 00:32:57
On Fri, Nov 26, 2021 at 9:09 AM Peter Zijlstra [off-list ref] wrote:
On Thu, Nov 25, 2021 at 09:28:49AM -0800, Peter Oskolkov wrote:
quoted
it looks like the overall approach did not raise many objections - is
it so? Have you finished reviewing the patch?
I've been trying to make sense of it, and while doing so deleted a bunch
of things and rewrote the rest.
Thanks a lot, Peter! If we can get this in, and work the kinks out
later, that would be great!
Things that went *poof*:
- wait_wake_only
- server_tid_ptr (now: server_tid)
- state_ts (now: state,blocked_ts,runnable_ts)
I've also changed next_tid to only be used as a context switch target,
never to find the server to enqueue the runnable tasks on.
All xchg() users seem to have disappeared.
Signals should now be handled, after which it'll go back to waiting on
RUNNING.
The code could fairly easily be changed to work on 32bit, big-endian is
the tricky bit, for now 64bit only.
Anyway, I only *think* the below code will work (it compiles with gcc-10
and gcc-11) but I've not yet come around to writing/updating the
userspace part, so it might explode on first contact -- I'll try that
next week if you don't beat me to it.
I'll take me some time to fully test this (got some other stuff to
look at at the moment); some notes are below. I'd prefer you to merge
whatever you believe is working, and to later adjust things that need
adjusting, rather than keep the endless stream of patchsets that go
nowhere.
That said, the below code seems somewhat sensible to me (I would say,
having written it :), but I'm fairly sure I killed some capabilities the
other thing had (notably the first two items above).
If you want either of them restored, can you please give a use-case for
them? Because I cannot seem to think of any sane cases for either
wait_wake_only or server_tid_ptr.
wait_wake_only is not needed if you have both next_tid and server_tid,
as your patch has. In my version of the patch, next_tid is the same as
server_tid, so the flag is needed to indicate to the kernel that
next_tid is the wakee, not the server.
re: (idle_)server_tid_ptr: it seems that you assume that blocked
workers keep their servers, while in my patch they "lose them" once
they block, and so there should be a global idle server pointer to
wake the server in my scheme (if there is an idle one). The main
difference is that in my approach a server has only a single, running,
worker assigned to it, while in your approach it can have a number of
blocked/idle workers to take care of as well.
The main difference between our approaches, as I see it: in my
approach if a worker is running, its server is sleeping, period. If we
have N servers, and N running workers, there are no servers to wake
when a previously blocked worker finishes its blocking op. In your
approach, it seems that N servers have each a bunch of workers
pointing at them, and a single worker running. If a previously blocked
worker wakes up, it wakes the server it was assigned to previously,
and so now we have more than N physical tasks/threads running: N
workers and the woken server. This is not ideal: if the process is
affined to only N CPUs, that means a worker will be preempted to let
the woken server run, which is somewhat against the goal of letting
the workers run more or less uninterrupted. This is not deal breaking,
but maybe something to keep in mind.
Another big concern I have is that you removed UMCG_TF_LOCKED. I
definitely needed it to guard workers during "sched work" in the
userspace in my approach. I'm not sure if the flag is absolutely
needed with your approach, but most likely it is - the kernel-side
scheduler does lock tasks and runqueues and disables interrupts and
migrations and other things so that the scheduling logic is not
hijacked by concurrent stuff. Why do you assume that the userspace
scheduling code does not need similar protections?
In summary, again, I'm fine with your patch/approach getting in,
provided things like UMCG_TF_LOCKED are considered later.
quoted hunk
Anyway, in large order it's very like what you did, but it's different
in pretty much all details.
Of note, it now has 5 hooks: sys_enter, pre-schedule, post-schedule
(still nop), sys_exit and notify_resume.
---
Subject: sched: User Mode Concurency Groups
From: Peter Zijlstra <peterz@infradead.org>
Date: Fri Nov 26 17:24:27 CET 2021
XXX split and changelog
Originally-by: Peter Oskolkov [off-list ref]
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
---
@@ -371,6 +371,8 @@447commonmemfd_secretsys_memfd_secret448commonprocess_mreleasesys_process_mrelease449commonfutex_waitvsys_futex_waitv+450commonumcg_ctlsys_umcg_ctl+451commonumcg_waitsys_umcg_wait## Due to a historical design error, certain syscalls are numbered differently---a/arch/x86/include/asm/thread_info.h+++b/arch/x86/include/asm/thread_info.h
@@ -83,6 +83,7 @@ struct thread_info {#define TIF_NEED_RESCHED 3 /* rescheduling necessary */#define TIF_SINGLESTEP 4 /* reenable singlestep on user return*/#define TIF_SSBD 5 /* Speculative store bypass disable */+#define TIF_UMCG 6 /* UMCG return to user hook */#define TIF_SPEC_IB 9 /* Indirect branch speculation mitigation */#define TIF_SPEC_L1D_FLUSH 10 /* Flush L1D on mm switches (processes) */#define TIF_USER_RETURN_NOTIFY 11 /* notify kernel of userspace return */
@@ -1687,6 +1697,13 @@ extern struct pid *cad_pid;#define PF_KTHREAD 0x00200000 /* I am a kernel thread */#define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */++#ifdef CONFIG_UMCG+#define PF_UMCG_WORKER 0x01000000 /* UMCG worker */+#else+#define PF_UMCG_WORKER 0x00000000+#endif+#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_mask */#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */#define PF_MEMALLOC_PIN 0x10000000 /* Allocation context constrained to zones which allow long term pinning. */
@@ -1693,6 +1693,21 @@ config MEMBARRIERIfunsure,sayY.+configHAVE_UMCG+bool++configUMCG+bool"Enable User Managed Concurrency Groups API"+depends on64BIT+depends onGENERIC_ENTRY+depends onHAVE_UMCG+defaultn+help+EnableUserManagedConcurrencyGroupsAPI,whichformthebasis+foranin-processM:Nuserspaceschedulingframework.+Atthemomentthisisanexperimental/RFCfeaturethatisnot+guaranteedtobebackward-compatible.+configKALLSYMSbool"Load all symbols for debugging/ksymoops"ifEXPERTdefaulty---a/kernel/entry/common.c+++b/kernel/entry/common.c
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 18:50:28
On Sat, Nov 27, 2021 at 01:45:20AM +0100, Thomas Gleixner wrote:
On Fri, Nov 26 2021 at 22:59, Peter Zijlstra wrote:
quoted
On Fri, Nov 26, 2021 at 10:08:14PM +0100, Thomas Gleixner wrote:
quoted
quoted
+ if (timo)
+ hrtimer_sleeper_start_expires(&timeout, HRTIMER_MODE_ABS);
+
+ freezable_schedule();
You can replace the whole hrtimer foo with
if (!schedule_hrtimeout_range_clock(timo ? &timo : NULL,
tsk->timer_slack_ns,
HRTIMER_MODE_ABS,
tsk->umcg_clock)) {
ret = -ETIMEOUT;
break;
}
That seems to loose the freezable crud.. then again, since we're
interruptible, that shouldn't matter. Lemme go do that.
We could add a freezable wrapper for that if necessary.
I should just finish rewriting that freezer crap and then we can delete
it all :-) But I don't think that's needed in this case, as long as
we're interruptible we'll pass through the signal path which has a
try_to_freezer() in it.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 18:50:51
On Sat, Nov 27, 2021 at 02:16:43AM +0100, Thomas Gleixner wrote:
So yes, this should work, but I hate the sticky nature of TIF_UMCG. I
have no real good idea how to avoid that yet, but let me think about it
some more.
Yeah, that, I couldn't come up with anything saner either.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 20:07:10
On Sun, Nov 28, 2021 at 04:29:11PM -0800, Peter Oskolkov wrote:
wait_wake_only is not needed if you have both next_tid and server_tid,
as your patch has. In my version of the patch, next_tid is the same as
server_tid, so the flag is needed to indicate to the kernel that
next_tid is the wakee, not the server.
Ah, okay.
re: (idle_)server_tid_ptr: it seems that you assume that blocked
workers keep their servers, while in my patch they "lose them" once
they block, and so there should be a global idle server pointer to
wake the server in my scheme (if there is an idle one). The main
difference is that in my approach a server has only a single, running,
worker assigned to it, while in your approach it can have a number of
blocked/idle workers to take care of as well.
Correct; I've been thinking in analogues of the way we schedule CPUs.
Each CPU has a ready/run queue along with the current task.
fundamentally the RUNNABLE tasks need to go somewhere when all servers
are busy. So at that point the previous server is as good a place as
any.
Now, I sympathise with a blocked task not having a relation; I often
argue this same, since we have wakeup balancing etc. And I've not really
thought about how to best do wakeup-balancing, also see below.
The main difference between our approaches, as I see it: in my
approach if a worker is running, its server is sleeping, period. If we
have N servers, and N running workers, there are no servers to wake
when a previously blocked worker finishes its blocking op. In your
approach, it seems that N servers have each a bunch of workers
pointing at them, and a single worker running. If a previously blocked
worker wakes up, it wakes the server it was assigned to previously,
Right; it does that. It can check the ::state of it's current task,
possibly set TF_PREEMPT or just go back to sleep.
and so now we have more than N physical tasks/threads running: N
workers and the woken server. This is not ideal: if the process is
affined to only N CPUs, that means a worker will be preempted to let
the woken server run, which is somewhat against the goal of letting
the workers run more or less uninterrupted. This is not deal breaking,
but maybe something to keep in mind.
I suppose it's easy enough to make this behaviour configurable though;
simply enqueue and not wake.... Hmm.. how would this worker know if the
server was 'busy' or not? The whole 'current' thing is a user-space
construct. I suppose that's what your pointer was for? Puts an actual
idle server in there, if there is one. Let me ponder that a bit.
However, do note this whole scheme fundamentally has some of that, the
moment the syscall unblocks until sys_exit is 'unmanaged' runtime for
all tasks, they can consume however much time the syscall needs there.
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
Another big concern I have is that you removed UMCG_TF_LOCKED. I
OOh yes, I forgot to mention that. I couldn't figure out what it was
supposed to do.
definitely needed it to guard workers during "sched work" in the
userspace in my approach. I'm not sure if the flag is absolutely
needed with your approach, but most likely it is - the kernel-side
scheduler does lock tasks and runqueues and disables interrupts and
migrations and other things so that the scheduling logic is not
hijacked by concurrent stuff. Why do you assume that the userspace
scheduling code does not need similar protections?
I've not yet come across a case where this is needed. Migration for
instance is possible when RUNNABLE, simply write ::server_tid before
::state. Userspace just needs to make sure who actually owns the task,
but it can do that outside of this state.
But like I said; I've not yet done the userspace part (and I lost most
of today trying to install a new machine), so perhaps I'll run into it
soon enough.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 22:29:19
On Mon, Nov 29, 2021 at 11:07:07PM +0100, Thomas Gleixner wrote:
On Fri, Nov 26 2021 at 22:52, Peter Zijlstra wrote:
The size is the same, i.e. 1 bit per nanosecond :)
:-)
quoted
The only time that's relevant though is when you're going to mix these
timestamps with CLOCK_THREAD_CPUTIME_ID, which might just be
interesting.
Uuurg. If you want to go towards CLOCK_THREAD_CPUTIME_ID, that's going
to be really nasty. Actually you can sleep on that clock, but that's a
completely different universe. If anything like that is desired then we
need to rewrite that posix CPU timer muck completely with all the bells
and whistels and race conditions attached to it. *Shudder*
Oh, I wasn't thinking anything as terrible as that. Sleeping on that
clock is fundamentally daft since it doesn't run when thats is
sleeping, consider trying to sleep on your own runtime :-)
I was only considering combining THREAD_CPUTIME timestamps with the
UMCG timestamps to compute how much unmanaged time there was, or other
such things.
Anyway, lets forget I bought this up and assume that for practical
purposes all [ns] are of equal length.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 22:41:18
On Mon, Nov 29, 2021 at 10:08:41PM +0100, Peter Zijlstra wrote:
I'm not sure I'm following. For this to be true A and C must be running
on a different server right?
So we have something like:
S0 running A S1 running B
Therefore:
S0::state == RUNNABLE S1::state == RUNNABLE
A::server_tid == S0.tid B::server_tid == S1.tid
A::state == RUNNING B::state == RUNNING
Now, you want A to switch to C, therefore C had better be with S0, eg we
have:
C::server_tid == S0.tid
C::state == RUNNABLE
So then A does:
A::next_tid = C.tid;
sys_umcg_wait();
Which will:
pin(A);
pin(S0);
cmpxchg(A::state, RUNNING, RUNNABLE);
next_tid = A::next_tid; // C
enqueue(S0::runnable, A);
At which point B steals S0's runnable queue, and tries to make A go.
runnable = xchg(S0::runnable_list_ptr, NULL); // == A
A::server_tid = S1.tid;
B::next_tid = A.tid;
sys_umcg_wait();
wake(C)
cmpxchg(C::state, RUNNABLE, RUNNING); <-- *fault*
Something like that, right?
And note that there's an XXX in the code about exactly this case; it has
a question whether we want to add pin(next) to umcg_pin_pages().
That would not in fact help here, because sys_umcg_wait() is faultable
and the only reason it'll return -EFAULT is because, as stated below, C
is garbage. But it does make a difference for when we do something like:
self->next_tid = someone;
sys_something_we_expect_to_block();
// handle not blocking
Because in that case userspace must have taken 'someone' from the
runnable queue and made it 'next', but then we'll not wake next but the
server, which then needs to figure out something went sideways.
So I'm tempted to add that optional 3rd pin, simply to reduce the
failure cases.
What currently happens is that S0 goes back to S0 and S1 ends up in A.
That is, if, for any reason we fail to wake next_tid, we'll wake
server_tid.
So then S0 wakes up and gets to re-evaluate life. If it has another
worker it can go run that, otherwise it can try and steal a worker
somewhere or just idle out.
Now arguably, the only reason A->C can fault is because C is garbage, at
which point your program is malformed and it doesn't matter what
happens one way or the other.
From: Peter Oskolkov <hidden> Date: 2021-11-29 22:43:41
On Mon, Nov 29, 2021 at 8:41 AM Peter Zijlstra [off-list ref] wrote:
On Sun, Nov 28, 2021 at 04:29:11PM -0800, Peter Oskolkov wrote:
quoted
wait_wake_only is not needed if you have both next_tid and server_tid,
as your patch has. In my version of the patch, next_tid is the same as
server_tid, so the flag is needed to indicate to the kernel that
next_tid is the wakee, not the server.
Ah, okay.
quoted
re: (idle_)server_tid_ptr: it seems that you assume that blocked
workers keep their servers, while in my patch they "lose them" once
they block, and so there should be a global idle server pointer to
wake the server in my scheme (if there is an idle one). The main
difference is that in my approach a server has only a single, running,
worker assigned to it, while in your approach it can have a number of
blocked/idle workers to take care of as well.
Correct; I've been thinking in analogues of the way we schedule CPUs.
Each CPU has a ready/run queue along with the current task.
fundamentally the RUNNABLE tasks need to go somewhere when all servers
are busy. So at that point the previous server is as good a place as
any.
Now, I sympathise with a blocked task not having a relation; I often
argue this same, since we have wakeup balancing etc. And I've not really
thought about how to best do wakeup-balancing, also see below.
quoted
The main difference between our approaches, as I see it: in my
approach if a worker is running, its server is sleeping, period. If we
have N servers, and N running workers, there are no servers to wake
when a previously blocked worker finishes its blocking op. In your
approach, it seems that N servers have each a bunch of workers
pointing at them, and a single worker running. If a previously blocked
worker wakes up, it wakes the server it was assigned to previously,
Right; it does that. It can check the ::state of it's current task,
possibly set TF_PREEMPT or just go back to sleep.
quoted
and so now we have more than N physical tasks/threads running: N
workers and the woken server. This is not ideal: if the process is
affined to only N CPUs, that means a worker will be preempted to let
the woken server run, which is somewhat against the goal of letting
the workers run more or less uninterrupted. This is not deal breaking,
but maybe something to keep in mind.
I suppose it's easy enough to make this behaviour configurable though;
simply enqueue and not wake.... Hmm.. how would this worker know if the
server was 'busy' or not? The whole 'current' thing is a user-space
construct. I suppose that's what your pointer was for? Puts an actual
idle server in there, if there is one. Let me ponder that a bit.
Yes, the idle_server_ptr was there to point to an idle server; this
naturally did wakeup balancing.
However, do note this whole scheme fundamentally has some of that, the
moment the syscall unblocks until sys_exit is 'unmanaged' runtime for
all tasks, they can consume however much time the syscall needs there.
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
It should not. Timed out workers should be added to the runnable list
and not become running unless a server chooses so. So sys_umcg_wait()
with a timeout should behave similarly to a normal sleep, in that the
server is woken upon the worker blocking, and upon the worker wakeup
the worker is added to the woken workers list and waits for a server
to run it. The only difference is that in a sleep the worker becomes
BLOCKED, while in sys_umcg_wait() the worker is RUNNABLE the whole
time.
Why then have sys_umcg_wait() with a timeout at all, instead of
calling nanosleep()? Because the worker in sys_umcg_wait() can be
context-switched into by another worker, or made running by a server;
if the worker is in nanosleep(), it just sleeps.
quoted
Another big concern I have is that you removed UMCG_TF_LOCKED. I
OOh yes, I forgot to mention that. I couldn't figure out what it was
supposed to do.
quoted
definitely needed it to guard workers during "sched work" in the
userspace in my approach. I'm not sure if the flag is absolutely
needed with your approach, but most likely it is - the kernel-side
scheduler does lock tasks and runqueues and disables interrupts and
migrations and other things so that the scheduling logic is not
hijacked by concurrent stuff. Why do you assume that the userspace
scheduling code does not need similar protections?
I've not yet come across a case where this is needed. Migration for
instance is possible when RUNNABLE, simply write ::server_tid before
::state. Userspace just needs to make sure who actually owns the task,
but it can do that outside of this state.
But like I said; I've not yet done the userspace part (and I lost most
of today trying to install a new machine), so perhaps I'll run into it
soon enough.
The most obvious scenario where I needed locking is when worker A
wants to context switch into worker B, while another worker C wants to
context switch into worker A, and worker A pagefaults. This involves:
worker A context: worker A context switches into worker B:
- worker B::server_tid = worker A::server_tid
- worker A::server_tid = none
- worker A::state = runnable
- worker B::state = running
- worker A::next_tid = worker B
- worker A calls sys_umcg_wait()
worker B context: before the above completes, worker C wants to
context switch into worker A, with similar steps.
"interrupt context": in the middle of the mess above, worker A pagefaults
Too many moving parts. UMCG_TF_LOCKED helped me make this mess
manageable. Maybe without pagefaults clever ordering of the operations
listed above could make things work, but pagefaults mess things badly,
so some kind of "preempt_disable()" for the userspace scheduling code
was needed, and UMCG_TF_LOCKED was the solution I had.
I'm still very hesitant to use ktime (fear the HPET); but I suppose it
makes sense to use a time base that's accessible to userspace. Was
MONOTONIC_RAW considered?
MONOTONIC_RAW is not really useful as you can't sleep on it and it won't
solve the HPET crap either.
But it's ns are of equal size to sched_clock(), if both share TSC IIRC.
Whereas MONOTONIC, being subject to ntp rate stuff, has differently
sized ns.
The size is the same, i.e. 1 bit per nanosecond :)
The only time that's relevant though is when you're going to mix these
timestamps with CLOCK_THREAD_CPUTIME_ID, which might just be
interesting.
Uuurg. If you want to go towards CLOCK_THREAD_CPUTIME_ID, that's going
to be really nasty. Actually you can sleep on that clock, but that's a
completely different universe. If anything like that is desired then we
need to rewrite that posix CPU timer muck completely with all the bells
and whistels and race conditions attached to it. *Shudder*
Thanks,
tglx
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-11-29 23:06:44
On Mon, Nov 29, 2021 at 09:34:49AM -0800, Peter Oskolkov wrote:
On Mon, Nov 29, 2021 at 8:41 AM Peter Zijlstra [off-list ref] wrote:
quoted
However, do note this whole scheme fundamentally has some of that, the
moment the syscall unblocks until sys_exit is 'unmanaged' runtime for
all tasks, they can consume however much time the syscall needs there.
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
It should not. Timed out workers should be added to the runnable list
and not become running unless a server chooses so. So sys_umcg_wait()
with a timeout should behave similarly to a normal sleep, in that the
server is woken upon the worker blocking, and upon the worker wakeup
the worker is added to the woken workers list and waits for a server
to run it. The only difference is that in a sleep the worker becomes
BLOCKED, while in sys_umcg_wait() the worker is RUNNABLE the whole
time.
OK, that's somewhat subtle and I hadn't gotten that either.
Currently it return -ETIMEDOUT in RUNNING state for both server and
worker callers.
Let me go fix that then.
quoted
quoted
Another big concern I have is that you removed UMCG_TF_LOCKED. I
OOh yes, I forgot to mention that. I couldn't figure out what it was
supposed to do.
quoted
definitely needed it to guard workers during "sched work" in the
userspace in my approach. I'm not sure if the flag is absolutely
needed with your approach, but most likely it is - the kernel-side
scheduler does lock tasks and runqueues and disables interrupts and
migrations and other things so that the scheduling logic is not
hijacked by concurrent stuff. Why do you assume that the userspace
scheduling code does not need similar protections?
I've not yet come across a case where this is needed. Migration for
instance is possible when RUNNABLE, simply write ::server_tid before
::state. Userspace just needs to make sure who actually owns the task,
but it can do that outside of this state.
But like I said; I've not yet done the userspace part (and I lost most
of today trying to install a new machine), so perhaps I'll run into it
soon enough.
The most obvious scenario where I needed locking is when worker A
wants to context switch into worker B, while another worker C wants to
context switch into worker A, and worker A pagefaults. This involves:
worker A context: worker A context switches into worker B:
- worker B::server_tid = worker A::server_tid
- worker A::server_tid = none
- worker A::state = runnable
- worker B::state = running
- worker A::next_tid = worker B
- worker A calls sys_umcg_wait()
worker B context: before the above completes, worker C wants to
context switch into worker A, with similar steps.
"interrupt context": in the middle of the mess above, worker A pagefaults
Too many moving parts. UMCG_TF_LOCKED helped me make this mess
manageable. Maybe without pagefaults clever ordering of the operations
listed above could make things work, but pagefaults mess things badly,
so some kind of "preempt_disable()" for the userspace scheduling code
was needed, and UMCG_TF_LOCKED was the solution I had.
I'm not sure I'm following. For this to be true A and C must be running
on a different server right?
So we have something like:
S0 running A S1 running B
Therefore:
S0::state == RUNNABLE S1::state == RUNNABLE
A::server_tid == S0.tid B::server_tid == S1.tid
A::state == RUNNING B::state == RUNNING
Now, you want A to switch to C, therefore C had better be with S0, eg we
have:
C::server_tid == S0.tid
C::state == RUNNABLE
So then A does:
A::next_tid = C.tid;
sys_umcg_wait();
Which will:
pin(A);
pin(S0);
cmpxchg(A::state, RUNNING, RUNNABLE);
next_tid = A::next_tid; // C
enqueue(S0::runnable, A);
At which point B steals S0's runnable queue, and tries to make A go.
runnable = xchg(S0::runnable_list_ptr, NULL); // == A
A::server_tid = S1.tid;
B::next_tid = A.tid;
sys_umcg_wait();
wake(C)
cmpxchg(C::state, RUNNABLE, RUNNING); <-- *fault*
Something like that, right?
What currently happens is that S0 goes back to S0 and S1 ends up in A.
That is, if, for any reason we fail to wake next_tid, we'll wake
server_tid.
So then S0 wakes up and gets to re-evaluate life. If it has another
worker it can go run that, otherwise it can try and steal a worker
somewhere or just idle out.
Now arguably, the only reason A->C can fault is because C is garbage, at
which point your program is malformed and it doesn't matter what
happens one way or the other.
From: Peter Oskolkov <hidden> Date: 2021-11-29 23:39:52
On Mon, Nov 29, 2021 at 1:08 PM Peter Zijlstra [off-list ref] wrote:
[...]
quoted
quoted
quoted
Another big concern I have is that you removed UMCG_TF_LOCKED. I
OOh yes, I forgot to mention that. I couldn't figure out what it was
supposed to do.
[...]
So then A does:
A::next_tid = C.tid;
sys_umcg_wait();
Which will:
pin(A);
pin(S0);
cmpxchg(A::state, RUNNING, RUNNABLE);
Hmm.... That's another difference between your patch and mine: my
approach was "the side that initiates the change updates the state".
So in my code the userspace changes the current task's state RUNNING
=> RUNNABLE and the next task's state, or the server's state, RUNNABLE
=> RUNNING before calling sys_umcg_wait(). The kernel changed worker
states to BLOCKED/RUNNABLE during block/wake detection, and marked
servers RUNNING when waking them during block/wake detection; but all
applicable state changes for sys_umcg_wait() happen in the userspace.
The reasoning behind this approach was:
- do in kernel only that which cannot be done in the userspace, to
make the kernel code smaller/simpler
- similar to how futexes work: futex_wait does not change the futex
value to the desired value, but just checks whether the futex value
matches the desired value
- similar to how futexes work, concurrent state changes can happen in
the userspace without calling into the kernel at all
for example:
- (a): worker A goes to sleep into sys_umcg_wait()
- (b): worker B wants to context switch into worker A "a moment" later
- due to preemption/interrupts/pagefaults/whatnot, (b) happens
in reality before (a)
in my patchset, the situation above happily resolves in the
userspace so that worker A keeps running without ever calling
sys_umcg_wait().
Again, I don't think this is deal breaking, and your approach will
work, just a bit less efficiently in some cases :)
I'm still not sure we can live without UMCG_TF_LOCKED. What if worker
A transfers its server to worker B that A intends to context switch
into, and then worker A pagefaults or gets interrupted before calling
sys_umcg_wait()? The server will be woken up and will see that it is
assigned to worker B; now what? If worker A is "locked" before the
whole thing starts, the pagefault/interrupt will not trigger
block/wake detection, worker A will keep RUNNING for all intended
purposes, and eventually will call sys_umcg_wait() as it had
intended...
[...]
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-12-06 11:33:10
Sorry, I haven't been feeling too well and as such procastinated on this
because thinking is required :/ Trying to pick up the bits.
On Mon, Nov 29, 2021 at 03:38:38PM -0800, Peter Oskolkov wrote:
On Mon, Nov 29, 2021 at 1:08 PM Peter Zijlstra [off-list ref] wrote:
[...]
quoted
quoted
quoted
quoted
Another big concern I have is that you removed UMCG_TF_LOCKED. I
OOh yes, I forgot to mention that. I couldn't figure out what it was
supposed to do.
[...]
quoted
So then A does:
A::next_tid = C.tid;
sys_umcg_wait();
Which will:
pin(A);
pin(S0);
cmpxchg(A::state, RUNNING, RUNNABLE);
Hmm.... That's another difference between your patch and mine: my
approach was "the side that initiates the change updates the state".
So in my code the userspace changes the current task's state RUNNING
=> RUNNABLE and the next task's state,
I couldn't make that work for wakeups; when a thread blocks in a
random syscall there is no userspace to wake the next thread. And since
it seems required in this case, it's easier and more consistent to
always do it.
or the server's state, RUNNABLE
=> RUNNING before calling sys_umcg_wait().
Yes, this is indeed required; I've found the same when trying to build
the userspace server loop. And yes, I'm starting to see where you're
coming from.
I'm still not sure we can live without UMCG_TF_LOCKED. What if worker
A transfers its server to worker B that A intends to context switch
S0 running A
Therefore:
S0::state == RUNNABLE
A::server_tid = S0.tid
A::state == RUNNING
you want A to switch to B, therefore:
B::state == RUNNABLE
if B is not yet on S0 then:
B::server_tid = S0.tid;
finally:
0:
A::next_tid = B.tid;
1:
A::state = RUNNABLE:
2:
sys_umcg_wait();
3:
into, and then worker A pagefaults or gets interrupted before calling
sys_umcg_wait()?
So the problem is tripping umcg_notify_resume() on the labels 1 and 2,
right? tripping it on 0 and 3 is trivially correct.
If we trip it on 1 and !(A::state & TG_PREEMPT), then nothing, since
::state == RUNNING we'll just continue onwards and all is well. That is,
nothing has happened yet.
However, if we trip it on 2: we're screwed. Because at that point
::state is scribbled.
The server will be woken up and will see that it is
assigned to worker B; now what? If worker A is "locked" before the
whole thing starts, the pagefault/interrupt will not trigger
block/wake detection, worker A will keep RUNNING for all intended
purposes, and eventually will call sys_umcg_wait() as it had
intended...
No, the failure case is different; umcg_notify_resume() will simply
block A until someone sets A::state == RUNNING and kicks it, which will
be no-one.
Now, the above situation is actually simple to fix, but it gets more
interesting when we're using sys_umcg_wait() to build wait primitives.
Because in that case we get stuff like:
for (;;) {
self->state = RUNNABLE;
smp_mb();
if (cond)
break;
sys_umcg_wait();
}
self->state = RUNNING;
And we really need to not block and also not do sys_umcg_wait() early.
So yes, I agree that we need a special case here that ensures
umcg_notify_resume() doesn't block. Let me ponder naming and comments.
Either a TF_COND_WAIT or a whole new state. I can't decide yet.
Now, obviously if you do a random syscall anywhere around here, you get
to keep the pieces :-)
I've also added ::next_tid to the whole umcg_pin_pages() thing, and made
it so that ::next_tid gets cleared when it's been used. That way things
like:
self->next_tid = pick_from_runqueue();
sys_that_is_expected_to_sleep();
if (self->next_tid) {
return_to_runqueue(self->next_tid);
self->next_tid = 0;
}
Are much simpler to manage. Either it did sleep and ::next_tid is
consumed, or it didn't sleep and it needs to be returned to the
runqueue.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-12-06 11:47:44
On Mon, Nov 29, 2021 at 09:34:49AM -0800, Peter Oskolkov wrote:
On Mon, Nov 29, 2021 at 8:41 AM Peter Zijlstra [off-list ref] wrote:
quoted
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
It should not. Timed out workers should be added to the runnable list
and not become running unless a server chooses so. So sys_umcg_wait()
with a timeout should behave similarly to a normal sleep, in that the
server is woken upon the worker blocking, and upon the worker wakeup
the worker is added to the woken workers list and waits for a server
to run it. The only difference is that in a sleep the worker becomes
BLOCKED, while in sys_umcg_wait() the worker is RUNNABLE the whole
time.
Why then have sys_umcg_wait() with a timeout at all, instead of
calling nanosleep()? Because the worker in sys_umcg_wait() can be
context-switched into by another worker, or made running by a server;
if the worker is in nanosleep(), it just sleeps.
I've been trying to figure out the semantics of that timeout thing, and
I can't seem to make sense of it.
Consider two workers:
S0 running A S1 running B
therefore:
S0::state == RUNNABLE S1::state == RUNNABLE
A::server_tid == S0.tid B::server_tid = S1.tid
A::state == RUNNING B::state == RUNNING
Doing:
self->state = RUNNABLE; self->state = RUNNABLE;
sys_umcg_wait(0); sys_umcg_wait(10);
umcg_enqueue_runnable() umcg_enqueue_runnable()
umcg_wake() umcg_wake()
umcg_wait() umcg_wait()
hrtimer_start()
In both cases we get the exact same outcome:
A::state == RUNNABLE B::state == RUNNABLE
S0::state == RUNNING S1::state == RUNNING
S0::runnable_ptr == &A S1::runnable_ptr = &B
Which is, AFAICT, the exact state you wanted to achieve, except B now
has an active timer, but what do you want it to do when that goes?
I'm tempted to say workers cannot have timeout, and servers can use it
to wake themselves.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-12-06 12:04:53
On Mon, Dec 06, 2021 at 12:32:22PM +0100, Peter Zijlstra wrote:
On Mon, Nov 29, 2021 at 03:38:38PM -0800, Peter Oskolkov wrote:
quoted
On Mon, Nov 29, 2021 at 1:08 PM Peter Zijlstra [off-list ref] wrote:
Now, the above situation is actually simple to fix, but it gets more
interesting when we're using sys_umcg_wait() to build wait primitives.
Because in that case we get stuff like:
for (;;) {
self->state = RUNNABLE;
smp_mb();
if (cond)
break;
sys_umcg_wait();
}
self->state = RUNNING;
And we really need to not block and also not do sys_umcg_wait() early.
So yes, I agree that we need a special case here that ensures
umcg_notify_resume() doesn't block. Let me ponder naming and comments.
Either a TF_COND_WAIT or a whole new state. I can't decide yet.
Hurmph... OTOH since self above hasn't actually done anything yet, it
isn't reported as runnable yet, and so for all intents and purposes the
userspace state thinks it's running (which is true) and nobody should be
trying a concurrent wakeup and there anre't any races.
Bah, now I'm confused again :-) Let me go think more.
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-12-13 13:56:00
On Mon, Dec 06, 2021 at 12:32:22PM +0100, Peter Zijlstra wrote:
Sorry, I haven't been feeling too well and as such procastinated on this
because thinking is required :/ Trying to pick up the bits.
*sigh* and yet another week gone... someone was unhappy about refcount_t.
No, the failure case is different; umcg_notify_resume() will simply
block A until someone sets A::state == RUNNING and kicks it, which will
be no-one.
Now, the above situation is actually simple to fix, but it gets more
interesting when we're using sys_umcg_wait() to build wait primitives.
Because in that case we get stuff like:
for (;;) {
self->state = RUNNABLE;
smp_mb();
if (cond)
break;
sys_umcg_wait();
}
self->state = RUNNING;
And we really need to not block and also not do sys_umcg_wait() early.
So yes, I agree that we need a special case here that ensures
umcg_notify_resume() doesn't block. Let me ponder naming and comments.
Either a TF_COND_WAIT or a whole new state. I can't decide yet.
Now, obviously if you do a random syscall anywhere around here, you get
to keep the pieces :-)
From: Peter Oskolkov <hidden> Date: 2022-01-19 17:26:56
On Mon, Dec 6, 2021 at 3:47 AM Peter Zijlstra [off-list ref] wrote:
On Mon, Nov 29, 2021 at 09:34:49AM -0800, Peter Oskolkov wrote:
quoted
On Mon, Nov 29, 2021 at 8:41 AM Peter Zijlstra [off-list ref] wrote:
quoted
quoted
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
It should not. Timed out workers should be added to the runnable list
and not become running unless a server chooses so. So sys_umcg_wait()
with a timeout should behave similarly to a normal sleep, in that the
server is woken upon the worker blocking, and upon the worker wakeup
the worker is added to the woken workers list and waits for a server
to run it. The only difference is that in a sleep the worker becomes
BLOCKED, while in sys_umcg_wait() the worker is RUNNABLE the whole
time.
Why then have sys_umcg_wait() with a timeout at all, instead of
calling nanosleep()? Because the worker in sys_umcg_wait() can be
context-switched into by another worker, or made running by a server;
if the worker is in nanosleep(), it just sleeps.
I've been trying to figure out the semantics of that timeout thing, and
I can't seem to make sense of it.
Consider two workers:
S0 running A S1 running B
therefore:
S0::state == RUNNABLE S1::state == RUNNABLE
A::server_tid == S0.tid B::server_tid = S1.tid
A::state == RUNNING B::state == RUNNING
Doing:
self->state = RUNNABLE; self->state = RUNNABLE;
sys_umcg_wait(0); sys_umcg_wait(10);
umcg_enqueue_runnable() umcg_enqueue_runnable()
sys_umcg_wait() should not enqueue the worker as runnable; workers are
enqueued to indicate wakeup events.
umcg_wake() umcg_wake()
umcg_wait() umcg_wait()
hrtimer_start()
In both cases we get the exact same outcome:
A::state == RUNNABLE B::state == RUNNABLE
S0::state == RUNNING S1::state == RUNNING
S0::runnable_ptr == &A S1::runnable_ptr = &B
So without sys_umcg_wait enqueueing into the queue, the state now is
A::state == RUNNABLE B::state == RUNNABLE
S0::state == RUNNING S1::state == RUNNING
S0::runnable_ptr == NULL S1::runnable_ptr = NULL
Which is, AFAICT, the exact state you wanted to achieve, except B now
has an active timer, but what do you want it to do when that goes?
When the timer goes off, _then_ B is enqueued into the queue, so the
state becomes
A::state == RUNNABLE B::state == RUNNABLE
S0::state == RUNNING S1::state == RUNNING
S0::runnable_ptr == NULL S1::runnable_ptr = &B
So worker timeouts in sys_umcg_wait are treated as wakeup events, with
the difference that when the worker is eventually scheduled by a
server, sys_umcg_wait returns with ETIMEDOUT.
I'm tempted to say workers cannot have timeout, and servers can use it
to wake themselves.
From: Peter Zijlstra <peterz@infradead.org> Date: 2022-01-20 11:08:05
On Wed, Jan 19, 2022 at 09:26:41AM -0800, Peter Oskolkov wrote:
On Mon, Dec 6, 2021 at 3:47 AM Peter Zijlstra [off-list ref] wrote:
quoted
On Mon, Nov 29, 2021 at 09:34:49AM -0800, Peter Oskolkov wrote:
quoted
On Mon, Nov 29, 2021 at 8:41 AM Peter Zijlstra [off-list ref] wrote:
quoted
quoted
Also, timeout on sys_umcg_wait() gets you the exact same situation (or
worse, multiple running workers).
It should not. Timed out workers should be added to the runnable list
and not become running unless a server chooses so. So sys_umcg_wait()
with a timeout should behave similarly to a normal sleep, in that the
server is woken upon the worker blocking, and upon the worker wakeup
the worker is added to the woken workers list and waits for a server
to run it. The only difference is that in a sleep the worker becomes
BLOCKED, while in sys_umcg_wait() the worker is RUNNABLE the whole
time.
Why then have sys_umcg_wait() with a timeout at all, instead of
calling nanosleep()? Because the worker in sys_umcg_wait() can be
context-switched into by another worker, or made running by a server;
if the worker is in nanosleep(), it just sleeps.
I've been trying to figure out the semantics of that timeout thing, and
I can't seem to make sense of it.
Consider two workers:
S0 running A S1 running B
therefore:
S0::state == RUNNABLE S1::state == RUNNABLE
A::server_tid == S0.tid B::server_tid = S1.tid
A::state == RUNNING B::state == RUNNING
Doing:
self->state = RUNNABLE; self->state = RUNNABLE;
sys_umcg_wait(0); sys_umcg_wait(10);
umcg_enqueue_runnable() umcg_enqueue_runnable()
sys_umcg_wait() should not enqueue the worker as runnable; workers are
enqueued to indicate wakeup events.
Oooh... I see.
So worker timeouts in sys_umcg_wait are treated as wakeup events, with
the difference that when the worker is eventually scheduled by a
server, sys_umcg_wait returns with ETIMEDOUT.
Right.. OK, let me go fold and polish what I have now before I go change
things again though.