Библиотека сайта rus-linux.net
Цилюрик О.И. Модули ядра Linux | ||
Назад | Внутренние механизмы ядра | Вперед |
Множественное блокирование
В системах с большим количеством блокировок (ядро именно такая система), необходимость проведения более чем одной блокировки за раз не является необычной для кода. Если какие-то операции должны быть выполнены с использованием двух различных ресурсов, каждый из которых имеет свою собственную блокировку, часто нет альтернативы, кроме получения обоих блокировок. Однако, получение множества блокировок может быть крайне опасным:
DEFINE_SPINLOCK( lock1, lock2 ); ... spin_lock ( &lock1 ); /* 1-й фрагмент кода */ spin_lock ( &lock2 ); ... spin_lock ( &lock2 ); /* где-то в совсем другом месте кода... */ spin_lock ( &lock1 );
- такой образец кода, в конечном итоге, когда-то обречён на бесконечное блокирование (dead lock).
Если есть необходимость захвата нескольких блокировок, то единственной возможностью есть а). один тот же порядок захвата, б). и освобождения блокировок, в). порядок освобождения обратный порядку захвата, и г). так это должно выглядеть в каждом из фрагментов кода. В этом смысле предыдущий пример может быть переписан так:
spin_lock ( &lock1 ); /* так должно быть везде, где использованы lock1 и lock2 */ spin_lock ( &lock2 ); /* ... здесь выполняется действие */ spin_unlock ( &lock2 ); spin_unlock ( &lock1 );
На практике обеспечить такую синхронность работы с блокировками в различных фрагментах кода крайне проблематично! (потому, что это может касаться фрагментов кода разных авторов).
Предписания порядка выполнения
Механизмы, предписывающие порядок выполнения кода, к синхронизирующим механизмам относятся весьма условно, они не являются непосредственно синхронизирующими механизмами, но рассматриваются всегда вместе с ними (по принципу: надо же их где-то рассматривать?).
Одним из таких механизмов являются определённые в <linux/compiler.h> макросы likely() и unlikely(), например:
if( unlikely() ) { /* сделать нечто редкостное */ };
Или:
if( likely() ) { /* обычное прохождение вычислений */ } else { /* что-то нетрадиционное */ };
Такие предписания а). имеют целью оптимизацию компилированного кода, б). делают код более читабельным, в). недопустимы (не определены) в пространстве пользовательского кода (только в ядре).
Примечание: подобные оптимизации становятся актуальными с появлением в процессорах конвейерных вычислений с предсказыванием.
Другим примером предписаний порядка выполнения являются барьеры в памяти, препятствующие в процессе оптимизации переносу операций чтения и записи через объявленный барьер. Например, при записи фрагмента кода:
a = 1; b = 2;
- порядок выполнения операция, вообще то говоря, непредсказуем, причём последовательность (во времени) выполнения операций может изменить а). компилятор из соображений оптимизации, б). процессор (периода выполнения) из соображений аппаратной оптимизации работы с шиной памяти. В этом случае это совершенно нормально, более того, даже запись операторов:
a = 1; b = a + 1;
- будет гарантировать отсутствие перестановок в процессе оптимизации, так как компилятор «видит» операции в едином контексте (фрагменте кода). Но в других случаях, когда операции производятся из различных мест кода нужно гарантировать, что они не будут перенесены через определённые барьеры. Операции (макросы) с барьерами объявлены в </asm-generic/system.h>, на сегодня все они (rmb(), wmb(), mb(), ...) определены одинаково:
#define mb() asm volatile ("": : :"memory")
Все они препятствуют выполнению операций с памятью после такого вызова до завершения всех операций, записанных до вызова.
Ещё один макрос объявлен в <linux/compiler.h>, он препятствует компилятору при оптимизации переставлять операторы до вызова и после вызова :
void barrier( void );
Предыдущий раздел: | Оглавление | Следующий раздел: |
Блокировки | Обработка прерываний |