На вытесняемом ядре SMP rcu_read_lock
компилирует следующее:
current->rcu_read_lock_nesting++;
barrier();
Поскольку barrier
является директивой компилятора, которая ничего не компилирует.
Итак, согласно техническому документу по заказу памяти Intel X86-64:
Loads may be reordered with older stores to different locations
почему реализация на самом деле в порядке?
Рассмотрим следующую ситуацию:
rcu_read_lock();
read_non_atomic_stuff();
rcu_read_unlock();
Что предотвращает «просачивание» read_non_atomic_stuff
вперед мимо rcu_read_lock
, заставляя его работать одновременно с кодом восстановления, работающим в другом потоке?
@HadiBrais Но разве загрузки внутри критической секции на стороне чтения не будут переупорядочены аппаратно с помощью rcu_read_lock
, который является загрузкой и сохранением в счетчике? Если да, то почему это нормально?
@HadiBrais: read_non_atomic_stuff()
обращается к памяти разные, чем current->rcu_read_lock_nesting
. Да, возможно изменение порядка StoreLoad, если наблюдатель на другом ядре прочитает его, не приняв особых мер предосторожности. Но эти особые меры предосторожности являются частью пункта RCU.
Для наблюдателей на других CPU этому ничто не мешает. Вы правы, изменение порядка StoreLoad части магазина ++
может сделать его видимым во всем мире после некоторых ваших загрузок.
Таким образом, мы можем заключить, что current->rcu_read_lock_nesting
когда-либо наблюдался только кодом, выполняющимся на этом ядре, или который удаленно запускал барьер памяти на этом ядре, будучи запланированным здесь, или с помощью специального механизма, позволяющего заставить все ядра выполнять барьер в обработчике для межпроцессорное прерывание (IPI). например аналогично системному вызову membarrier()
пользовательского пространства.
Если это ядро начнет выполнять другую задачу, эта задача гарантированно увидит операции этой задачи в программном порядке. (Поскольку оно находится на одном и том же ядре, а ядро всегда видит свои операции в порядке.) Кроме того, переключение контекста может включать в себя полный барьер памяти, поэтому задачи можно возобновить на другом ядре, не нарушая однопоточную логику. (Это позволит любому ядру безопасно просматривать rcu_read_lock_nesting
, когда эта задача/поток нигде не выполняется.)
Обратите внимание, что ядро запускает одну задачу RCU для каждого ядра вашей машины; например ps
выходные данные показывают [rcuc/0]
, [rcuc/1]
, ..., [rcu/7]
на моем четырехъядерном процессоре 4c8t. Предположительно, они являются важной частью этого дизайна, который позволяет читателям не ждать и не иметь никаких препятствий.
Я не вникал в подробности RCU, но один из "игрушечных" примеров в
https://www.kernel.org/doc/Documentation/RCU/whatisRCU.txt — это «классический RCU», который реализует synchronize_rcu()
как for_each_possible_cpu(cpu) run_on(cpu);
, чтобы заставить средство восстановления выполняться на каждом ядре, которое могло выполнить операцию RCU ( то есть каждое ядро). Как только это будет сделано, мы узнаем, что где-то здесь в результате переключения должен был возникнуть полный барьер памяти.
Так что да, RCU не следует классическому методу, когда вам нужен полный барьер памяти (включая StoreLoad), чтобы ядро ждало, пока не станет видно первое хранилище, прежде чем выполнять какие-либо операции чтения. RCU позволяет избежать накладных расходов, связанных с полным барьером памяти на пути чтения. Это одна из главных привлекательных черт, помимо избежания раздора.
Хм?
barrier()
является барьером памяти компилятора, поэтому никакие обращения внутри области rcu не могут пройти функцию блокировки или разблокировки. Более того, как ясно сказано в руководстве, загрузки не могут быть переупорядочены со старыми хранилищами в местоположения такой же.