Многопоточная реализация UNIX cp на C

Недавно я закончил свой школьный проект для класса, посвященного созданию моей собственной многопоточной реализации команды терминала “cp” в системах UNIX.

Я ищу помощи у опытных людей в C, чтобы они помогли мне сделать мой код лучше, предлагая конструктивную критику и в то же время улучшая себя.

В частности:

  • Может ли мой код стать быстрее, чем сейчас?
  • Есть ли в моем коде возможные ошибки времени выполнения? (Я протестировал его на нескольких примерах, и все будет работать нормально, я просто пытаюсь найти те, о которых я не думал)
  • У моего кода хорошая общая структура?

Поскольку полный код относительно длинный, я предоставлю ссылку на свой репозиторий Github. Вот. Я структурировал его так, чтобы рецензенты могли легко его просмотреть, а также предоставил комментарии, чтобы помочь.

Точки интереса:

src/crawler.c

#include "../headers/crawler.h"

int dircrawler(char* path, char* dest, int pathsize)
{
   // dirent variables and structures
   DIR* dp;
   struct dirent* entry;
   // buffers
   size_t bufsize;
   char *buf1, *buf2;
   buf1 = buf2 = NULL;
   struct cp_thr_args* args = NULL;

   if ((dp = opendir(path)) != NULL) {
      while ((entry = readdir(dp)) != NULL) {
         // skip over . and ..
         if (!(strcmp(entry->d_name, ".")) || !(strcmp(entry->d_name, "..")))
            continue;

         if (buf1 != NULL)
            free(buf1);
         if (buf2 != NULL)
            free(buf2);

         // printf("%sn",entry->d_name);

         // buf1: absolute new path, buf2: relative destination path
         bufsize = snprintf(NULL, 0, "%s/%s", path, entry->d_name) + 1;
         mmalloc(&buf1, bufsize); // buf1
         snprintf(buf1, bufsize, "%s/%s", path, entry->d_name);

         bufsize = snprintf(NULL, 0, "%s%s/%s", dest, (path + pathsize - 1), entry->d_name) + 1;
         mmalloc(&buf2, bufsize); // buf2
         snprintf(buf2, bufsize, "%s%s/%s", dest, (path + pathsize), entry->d_name);

         if (entry->d_type == DT_DIR) {
            // make new directory in dest
            mkdir(buf2, 0755);
            if (dircrawler(buf1, dest, pathsize))
               closedir(dp);

         } else {
            // thread copying section
            int found = 1;
            while (1) { // ((Point of interest))
               // trying to find a non occupied thread
               // using status[]
               for (int i = 0; i < THREADCOUNT; ++i) {
                  if (status[i] == 1) {
                     cp_thr_init(&args, buf1, buf2, &status[i]);
                     pthread_create(&cpthr[i], NULL, cp_thr, (void*)args);
                     pthread_barrier_wait(&can_free);
                     cp_thr_free(args);
                     found = 0;
                     break;
                  }
               }
               if (!found)
                  break;
            }
         }
      }
      if (buf1 != NULL)
         free(buf1);
      if (buf2 != NULL)
         free(buf2);
      closedir(dp);
      return 0;
   }
}

src/threads.c

#include "../headers/threads.h"

int status[THREADCOUNT] = { 1, 1, 1, 1 };
pthread_mutex_t cp_lock = PTHREAD_MUTEX_INITIALIZER;

void* cp_thr(void* args)
{
   // extract args data before it gets rewritten
   struct cp_thr_args* temp = args;
   char* from;
   mmalloc(&from, return_size(temp->from) + 1);
   char* dest;
   mmalloc(&dest, return_size(temp->dest) + 1);
   int* status = temp->status;
   strcpy(from, temp->from);
   strcpy(dest, temp->dest);
   // send signal to release barrier and free args from crawl
   pthread_barrier_wait(&can_free); // ((Point of interest))

   // assign status to 0 to mark thread as occupied
   pthread_mutex_lock(&cp_lock);
   *status = 0;
   pthread_mutex_unlock(&cp_lock);

   copy_file(from, dest);

   free(from);
   free(dest);

   // assign status to 1 to mark thread as free to use
   pthread_mutex_lock(&cp_lock);
   *status = 1;
   pthread_mutex_unlock(&cp_lock);

   return (void*)0;
}

1 ответ
1

Использовать ftw() сканировать каталоги

ftw() – это функция POSIX, которая рекурсивно просматривает каталоги и вызывает функцию обратного вызова для каждой найденной записи каталога.

Рассмотрите возможность использования strdup() и asprintf()

Вместо копирования строк с помощью какого-то strlen()+malloc()+strcpy(), используйте функцию POSIX strdup() если возможно, который позаботится обо всем этом за вас.

Linux и большинство BSD также реализуют asprintf(), который позаботится о выделении для вас буфера правильного размера. Имейте в виду, что это определенно не кроссплатформенный.

Не выбрасывайте нитки

Хотя создание и уничтожение потоков в Linux происходит довольно быстро, они все же связаны с накладными расходами. Если вы копируете много небольших файлов, эти накладные расходы могут начать преобладать. Было бы гораздо лучше иметь фиксированное количество потоков, ожидающих выполнения работы.

Типичная реализация будет использовать для этого потокобезопасную очередь. Попросите искателя каталогов заполнить очередь имен файлов для копирования. Очередь охраняется мьютексом и имеет связанную переменную условия. Каждый раз, когда он помещает элемент в очередь, он сигналы переменная условия. Нити Подождите для элементов, которые должны быть помещены, и когда они просыпаются, они выталкивают одну запись из очереди и начинают ее копировать.

Однако если предположить, что искатель каталогов работает намного быстрее, чем файлы могут быть скопированы, заполнение очереди всеми встречающимися именами файлов было бы пустой тратой памяти, особенно если дерево каталогов большое. Вам нужно всего несколько записей в очереди, чтобы рабочие потоки взяли работу. Вы можете сделать так, чтобы поисковый робот блокировал себя, если очередь становится слишком большой. Затем вам следует добавить вторую переменную условия, которая будет сигнализироваться всякий раз, когда работник удаляет имя файла из очереди для копирования.

Найдите нужное количество потоков для использования

Трудно предсказать, сколько потоков будет оптимальным. Возможно, оптимальным будет один поток, в зависимости от того, насколько быстро хранилище по сравнению с накладными расходами, связанными с наличием нескольких потоков. Количество потоков, равное количеству копируемых файлов, тоже может быть не идеальным; даже если они не используют много процессорного времени, выполнение операций ввода-вывода для нескольких файлов одновременно может привести к большему количеству обращений к диску (что в основном является проблемой для жестких дисков, но некоторые твердотельные накопители также имеют некоторое снижение производительности из-за того, что не -последовательный доступ). Вы можете протестировать свой код и оптимизировать количество потоков для своей системы, но производительность может быть другой в других системах.

Альтернативы нитям

Потоки – это решение проблемы неявной сериализации при выполнении обычных операций ввода-вывода с файлами в одном потоке. Однако существуют и другие методы одновременной передачи в ядро ​​нескольких операций ввода-вывода. POSIX AIO – это один из способов сделать это, однако у него есть свои ограничения. В частности, я не думаю, что вы легко можете передать файл копировать операция к нему. Что более перспективно, так это Linux io_uring, хотя это, конечно, не кроссплатформенное решение вашей проблемы. Если вас интересует последнее, то также смотрите эта статья LWN и это отличное руководство. У большинства разновидностей BSD есть нечто подобное, называемое kqueue.

  • 1

    IIRC, asprintf() была функцией GNU GPL (не LGPL), но существуют другие версии с более разрешительными лицензиями (например, эта реализация с лицензией MIT). И если нужно, написать свой собственный несложно.

    – Тоби Спейт

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *