Fixing the TCMalloc regression with RSEQ operations
As a quick reminder of the problem: the restartable-sequences API is built around struct rseq, which is shared between user space and the kernel. The cpu_id_start field of that structure is maintained by the kernel; it contains the number of the CPU on which the owning thread is executing. It is documented as being read-only for user space. The TCMalloc library, though, wrote to this field anyway, setting it to a sentinel and using it, by way of some clever (to be polite) data-structure layout tricks, to notice when the kernel had reset it to a valid CPU number. That reset would indicate that the execution of the thread had been interrupted at some point. One of the optimizations in 6.19 eliminated the unconditional setting of that field, defeating this trick.
Until now, the problem has been worked around by detecting users of the older restartable sequences API and preserving the previous behavior, at a significant cost in both code complexity and performance. Using restartable sequences in this mode is also incompatible with how the GNU C Library uses it, causing other performance problems. For more details on the story so far, see this article.
It is clearly desirable to bring TCMalloc up to the current form of restartable sequences, restoring compatibility with the C library and performance as well. Dion's solution takes the form of "RSEQ operations", which is essentially the ability to program a set of operations for the kernel to carry out every time it returns to user space. An operation can be set up to restore the unconditional setting of the CPU number for TCMalloc in a way that does not hurt performance for any thread that is not using this new feature.
The simplest of the operations that are defined in this series is RSEQ_OP_RESET, which resets a word in user-space memory to a specific value. To set up such an operation, user space allocates and initializes a copy of this structure:
struct rseq_op_reset {
struct rseq_op_node node;
__u64 src;
__u64 dst;
__u32 len;
};
The src field holds a pointer to the value to be stored, or simply zero if zero should be stored; dst is a pointer to where that value should be stored, and len is the length of the destination, either four or eight bytes. The node field is a structure of its own:
struct rseq_op_node {
__u64 next;
__u64 prev;
struct {
__u8 type;
__u8 reserved[7];
};
};
Here, type should be set to the operation type (RSEQ_OP_RESET in this case); everything else must be zeroed and not touched again thereafter.
There are two other operations defined as well. RSEQ_OP_RESET_WITH_STRIDE_CPUID sets a value into an array indexed by the current CPU ID, while RSEQ_OP_RESET_WITH_STRIDE_MMCID indexes by the current virtual-CPU ID instead. Either uses this structure:
struct rseq_op_reset_with_stride {
struct rseq_op_node node;
__u64 src;
__u64 dst;
__u64 dst_stride;
__u32 len;
};
The added dst_stride value is multiplied by the CPU ID to obtain the actual address to store the value into.
Once one of these structures has been set up, it can be registered with the new PR_RSEQ_OP operation to prctl(). If the second prctl() argument is PR_RSEQ_OP_REGISTER, the new operation will be registered; a value of PR_RSEQ_OP_UNREGISTER will unregister an existing operation. Once an operation has been registered, it will be executed on each return to user space. There is a limit of 2,048 operations total; one imagines that should be enough.
There is an interesting quirk to this API. Normally, with a registration operation of this type, one would expect that the describing structure would be copied into the kernel and used there. In this case, though, the data is left in place, and the next and prev pointers in the rseq_op_node structure are used to link the containing structure into a list maintained by the kernel. On each return to user space, the requested operations will be carried out by traversing the list, which remains in user-space memory. Unlike the rseq structure, the operations structures are not mapped into the kernel's address space; the normal primitives for access to user-space memory are used as needed.
This seems like a curious decision; all of those user-space accesses will surely slow the operations down, and user space will have the ability to change the contents of those structures (including the pointers) at any time. Quite a bit of care is taken in the code to validate this list and its contents at every step (increasing the cost further), but it still feels like dangerous territory. It would seem that the ability for user space to make changes is actually a desired feature in this case; the ability to change the value to be stored or the location where that value should go is evidently considered to be worth the cost.
Given that this series is, in the end, adding another virtual machine to the kernel (albeit an extremely simple one), one might wonder why Dion did not just implement the ability to run a BPF program on return to user space and be done with it. The patches do not address that question, but a plausible answer is that loading BPF programs requires privilege, while restartable sequences does not. Forcing every program that needs RSEQ operations to run with CAP_BPF would create problems of its own.
The patch series is so new that the bits are not yet dry; it is at the RFC
stage and has not, as yet, received a lot of comments. Dmitry Vyukov asked
about whether it would be better to keep the operations structures in the
kernel. Florian Weimer, instead, worried that
the feature "would turn into another bytecode interpreter over time,
basically reimplementing BPF
". As of this writing, Dion has not
responded to these questions. There is definitely interest in changes that
can enable TCMalloc to use the current API and, eventually, allow the
removal of the compatibility code; how much this particular proposal will
have to evolve before being merged remains to be seen, though.
| Index entries for this article | |
|---|---|
| Kernel | Restartable sequences |
The LWN site is currently under high scraper load, so comment display has been suppressed for anonymous users. If you are a human, you may read the comments by clicking the button below:
Note: you can avoid this step in the future by logging into your LWN account.
