Введение
Продолжим тему использования механизма 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 для облегчения отладки и запуска.