В информатика, чтение-копирование-обновление (RCU ) - это механизм синхронизации, который позволяет избежать использования примитивов lock, в то время как несколько потоков одновременно читают и обновляют элементы, связанные через указатели и которые принадлежат общим структурам данных (например, связанные списки, деревья, хеш-таблицы ).
всякий раз, когда поток вставляет или удаляя элементы структур данных в разделяемой памяти, все считыватели гарантированно видят и просматривают либо старую, либо новую структуру, что позволяет избежать несогласованности (например, разыменования нулевых указателей).
Это используется, когда производительность чтения имеет решающее значение, и является примером компромисса между пространством и временем, позволяющего выполнять быстрые операции за счет большего пространства. Это заставляет всех считывателей работать так, как если бы не было синхронизации вовлечены, следовательно, они будут быстрыми, но также будут делать обновления более трудно.
Название происходит от способ, которым RCU используется для обновления связанной структуры на месте. Поток, желающий сделать это, использует следующие шаги:
Когда поток, создавший копию, пробуждается ядром, он может безопасно освободить старую структуру.
Таким образом, структура считывается одновременно с копированием потока, чтобы выполнить обновление, отсюда и название «обновление чтения-копирования». Аббревиатура «RCU» была одной из многих, внесенных сообществом Linux. Другие названия подобных методов включают пассивную сериализацию и отложенную передачу MP программистами VM / XA и поколения программистов K42 и Tornado.
Ключевым свойством RCU является то, что читатели могут получить доступ к структуре данных, даже когда она находится в процессе обновления : Программы обновления RCU не могут блокировать считыватели или заставлять их повторять попытки доступа. Этот обзор начинается с демонстрации того, как данные могут быть безопасно вставлены в связанные структуры и удалены из них, несмотря на одновременное чтение. Первая диаграмма справа изображает процедуру вставки с четырьмя состояниями, при этом время продвигается слева направо.
Первое состояние показывает глобальный указатель с именем gptr, который изначально имеет значение NULL, окрашенный в красный цвет, чтобы указать, что он может быть доступен для чтения в любое время, таким образом требуя, чтобы средства обновления были осторожны. Выделение памяти для новой структуры переходит во второе состояние. Эта структура имеет неопределенное состояние (обозначено вопросительными знаками), но недоступна для читателей (обозначена зеленым цветом). Поскольку структура недоступна для читателей, программа обновления может выполнять любую желаемую операцию, не опасаясь нарушения работы одновременных читателей. Инициализация этой новой структуры переходит в третье состояние, в котором отображаются инициализированные значения полей структуры. Назначение ссылки на эту новую структуру для gptr переходит в четвертое и последнее состояние. В этом состоянии структура доступна читателям, поэтому окрашена в красный цвет. Примитив rcu_assign_pointer используется для выполнения этого присваивания и гарантирует, что присвоение является атомарным в том смысле, что одновременные читатели увидят либо указатель NULL, либо действительный указатель на новую структуру, но не какой-то месиво. из двух значений. Дополнительные свойства rcu_assign_pointer описаны далее в этой статье.
Процедура удаления чтения-копирования-обновленияЭта процедура демонстрирует, как новые данные могут быть вставлены в связанную структуру данных, даже если считыватели одновременно просматривают структуру данных до, во время и после вставки. На второй диаграмме справа изображена процедура удаления с четырьмя состояниями, опять же с перемещением времени слева направо.
Первое состояние показывает связанный список, содержащий элементы A, B и C. Все три элемента окрашены в красный цвет, чтобы указать, что устройство чтения RCU может ссылаться на любой из них в любое время. Использование list_del_rcu для удаления элемента B из этого списка переходит во второе состояние. Обратите внимание, что ссылка от элемента B к C остается нетронутой, чтобы позволить читателям, в настоящее время ссылающимся на элемент B, перемещаться по оставшейся части списка. Читатели, обращающиеся к ссылке из элемента A, получат ссылку на элемент B или элемент C, но в любом случае каждый читатель увидит действительный и правильно отформатированный связанный список. Элемент B теперь окрашен в желтый цвет, что указывает на то, что, хотя уже существующие читатели могут все еще иметь ссылку на элемент B, новые читатели не имеют возможности получить ссылку. Операция ожидания читателей переходит в третье состояние. Обратите внимание, что для этой операции ожидания читателей нужно только дождаться уже существующих читателей, но не новых. Элемент B теперь окрашен в зеленый цвет, чтобы указать, что читатели больше не могут ссылаться на него. Следовательно, теперь для программы обновления безопасно освободить элемент B, тем самым перейдя в четвертое и последнее состояние.
Важно повторить, что во втором состоянии разные читатели могут видеть две разные версии списка, с элементом B или без него. Другими словами, RCU обеспечивает координацию в пространстве (разные версии списка) а также по времени (разные состояния в процедурах удаления). Это резко контрастирует с более традиционными примитивами синхронизации, такими как блокировка или транзакции, которые координируются во времени, но не в пространстве.
Эта процедура демонстрирует, как старые данные могут быть удалены из связанной структуры данных, даже если считыватели одновременно просматривают структуру данных до, во время и после удаления. Учитывая вставку и удаление, с помощью RCU можно реализовать широкий спектр структур данных.
Считыватели RCU выполняются в критических секциях на стороне чтения, которые обычно разделяются rcu_read_lock и rcu_read_unlock. Любой оператор, не входящий в критическую секцию на стороне чтения RCU, считается находящимся в состоянии покоя, и таким операторам не разрешается содержать ссылки на структуры данных, защищенные RCU, и при этом операция ожидания чтения не требуется для ожидания для потоков в состоянии покоя. Любой период времени, в течение которого каждый поток хотя бы один раз пребывает в состоянии покоя, называется льготным периодом. По определению, любой критический раздел RCU на стороне чтения, существующий в начале данного льготного периода, должен завершиться до конца этого льготного периода, что составляет основную гарантию, предоставляемую RCU. Кроме того, операция ожидания считывателей должна дождаться истечения хотя бы одного льготного периода. Оказывается, эта гарантия может быть предоставлена с чрезвычайно небольшими накладными расходами на стороне чтения, фактически, в предельном случае, который фактически реализуется сборками ядра Linux серверного класса, накладные расходы на стороне чтения равны нулю.
Основная гарантия RCU может быть использована путем разделения обновлений на этапы удаления и восстановления. Фаза удаления удаляет ссылки на элементы данных в структуре данных (возможно, заменяя их ссылками на новые версии этих элементов данных) и может выполняться одновременно с критическими секциями на стороне чтения RCU. Причина, по которой безопасно запускать этап удаления одновременно со считывателями RCU, заключается в семантике современных процессоров, гарантирующих, что считыватели будут видеть либо старую, либо новую версию структуры данных, а не частично обновленную ссылку. По истечении льготного периода никакие считыватели, ссылающиеся на старую версию, больше не могут быть, поэтому на этапе восстановления можно безопасно освободить (восстановить) элементы данных, составляющие эту старую версию.
Разделение обновление на этапах удаления и восстановления позволяет средству обновления немедленно выполнить этап удаления и отложить этап восстановления до тех пор, пока все считыватели, активные во время этапа удаления, не завершатся, другими словами, пока не истечет льготный период.
Таким образом, типичная последовательность обновления RCU выглядит примерно так:
В описанной выше процедуре (которая соответствует более ранняя диаграмма), программа обновления выполняет как удаление, так и этап восстановления, но часто бывает полезно, чтобы совершенно другой поток выполнял восстановление. Подсчет ссылок может использоваться, чтобы позволить читателю выполнить удаление, поэтому, даже если один и тот же поток выполняет как этап обновления (шаг (2) выше), так и этап восстановления (шаг (4) выше), часто полезно подумать о них. по отдельности.
К началу 2008 года в ядре Linux было почти 2000 применений RCU API, включая стеки сетевых протоколов и систему управления памятью. По состоянию на март 2014 года было более 9000 использований. С 2006 года исследователи применяют RCU и аналогичные методы для решения ряда проблем, включая управление метаданными, используемыми в динамическом анализе, управление сроком службы кластеризованных объектов, управление временем жизни объекта в исследовательской операционной системе K42 и оптимизацию реализация программной транзакционной памяти. Dragonfly BSD использует методику, аналогичную RCU, которая больше всего напоминает реализацию Sleepable RCU (SRCU) в Linux.
Возможность дождаться завершения всех считывателей позволяет считывающим устройствам RCU использовать гораздо более легкую синхронизацию - в некоторых случаях вообще никакой синхронизации. Напротив, в более традиционных схемах, основанных на блокировках, считыватели должны использовать тяжелую синхронизацию, чтобы предотвратить удаление программой обновления структуры данных из-под них. Причина в том, что программы обновления на основе блокировки обычно обновляют данные на месте и поэтому должны исключать читателей. Напротив, программы обновления на основе RCU обычно используют тот факт, что запись в одиночные выровненные указатели является атомарной на современных ЦП, что позволяет атомарную вставку, удаление и замену данных в связанной структуре без нарушения работы считывателей. Затем параллельные считыватели RCU могут продолжить доступ к старым версиям и могут обойтись без атомарных инструкций чтения-изменения-записи, барьеров памяти и промахов кеша, которые так дороги в современных компьютерных системах SMP, даже при отсутствии блокировка конкуренции. Облегченный характер примитивов на стороне чтения RCU обеспечивает дополнительные преимущества помимо отличной производительности, масштабируемости и отклика в реальном времени. Например, они обеспечивают невосприимчивость к большинству условий взаимоблокировки и блокировки.
Конечно, RCU также имеет недостатки. Например, RCU - это специализированный метод, который лучше всего работает в ситуациях, когда в основном операции чтения и мало обновлений, но часто менее применим к рабочим нагрузкам, связанным только с обновлениями. В качестве другого примера, хотя тот факт, что средства чтения и обновления RCU могут выполняться одновременно, это то, что обеспечивает облегченный характер примитивов на стороне чтения RCU, некоторые алгоритмы могут не подходить для параллельного чтения / обновления.
Несмотря на более чем десятилетний опыт работы с RCU, точная степень его применимости все еще остается предметом исследования.
Этот метод защищен US патентом на программное обеспечение 5,442,758, выданным 15 августа 1995 г. и переуступленным Sequent Computer Systems, а также на 5 608 893 (истекшие 30 марта 2009 г.), 5 727 209 (истекшие 5 апреля 2010 г.), 6 219 690 (истекшие 18 мая 2009 г.) и 6 886 162 (истекшие 25 мая 2009 г.). Патент США 4809168 с истекшим сроком действия охватывает тесно связанный метод. RCU также является темой одной претензии в иске SCO против IBM .
RCU доступен в ряде операционных систем и был добавлен в ядро Linux в октябре 2002 года. Также доступны реализации на уровне пользователя, такие как liburcu.
Реализация RCU в версии 2.6 ядра Linux входит в число более известные реализации RCU, и в оставшейся части этой статьи мы будем использовать их как основу для API RCU. Базовый API (Application Programming Interface ) довольно мал:
synchronize_rcu
не обязательно будет ждать завершения всех последующих критических разделов на стороне чтения RCU. Например, рассмотрим следующую последовательность событий:CPU 0 CPU 1 CPU 2 ----------------- -------------- ----------- --------------- 1. rcu_read_lock () 2. входит в synchronize_rcu () 3. rcu_read_lock () 4. rcu_read_unlock () 5. завершается synchronize_rcu () 6. rcu_read_unlock ()
synchronize_rcu
- это API, который должен определять, когда считыватели завершены, его реализация является ключевой для RCU. Чтобы RCU был полезен во всех ситуациях, кроме наиболее интенсивных по чтению, накладные расходы synchronize_rcu
также должны быть довольно небольшими.call_rcu
в ядре Linux.rcu_dereference
для получения указателя, защищенного RCU, который возвращает значение, которое может затем безопасно разыменовать. Он также выполняет любые директивы, требуемые компилятором или процессором, например, изменчивое приведение для gcc, загрузку memory_order_consume для C / C ++ 11 или инструкцию барьера памяти, требуемую старым процессором DEC Alpha. Значение, возвращаемое rcu_dereference
, допустимо только в пределах критического раздела на стороне чтения включающего RCU. Как и в случае с rcu_assign_pointer
, важной функцией rcu_dereference
является документирование того, какие указатели защищены RCU.Диаграмма справа показано, как каждый API взаимодействует между читателем, программой обновления и реклаймером.
Инфраструктура RCU наблюдает временную последовательность вызовов rcu_read_lock
, rcu_read_unlock
, synchronize_rcu
и call_rcu
, чтобы определить, когда (1) вызовы synchronize_rcu
могут возвращаться своим вызывающим абонентам и (2) могут быть вызваны обратные вызовы call_rcu
. В эффективных реализациях инфраструктуры RCU интенсивно используется пакетная обработка, чтобы компенсировать накладные расходы, связанные с использованием соответствующих API-интерфейсов.
RCU имеет чрезвычайно простые «игрушечные» реализации, которые могут помочь понять RCU. В этом разделе представлена одна такая «игрушечная» реализация, которая работает в среде без вытеснения.
void rcu_read_lock (void) {} void rcu_read_unlock (void) {} void call_rcu (void (* callback) (void *), void * arg) {// добавляем пару обратный вызов / аргумент в список} void synchronize_rcu (void) {int cpu, ncpus = 0; for_each_cpu (cpu) schedule_current_task_to (cpu); для каждой записи в списке call_rcu entry->callback (entry->arg); }
В примере кода rcu_assign_pointer
и rcu_dereference
можно игнорировать, ничего не теряя. Однако они необходимы для подавления вредоносной оптимизации компилятора и предотвращения переупорядочения доступа ЦП.
#define rcu_assign_pointer (p, v) ({\ smp_wmb (); / * Упорядочить предыдущие записи. * / \ ACCESS_ONCE (p) = (v); \}) #define rcu_dereference (p) ({\ typeof ( p) _value = ACCESS_ONCE (p); \ smp_read_barrier_depends (); / * nop на большинстве архитектур * / \ (_value); \})
Обратите внимание, что rcu_read_lock
и rcu_read_unlock
ничего не делать. В этом сильная сторона классического RCU в ядре без вытеснения: накладные расходы на стороне чтения равны нулю, поскольку smp_read_barrier_depends ()
- пустой макрос на всех процессорах, кроме DEC Alpha ; такие барьеры памяти не нужны на современных процессорах. Макрос ACCESS_ONCE ()
- это изменчивое приведение, которое в большинстве случаев не генерирует дополнительный код. И нет никакого способа, чтобы rcu_read_lock
мог участвовать в цикле взаимоблокировки, приводить к тому, что процесс реального времени пропускает свой крайний срок планирования, ускоряет инверсию приоритета или приводит к высокому конфликт блокировки. Однако в этой игрушечной реализации RCU блокировка в критической секции RCU на стороне чтения недопустима, так же как и блокировка с сохранением чистой спин-блокировки.
Реализация synchronize_rcu
перемещает вызывающего синхронизацию_cpu на каждый ЦП, таким образом блокируя, пока все ЦП не смогут выполнить переключение контекста. Напомним, что это среда без вытеснения и что блокировка в критической секции на стороне чтения RCU является незаконной, что означает, что не может быть точек вытеснения в критической секции RCU на стороне чтения. Следовательно, если данный ЦП выполняет переключение контекста (для планирования другого процесса), мы знаем, что этот ЦП должен завершить все предыдущие критические секции на стороне чтения RCU. После того, как все процессоры выполнили переключение контекста, все предыдущие критические разделы на стороне чтения RCU будут завершены.
Хотя RCU можно использовать по-разному, очень распространенное использование RCU аналогично блокировке чтения-записи. Следующий экран с параллельным отображением кода показывает, насколько тесно могут быть связаны блокировка чтения-записи и RCU.
/ * блокировка чтения-записи * / / * RCU * / 1 struct el {1 struct el {2 struct list_head lp ; 2 struct list_head lp; 3 длинных ключа; 3 длинных ключа; 4 spinlock_t мьютекс; 4 spinlock_t мьютекс; 5 int data; 5 int data; 6 / * Другие поля данных * / 6 / * Другие поля данных * / 7}; 7}; 8 DEFINE_RWLOCK (listmutex); 8 DEFINE_SPINLOCK (listmutex); 9 LIST_HEAD (голова); 9 LIST_HEAD (голова); 1 поиск int (длинный ключ, int * результат) 1 поиск int (длинный ключ, int * результат) 2 {2 {3 struct el * p; 3 struct el * p; 4 4 5 read_lock (listmutex); 5 rcu_read_lock (); 6 list_for_each_entry (p, head, lp) {6 list_for_each_entry_rcu (p, head, lp) {7 if (p->key == key) {7 if (p->key == key) {8 * result = p->данные; 8 * результат = p->данные; 9 read_unlock (listmutex); 9 rcu_read_unlock (); 10 возврат 1; 10 возврат 1; 11} 11} 12} 12} 13 read_unlock (listmutex); 13 rcu_read_unlock (); 14 возврат 0; 14 возврат 0; 15} 15} 1 int delete (длинный ключ) 1 int delete (длинный ключ) 2 {2 {3 struct el * p; 3 struct el * p; 4 4 5 write_lock (listmutex); 5 spin_lock (listmutex); 6 list_for_each_entry (p, head, lp) {6 list_for_each_entry (p, head, lp) {7 if (p->key == key) {7 if (p->key == key) {8 list_del (p->lp); 8 list_del_rcu (p->lp); 9 write_unlock (listmutex); 9 spin_unlock (listmutex); 10 synchronize_rcu (); 10 kfree (п); 11 kfree (p); 11 возврат 1; 12 возврат 1; 12} 13} 13} 14} 14 write_unlock (listmutex); 15 spin_unlock (listmutex); 15 возврат 0; 16 возврат 0; 16} 17}
Различия между двумя подходами невелики. Блокировка на стороне чтения перемещается в rcu_read_lock
и rcu_read_unlock
, блокировка на стороне обновления перемещается от блокировки чтения-записи к простой спин-блокировке, а synchronize_rcu
предшествует kfree
.
Однако есть одна потенциальная загвоздка: критические секции на стороне чтения и на стороне обновления теперь могут работать одновременно. Во многих случаях это не будет проблемой, но все равно необходимо тщательно проверить. Например, если несколько независимых обновлений списка должны рассматриваться как одно атомарное обновление, преобразование в RCU потребует особого внимания.
Кроме того, наличие synchronize_rcu
означает, что версия RCU delete
теперь может блокироваться. Если это проблема, call_rcu
можно использовать как call_rcu (kfree, p)
вместо synchronize_rcu
. Это особенно полезно в сочетании с подсчетом ссылок.
Методы и механизмы, похожие на RCU, были изобретены независимо несколько раз:
Бауэр, RT, (июнь 2009 г.), «Оперативная проверка релятивистской программы» Технический отчет PSU TR-09-04 (http://www.pdx.edu/sites /www.pdx.edu.computer-science/files/tr0904.pdf )