2022-07-11 14:02:08

by Brian Foster

[permalink] [raw]
Subject: [PATCH v2 0/4] proc: improve root readdir latency with many threads

Hi all,

Here's v2 of the /proc readdir optimization patches. See v1 for the full
introductary cover letter.

The refactoring in v2 adds a bit more to the idr code, but it remains
trivial with respect to eventual xarray (tag -> mark) conversion. On
that topic, I'm still looking for some feedback in the v1 thread [1] on
the prospective approach...

Brian

[1] https://lore.kernel.org/linux-fsdevel/YrykXim1t71TgdYg@bfoster/

v2:
- Clean up idr helpers to be more generic.
- Use ->idr_base properly.
- Lift tgid iteration helper into pid.c to abstract tag logic from
users.
v1: https://lore.kernel.org/linux-fsdevel/[email protected]/

Brian Foster (4):
radix-tree: propagate all tags in idr tree
idr: support optional id tagging
pid: tag pids associated with group leader tasks
procfs: use efficient tgid pid search on root readdir

fs/proc/base.c | 17 +----------------
include/linux/idr.h | 26 ++++++++++++++++++++++++++
include/linux/pid.h | 3 ++-
kernel/fork.c | 2 +-
kernel/pid.c | 40 +++++++++++++++++++++++++++++++++++++++-
lib/radix-tree.c | 26 +++++++++++++++-----------
6 files changed, 84 insertions(+), 30 deletions(-)

--
2.35.3


2022-07-11 14:17:31

by Brian Foster

[permalink] [raw]
Subject: [PATCH v2 2/4] idr: support optional id tagging

Certain idr users can benefit from generic tagging support of the
underlying radix-tree (or xarray) data structure. For example, a
readdir of the /proc root dir performs an inefficient walk of the
pid namespace idr tree. This involves checking the entry of every
allocated id for a group leader task association. Expose a simple,
single tag interface for idr users to facilitate more efficient
scans in situations like this.

Signed-off-by: Brian Foster <[email protected]>
---
include/linux/idr.h | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)

diff --git a/include/linux/idr.h b/include/linux/idr.h
index a0dce14090a9..44e8bb287d0e 100644
--- a/include/linux/idr.h
+++ b/include/linux/idr.h
@@ -27,6 +27,7 @@ struct idr {
* to users. Use tag 0 to track whether a node has free space below it.
*/
#define IDR_FREE 0
+#define IDR_TAG 1

/* Set the IDR flag and the IDR_FREE tag */
#define IDR_RT_MARKER (ROOT_IS_IDR | (__force gfp_t) \
@@ -174,6 +175,31 @@ static inline void idr_preload_end(void)
local_unlock(&radix_tree_preloads.lock);
}

+static inline void idr_set_tag(struct idr *idr, unsigned long id)
+{
+ radix_tree_tag_set(&idr->idr_rt, id - idr->idr_base, IDR_TAG);
+}
+
+static inline bool idr_get_tag(struct idr *idr, unsigned long id)
+{
+ return radix_tree_tag_get(&idr->idr_rt, id - idr->idr_base, IDR_TAG);
+}
+
+/*
+ * Find the next id with the internal tag set.
+ */
+static inline void *idr_get_next_tag(struct idr *idr, unsigned long id)
+{
+ unsigned int ret;
+ void *entry;
+
+ ret = radix_tree_gang_lookup_tag(&idr->idr_rt, &entry,
+ id - idr->idr_base, 1, IDR_TAG);
+ if (ret != 1)
+ return NULL;
+ return entry;
+}
+
/**
* idr_for_each_entry() - Iterate over an IDR's elements of a given type.
* @idr: IDR handle.
--
2.35.3

2022-07-11 14:18:19

by Brian Foster

[permalink] [raw]
Subject: [PATCH v2 4/4] procfs: use efficient tgid pid search on root readdir

find_ge_pid() walks every allocated id and checks every associated
pid in the namespace for a link to a PIDTYPE_TGID task. If the pid
namespace contains processes with large numbers of threads, this
search doesn't scale and can notably increase getdents() syscall
latency.

For example, on a mostly idle 2.4GHz Intel Xeon running Fedora on
5.19.0-rc2, 'strace -T xfs_io -c readdir /proc' shows the following:

getdents64(... /* 814 entries */, 32768) = 20624 <0.000568>

With the addition of a dummy (i.e. idle) process running that
creates an additional 100k threads, that latency increases to:

getdents64(... /* 815 entries */, 32768) = 20656 <0.011315>

While this may not be noticeable to users in one off /proc scans or
simple usage of ps or top, we have users that report problems caused
by this latency increase in these sort of scaled environments with
custom tooling that makes heavier use of task monitoring.

Optimize the tgid task scanning in proc_pid_readdir() by using the
more efficient find_get_tgid_task() helper. This significantly
improves readdir() latency when the pid namespace is populated with
processes with very large thread counts. For example, the above 100k
idle task test against a patched kernel now results in the
following:

Idle:
getdents64(... /* 861 entries */, 32768) = 21048 <0.000670>

"" + 100k threads:
getdents64(... /* 862 entries */, 32768) = 21096 <0.000959>

... which is a much smaller latency hit after the high thread count
task is started.

Signed-off-by: Brian Foster <[email protected]>
---
fs/proc/base.c | 17 +----------------
1 file changed, 1 insertion(+), 16 deletions(-)

diff --git a/fs/proc/base.c b/fs/proc/base.c
index 8dfa36a99c74..b3bff6d26dcc 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -3429,24 +3429,9 @@ struct tgid_iter {
};
static struct tgid_iter next_tgid(struct pid_namespace *ns, struct tgid_iter iter)
{
- struct pid *pid;
-
if (iter.task)
put_task_struct(iter.task);
- rcu_read_lock();
-retry:
- iter.task = NULL;
- pid = find_ge_pid(iter.tgid, ns);
- if (pid) {
- iter.tgid = pid_nr_ns(pid, ns);
- iter.task = pid_task(pid, PIDTYPE_TGID);
- if (!iter.task) {
- iter.tgid += 1;
- goto retry;
- }
- get_task_struct(iter.task);
- }
- rcu_read_unlock();
+ iter.task = find_get_tgid_task(&iter.tgid, ns);
return iter;
}

--
2.35.3

2022-07-11 14:37:05

by Brian Foster

[permalink] [raw]
Subject: [PATCH v2 3/4] pid: tag pids associated with group leader tasks

Searching the pid_namespace for group leader tasks is a fairly
inefficient operation. Listing the root directory of a procfs mount
performs a linear scan of allocated pids, checking each entry for an
associated PIDTYPE_TGID task to determine whether to populate a
directory entry. This can cause a significant increase in readdir()
syscall latency when run in namespaces that might have one or more
processes with significant thread counts.

To facilitate improved TGID pid searches, tag the ids of pid entries
that are likely to have an associated PIDTYPE_TGID task. To keep the
code simple and avoid having to maintain synchronization between tag
state and post-fork pid-task association changes, the tag is applied
to all pids allocated for tasks cloned without CLONE_THREAD.

This means that it is possible for a pid to remain tagged in the idr
tree after being disassociated from the group leader task. For
example, a process that does a setsid() followed by fork() and
exit() (to daemonize) will remain associated with the original pid
for the session, but link with the child pid as the group leader.
OTOH, the only place other than fork() where a tgid association
occurs is in the exec() path, which kills all other tasks in the
group and associates the current task with the preexisting leader
pid. Therefore, the semantics of the tag are that false positives
(tagged pids without PIDTYPE_TGID tasks) are possible, but false
negatives (untagged pids without PIDTYPE_TGID tasks) should never
occur.

This is an effective optimization because false negatives are fairly
uncommon and don't add overhead (i.e. we already have to check
pid_task() for tagged entries), but still filters out thread pids
that are guaranteed not to have TGID task association.

Tag entries in the pid allocation path when the caller specifies
that the pid associates with a new thread group. Since false
negatives are not allowed, warn in the event that a PIDTYPE_TGID
task is ever attached to an untagged pid. Finally, create a helper
to implement the task search based on the tag semantics defined
above (based on search logic currently implemented by next_tgid() in
procfs).

Signed-off-by: Brian Foster <[email protected]>
---
include/linux/pid.h | 3 ++-
kernel/fork.c | 2 +-
kernel/pid.c | 40 +++++++++++++++++++++++++++++++++++++++-
3 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/include/linux/pid.h b/include/linux/pid.h
index 343abf22092e..64caf21be256 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -132,9 +132,10 @@ extern struct pid *find_vpid(int nr);
*/
extern struct pid *find_get_pid(int nr);
extern struct pid *find_ge_pid(int nr, struct pid_namespace *);
+struct task_struct *find_get_tgid_task(int *id, struct pid_namespace *);

extern struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
- size_t set_tid_size);
+ size_t set_tid_size, bool group_leader);
extern void free_pid(struct pid *pid);
extern void disable_pid_allocation(struct pid_namespace *ns);

diff --git a/kernel/fork.c b/kernel/fork.c
index 9d44f2d46c69..3c52f45ec93e 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2254,7 +2254,7 @@ static __latent_entropy struct task_struct *copy_process(

if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
- args->set_tid_size);
+ args->set_tid_size, !(clone_flags & CLONE_THREAD));
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
diff --git a/kernel/pid.c b/kernel/pid.c
index 2fc0a16ec77b..bd72d1dbff95 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -157,7 +157,7 @@ void free_pid(struct pid *pid)
}

struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
- size_t set_tid_size)
+ size_t set_tid_size, bool group_leader)
{
struct pid *pid;
enum pid_type type;
@@ -272,6 +272,8 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
for ( ; upid >= pid->numbers; --upid) {
/* Make the PID visible to find_pid_ns. */
idr_replace(&upid->ns->idr, pid, upid->nr);
+ if (group_leader)
+ idr_set_tag(&upid->ns->idr, upid->nr);
upid->ns->pid_allocated++;
}
spin_unlock_irq(&pidmap_lock);
@@ -331,6 +333,10 @@ static struct pid **task_pid_ptr(struct task_struct *task, enum pid_type type)
void attach_pid(struct task_struct *task, enum pid_type type)
{
struct pid *pid = *task_pid_ptr(task, type);
+ struct pid_namespace *pid_ns = ns_of_pid(pid);
+ pid_t pid_nr = pid_nr_ns(pid, pid_ns);
+
+ WARN_ON(type == PIDTYPE_TGID && !idr_get_tag(&pid_ns->idr, pid_nr));
hlist_add_head_rcu(&task->pid_links[type], &pid->tasks[type]);
}

@@ -520,6 +526,38 @@ struct pid *find_ge_pid(int nr, struct pid_namespace *ns)
return idr_get_next(&ns->idr, &nr);
}

+/*
+ * Used by proc to find the first thread group leader task with an id greater
+ * than or equal to *id.
+ *
+ * Use the idr tag hint to find the next best pid. The tag does not guarantee a
+ * linked task exists, so retry until a suitable entry is found.
+ */
+struct task_struct *find_get_tgid_task(int *id, struct pid_namespace *ns)
+{
+ struct pid *pid;
+ struct task_struct *t;
+ unsigned int nr = *id;
+
+ rcu_read_lock();
+
+ do {
+ pid = idr_get_next_tag(&ns->idr, nr);
+ if (!pid) {
+ rcu_read_unlock();
+ return NULL;
+ }
+ t = pid_task(pid, PIDTYPE_TGID);
+ nr++;
+ } while (!t);
+
+ *id = pid_nr_ns(pid, ns);
+ get_task_struct(t);
+ rcu_read_unlock();
+
+ return t;
+}
+
struct pid *pidfd_get_pid(unsigned int fd, unsigned int *flags)
{
struct fd f;
--
2.35.3

2022-07-11 20:23:52

by Matthew Wilcox

[permalink] [raw]
Subject: Re: [PATCH v2 0/4] proc: improve root readdir latency with many threads

On Mon, Jul 11, 2022 at 09:52:33AM -0400, Brian Foster wrote:
> Hi all,
>
> Here's v2 of the /proc readdir optimization patches. See v1 for the full
> introductary cover letter.

I really don't want to change the radix tree or IDR code. That's why
I did the conversion of the pid code to use the XArray for you.

> The refactoring in v2 adds a bit more to the idr code, but it remains
> trivial with respect to eventual xarray (tag -> mark) conversion. On
> that topic, I'm still looking for some feedback in the v1 thread [1] on
> the prospective approach...

Oh, I missed the last few emails in that thread due to being on holiday.
I'll go back and read/reply to them now.