Compare commits

...

7 Commits

Author SHA1 Message Date
Érico Nogueira e56abc9ff9 Add directory file descriptor caching as well.
Now a parent can store its own fd, which will live on for as long as the
parent does, allowing us to use relative *at functions (that might be
faster on the kernel side?) and store smaller path buffers.

Since we were playing with soft limit detection, also make the EMFILE
operations conditional on limited_fds, avoiding potentially expensive
mutex operations when possible.

When checking the soft limit, we use 2 for the number of standard
file descriptors, because we closed stdin in main().
2022-06-12 23:14:31 -03:00
Érico Nogueira 3db1db5cf4 Close stdin on program launch.
It's unused and can give us an extra file descriptor.
2022-06-12 22:59:17 -03:00
Érico Nogueira fe0c1705b2 Move struct task cache to file scope.
Making it a thread_local variable that can be used by any function means
we can set it from recurse_into_parents with ease, increasing the
likelihood it's set when allocating a new struct.

We also add a debugging print to the malloc branch in task allocation;
this allows us to compare how many allocations use p_old and how many
don't. Again with the linux kernel tree, ~50% of allocations now use
p_old.
2022-06-12 21:59:45 -03:00
Érico Nogueira a626ae594d Add a per-thread struct task cache.
This allows us to save some malloc()/free() calls.

The structs can leak on program exit, but an external vector to store
them could fix this.

As it stands, this isn't really useful: in multiple tests with the linux
kernel tree, the p_old branch was taken between 0 and 3 times in a full
run.
2022-06-12 21:47:52 -03:00
Érico Nogueira 630ccbf1ce Only compute plen if necessary.
Save on strlen calls by only doing them if plen is going to be used.
2022-06-12 21:33:36 -03:00
Érico Nogueira 851bd62ab6 Implement . and .. detection manually.
strcmp() startup cost is too high and not worth it here.
2022-06-12 21:29:22 -03:00
Érico Nogueira 2fbe3a2789 Use more specific memory ordering.
- other threads need to see the correct value of p->files, so use
  'release' in the parent cleanup section
- recurse_into_parents needs to see the correct value of p->files, so
  use 'acquire' there
- also add comments explaining each branch in the parent cleanup section
2022-06-12 21:25:34 -03:00
2 changed files with 94 additions and 25 deletions

3
erm.c
View File

@ -19,6 +19,9 @@ int main(int argc, char **argv)
bool recursive = false;
bool stop_at_error = true;
/* we don't use stdin, so give ourselves an extra fd */
fclose(stdin);
int opt;
while ((opt = getopt(argc, argv, "reh")) != -1) {
switch (opt) {

116
remove.c
View File

@ -5,9 +5,12 @@
#include <pthread.h>
#include <sched.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <threads.h>
#include <unistd.h>
#include "erm.h"
@ -24,6 +27,8 @@ struct task {
struct task *parent;
/* reference counting */
unsigned files;
/* stores the dirfd for this path or -1 */
int dfd;
atomic_uint removed_count;
};
@ -40,6 +45,13 @@ static pthread_mutex_t fd_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t fd_cond = PTHREAD_COND_INITIALIZER;
static unsigned nproc;
/* is true if number of available fds is smaller than number of threads being used */
static bool limited_fds;
/* stores amount of additional fds that can be used beyond the one per thread */
static atomic_int dfd_max;
/* p_old is a struct task cache; this means each thread can leak one struct task in total */
static thread_local struct task *p_old = NULL;
static inline void queue_print(struct queue *q)
{
@ -94,27 +106,42 @@ static inline void queue_remove(struct queue *q, struct task *t)
pthread_mutex_unlock(&q->mtx);
}
static void close_dfd(const struct task *t)
{
if (t->dfd == -1) return;
close(t->dfd);
atomic_fetch_add_explicit(&dfd_max, 1, memory_order_relaxed);
}
static int rmdir_parent(const struct task *t)
{
int rfd = (t->parent && t->parent->dfd != -1) ? t->parent->dfd : AT_FDCWD;
return unlinkat(rfd, t->path, AT_REMOVEDIR);
}
static inline void recurse_into_parents(struct task *t)
{
struct task *recurse = t;
void *free_list = NULL;
struct task *recurse = t, *free_list = NULL;
while ((recurse = recurse->parent)) {
free(free_list); free_list = NULL;
unsigned rc = atomic_fetch_add_explicit(&recurse->removed_count, 1, memory_order_acq_rel);
unsigned rc = atomic_fetch_add_explicit(&recurse->removed_count, 1, memory_order_acquire);
if (rc & ACQUIRED) break;
printf("parent: removed %04d total %04d '%s'\n", rc, recurse->files, recurse->path);
if (rc == recurse->files) {
/* we have removed all files in the directory */
if (rmdir(recurse->path)) {
if (rmdir_parent(recurse)) {
fprintf(stderr, "rec rmdir failed '%s': %m\n", recurse->path);
} else {
printf("rec rmdir succeeded '%s'\n", recurse->path);
}
free(recurse->path);
/* can't free now because the while condition uses it */
free_list = recurse;
close_dfd(recurse);
if (p_old) free_list = recurse;
else p_old = recurse;
} else {
/* if we haven't removed this directory yet,
* there's no reason to recurse further */
@ -133,8 +160,9 @@ static void *process_queue_item(void *arg)
queue_remove(q, &t);
int dfd;
while ((dfd = open(t.path, O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC)) < 0) {
if (errno == EMFILE) {
int rfd = (t.parent && t.parent->dfd != -1) ? t.parent->dfd : AT_FDCWD;
while ((dfd = openat(rfd, t.path, O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC)) < 0) {
if (limited_fds && errno == EMFILE) {
pthread_mutex_lock(&fd_mtx);
pthread_cond_wait(&fd_cond, &fd_mtx);
pthread_mutex_unlock(&fd_mtx);
@ -152,10 +180,13 @@ static void *process_queue_item(void *arg)
struct task *p = NULL;
unsigned n = 0;
size_t plen = strlen(t.path);
size_t plen;
struct dirent *entry;
while ((entry = readdir(d))) {
if (strcmp(".", entry->d_name)==0 || strcmp("..", entry->d_name)==0) continue;
if (entry->d_name[0] == '.' &&
(entry->d_name[1] == '\0' ||
(entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
continue;
/* fast path to avoid allocations */
int trv;
@ -168,45 +199,76 @@ fast_path_dir:
n++;
/* lazy allocation of p */
/* lazy allocation of p and other operations */
if (!p) {
p = malloc(sizeof *p);
if (p_old) {
p = p_old;
p_old = NULL;
puts("used p_old");
} else {
p = malloc(sizeof *p);
puts("didn't use p_old");
}
*p = t;
p->dfd = -1;
/* access happens only after mutex lock and release */
atomic_store_explicit(&p->removed_count, ACQUIRED, memory_order_relaxed);
if (!limited_fds) {
if (atomic_fetch_sub_explicit(&dfd_max, 1, memory_order_relaxed) > 0) {
/* we need to duplicate the fd due to calling closedir() below */
p->dfd = dup(dfd);
} else {
atomic_fetch_add_explicit(&dfd_max, 1, memory_order_relaxed);
/* we only use plen for absolute paths */
plen = strlen(t.path);
}
}
}
size_t nlen = strlen(entry->d_name);
char *buf = malloc(plen + nlen + 2);
memcpy(buf, p->path, plen);
buf[plen] = '/';
memcpy(buf+plen+1, entry->d_name, nlen);
buf[plen+nlen+1] = '\0';
char *buf;
if (p->dfd == -1) {
size_t nlen = strlen(entry->d_name);
buf = malloc(plen + nlen + 2);
memcpy(buf, p->path, plen);
buf[plen] = '/';
memcpy(buf+plen+1, entry->d_name, nlen);
buf[plen+nlen+1] = '\0';
} else {
buf = strdup(entry->d_name);
}
printf("adding to queue'%s'\n", buf);
queue_add(q, buf, p);
}
closedir(d);
pthread_mutex_lock(&fd_mtx);
pthread_cond_signal(&fd_cond);
pthread_mutex_unlock(&fd_mtx);
if (limited_fds) {
pthread_mutex_lock(&fd_mtx);
pthread_cond_signal(&fd_cond);
pthread_mutex_unlock(&fd_mtx);
}
if (p) {
p->files = n-1; /* other thread will compare against removed_count-1 */
unsigned rc = atomic_fetch_and_explicit(&p->removed_count, ~ACQUIRED, memory_order_acq_rel);
unsigned rc = atomic_fetch_and_explicit(&p->removed_count, ~ACQUIRED, memory_order_release);
if (rc == (n|ACQUIRED)) {
free(p);
if (rmdir(t.path)) {
/* this branch is taken when other threads have already removed all of p's children */
close_dfd(p);
if (p_old) free(p);
else p_old = p;
if (rmdir_parent(&t)) {
fprintf(stderr, "atomic rmdir failed '%s': %m\n", t.path);
} else {
printf("atomic rmdir succeeded '%s'\n", t.path);
}
} else {
/* we can't recurse into p's parent if p still has children that need to be removed */
continue;
}
} else {
/* p wasn't set because we could delete everything inside it */
if (rmdir(t.path)) {
if (rmdir_parent(&t)) {
fprintf(stderr, "fast path rmdir failed '%s': %m\n", t.path);
} else {
printf("fast path rmdir succeeded '%s'\n", t.path);
@ -234,10 +296,14 @@ void run_queue(void)
if (nproc_l > 64) nproc_l = 64;
nproc = nproc_l;
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl)) exit_init();
/* soft limit minus open std streams and minus directory fds from each thread */
if (rl.rlim_cur < nproc + 2) limited_fds = true;
else atomic_store_explicit(&dfd_max, rl.rlim_cur - 2 - nproc, memory_order_relaxed);
/* main thread will also be a task */
unsigned nproc1 = nproc - 1;
if (nproc1) {
pthread_attr_t pattr;
if (pthread_attr_init(&pattr)) exit_init();