Введение
Продолжим тему использования механизма RCU в системном программировании Linux. В первой части мы создали примитивный цикл неблокирующего изменения одной переменной, в данной части разберем пример неблокирующего изменения списка значений.
Библиотека Liburcu
В тесте используем библиотеку liburcu. Данная библиотека предоставляет как примитивы RCU так и работу с полноценными структурами, такими как хеш-таблицы, очереди, стеки и списки.
Подробное описание https://liburcu.org/
Для нашего сегодняшнего теста выбрана работа с динамическим RCU списком.
https://lwn.net/Articles/573441/.
Описание задачи
Поскольку основной смысл применения RCU это задачи с одним писателем и множеством читателей, попробуем реализовать следующий пример. Дан динамический список t_list в который необходимо добавлять и удалять записи, без блокировок чтения и нарушения целостности данных. Каждый элемент списка будет указывать на структуру node из структуры t_node в которой поля node и rcu_head необходимы для работы RCU, а value является пользовательскими данными.
CDS_LIST_HEAD(t_list); struct t_node { long long value; struct cds_list_head node; struct rcu_head rcu_head; };
Для работы со списком формируем пул потоков из которых, один является писателем и определяется функцией void *test_l_add(void *arg) , а остальные будут читателями с функцией void *test_l_get(void *arg).
for (i = 0; i < num_threads; i++) { if (i < 1) pthread_create(&tid[i], NULL, &test_l_add, (void *) i); else pthread_create(&tid[i], NULL, &test_l_get, (void *) i); }
После запуска нитей, основной поток ожидает 5 секунд, разблокирует Mutex остановки и ждет завершения всех дочерних нитей. Затем освобождение памяти и завершение процесса.
sleep(5); pthread_mutex_unlock(&m1); for (i = 0; i < num_threads; i++) { pthread_join(tid[i], NULL); }
Писатель RCU
Функция писателя void *test_l_add(void *arg) выполняет бесконечный цикл в ожидании освобождения Mutex. В цикле выполняется имитация работы со списком, при каждой итерации цикла добавляются 2 элемента списка со значениями REZ1 и REZ2. Функция cds_list_add_tail_rcu выполняется вне секции rcu_read_lock и не блокирует читателей. Каждый читатель, пока находится в rcu_read_lock, будет видеть старую версию списка.
struct t_node *node = malloc(sizeof(*node)); if (node) { node->value = REZ1; cds_list_add_tail_rcu(&node->node, &t_list); count++; }
На каждый сотый цикл выполняется поиск и удаление всех элементов со значением REZ2. В данном фрагменте реализован механизм отложенного удаления ресурсов, после вызова функции cds_list_del_rcu вызывается функция call_rcu которой передается в качестве параметра имя функции для освобождения ресурсов.
if (count1 % 100 == 0) { rcu_read_lock(); struct t_node *node = NULL; cds_list_for_each_entry_rcu(node, &t_list, node) { if (node->value == REZ2) { cds_list_del_rcu(&node->node); call_rcu(&node->rcu_head, free_node_rcu); } } rcu_read_unlock(); }
Функция освобождения ресурсов , вызывается в момент когда все нити читатели данного ресурса покинули секцию rcu_read_lock. Это очень важный момент, мы не вызываем synchronize_rcu() а поручаем библиотеке освободить ресурс когда это станет возможным, тем самым не блокируем писателя.
static void free_node_rcu(struct rcu_head *head) { struct t_node *node = caa_container_of(head, struct t_node, rcu_head); node->value = 0; free(node); }
Читатель RCU
Функция читателя void *test_l_get(void *arg) , также в бесконечном цикле выполняет чтение всего списка с проверкой корректности значений value. Чтение списка происходит в в секции rcu_read_lock(). Макрос cds_list_for_each_entry_rcu выполняет просмотр всего списка, возвращая указатель на структуру t_node. При освобождении Mutex нить завершает работу и выводит на экран количество циклов чтения и количество считанных элементов в последнем цикле. Необходимо обратить внимание что rcu_read_lock() и rcu_read_unlock() всегда парные , не допускайте незакрытую секцию чтения.
while (pthread_mutex_trylock(&m1) == EBUSY) { rcu_read_lock(); struct t_node *node = NULL; count_elm = 0; cds_list_for_each_entry_rcu(node, &t_list, node) { if (node->value != REZ1 && node->value != REZ2) { printf("Fatal error :%lld :%lld :%lld\n", i, count, count_elm); exit(-1); } count_elm++; } count++; rcu_read_unlock(); } printf("End GET :%lld :%lld :%lld\n", i, count, count_elm);
Тестирование
Приступаем к тестированию основной задачи — показать что тестовое приложение работает эффективно в независимости от числа нитей читателей.
Первый запуск выполним для проверки холостой работы писателя. Установим количество нитей = 1. Результатом работы являются строка Start и End , которые показывают номер нити и количество циклов. List empty выдает информацию сколько в нашем списке значений value равно REZ1 и REZ2 соответственно. Результаты постоянно изменяются +/- тысяча при 350 тысячах циклов за 5 секунд это нормально. Итак примем что моих условиях одна нить совершает ~340-360 тысяч циклов.
Start ADD :0
End ADD :0 :356500
List empty 356500 1
Писателям без читателей скучно, добавим 1. Появилась запись End GET читатель выполнил 3947 циклов и в последнем цикле в списке было 359499 элементов.
Start ADD :0
Start GET :1
End GET :1 :3947 :359499
End ADD :0 :359440
List empty 359440 41
Добавим сразу до 4.
Start ADD :0
Start GET :2
Start GET :1
Start GET :3
Start GET :4
End GET :1 :4567 :364999
End ADD :0 :364977
End GET :4 :3669 :365055
End GET :2 :4175 :365055
End GET :3 :3794 :365055
List empty 364977 78
И до 12, больше чем количество ядер особого смысла нет.
Start ADD :0
Start GET :3
Start GET :2
Start GET :1
Start GET :4
Start GET :11
Start GET :10
Start GET :5
Start GET :7
Start GET :9
Start GET :6
Start GET :8
End GET :10 :4101 :387799
End GET :1 :4414 :387899
End GET :6 :3699 :387999
End GET :2 :4140 :387999
End ADD :0 :387974
End GET :3 :4957 :388049
End GET :4 :3901 :388049
End GET :5 :3550 :388049
End GET :7 :3999 :388049
End GET :11 :3917 :388049
End GET :8 :3637 :388049
End GET :9 :3755 :388049
List empty 387974 75
По результатам прошу обратить внимание что каждая из нитей останавливается со своим значением количество элементов, до тех пор пока не остановилась нить-писатель, далее все читатели считали зафиксированный результат 387974+75 и остановились.
И каков итог измерений? RCU работает и очень эффективно, важно только правильно применить.
Вывод
Вполне уверенно можно сказать что тестовый проект получился рабочий, эффективный и простой в понимании. Надеюсь данный материал будет полезен людям, изучающим системное программирование в Linux.
Код проекта https://github.com/vic-35/test_liburcu2 , проект создан в QT creator для облегчения отладки и запуска.
ссылка на оригинал статьи https://habr.com/ru/post/715852/
Добавить комментарий