И наконец, показанный ниже код осуществляет запись.
write_lock(&mr_rwlock);
/* критический участок (чтение и запись) ... */
write_unlock{&mr_rwlock);
Обычно считывание и запись информации осуществляются в разных участках кода, как это показано в данном примере.
Заметим, что блокировку, захваченную для чтения, нельзя "повышать" до блокировки, захваченной для записи. В следующем коде
read_lock(&mr_rwlock);
write_lock(&mr_rwlock);
возникнет самоблокировка, так как при захвате блокировки на запись будет выполняться периодическая проверка, пока все потоки, которые захватили блокировку для чтения, ее не освободят; это касается и текущего потока. Если в каком-либо месте будет необходима запись, то нужно сразу же захватывать блокировку для записи. Если в вашем коде нет четкого разделения на код, который осуществляет считывание, и код, который осуществляет запись, то нет необходимости использовать блокировки чтения- записи. В таком случае оптимальным будет использование обычных спин-блокировок.
Несколько потоков чтения безопасно могут удерживать одну и ту же блокировку чтения-записи. На самом деле один поток также может безопасно рекурсивно захватывать одну и ту же блокировку для чтения. Это позволяет выполнить полезную и часто используемую оптимизацию. Если в обработчиках прерываний осуществляется только чтение и не выполняется запись, то можно "смешивать" использование блокировок с запрещением прерываний и без запрещения. Для защиты данных при чтении можно использовать функцию read_lock()
вместо read_lock_irqsave()
. При обращении к данным для записи все равно необходимо запрещать прерывания, например использовать функцию write_lock_irqsave()
, так как в обработчике прерывания может возникнуть взаимоблокировка в связи с ожиданием захвата блокировки на чтение при захваченной блокировке на запись. В табл. 9.4 показан полный список средств работы с блокировками чтения-записи.
Таблица 9.4. Список функций работы со спин-блокировками чтения-записи
Функция | Описание |
---|---|
read_lock() |
Захватить указанную блокировку на чтение |
read_lock_irq() |
Запретить прерывания на локальном процессоре и захватить указанную блокировку на чтение |
read_lock_irqsave() |
Сохранить состояние системы прерываний на текущем процессоре, запретить прерывания на локальном процессоре и захватить указанную блокировку на чтение |
read_unlock() |
Освободить указанную блокировку, захваченную для чтения |
read_unlock_irq() |
Освободить указанную блокировку, захваченную для чтения, и разрешить прерывания на локальном процессоре |
read_unlock_irqrestore() |
Освободить указанную блокировку, захваченную для чтения, и восстановить состояние системы прерываний в указанное значение |
write_lock() |
Захватить заданную блокировку на запись |
write_lock_irq() |
Запретить прерывания на локальном процессоре и захватить указанную блокировку на запись |
write_lock_irqsave() |
Сохранить состояние системы прерываний на текущем процессоре, запретить прерывания на локальном процессоре и захватить указанную блокировку на запись |
write_unlock() |
Освободить указанную блокировку, захваченную для записи |
write_unlock_irq() |
Освободить указанную блокировку, захваченную для записи, и разрешить прерывания на локальном процессоре |
write_unlock_irqrestore() |
Освободить указанную блокировку, захваченную для записи, и восстановить состояние системы прерываний в указанное значение |
write_trylock() |
Выполнить попытку захватить заданную блокировку на запись и в случае неудачи возвратить ненулевое значение |
rw_lock_init() |
Инициализировать объект типа rwlock_t в заданной области памяти |
rw_is_locked() |
Возвратить ненулевое значение, если указанная блокировка захвачена, иначе возвратить нуль |
Еще один факт, который необходимо принимать во внимание при работе с блокировками чтения-записи в операционной системе Linux, — это то, что блокировка на чтение всегда имеет большее преимущество по сравнению с блокировкой на запись. Если блокировка захвачена на чтение и поток записи ожидает на ее освобождение, то все потоки, которые будут пытаться захватить блокировку на чтение, будут добиваться успеха. Поток записи, который периодически проверяет на освобождение блокировки, не сможет захватить блокировку, пока все потоки чтения эту блокировку не освободят. Поэтому большое количество потоков чтения будет приводить к "подвисанию" ожидающих потоков записи. Это важное обстоятельство всегда нужно помнить при разработке схемы блокировок.
Спин-блокировки обеспечивают очень быстрые и простые блокировки. Выполнение постоянных проверок в цикле является оптимальным, когда блокировки захватываются на очень короткое время и код не может переходить в состояние ожидания (например, в обработчиках прерываний). Если же период времени ожидания на освобождение блокировки может быть большим или имеется потенциальная возможность перехода в состояние ожидания при захваченной блокировке, то задача может быть решена с помощью семафоров.
Семафоры
В операционной системе Linux семафоры (semaphore) — это блокировки, которые переводят процессы в состояние ожидания. Когда задание пытается захватить семафор, который уже удерживается, семафор помещает это задание в очередь ожидания (wait queue) и переводит это задание в состояние ожидания (sleep). Когда процессы[49], которые удерживают семафор, освобождают блокировку, одно из заданий очереди ожидания возвращается к выполнению и может захватить семафор.
Давайте снова возвратимся к аналогии двери и ключа. Когда человек, который хочет открыть дверь, подходит к двери, он может захватить ключ и войти в комнату. Отличие состоит в том, что произойдет, если к двери подойдет другой человек. В этом случае он записывает свое имя в список на двери и ложится поспать.
Когда человек, который находился внутри комнаты, выходит из нее, он проверяет список на двери. Если в списке есть чье-либо имя, то он выбирает первое из имен списка, дает соответствующему человеку пинка, будит его и позволяет войти в комнату. Таким образом, ключ (т.е. семафор) позволяет только одному человеку (т.е. потоку) находиться в комнате (т.е. в критическом разделе) в один момент времени. Если комната занята, то, вместо того чтобы периодически проверять, человек записывает свое имя в список (т.е. в очередь ожидания) и засыпает (т.е. блокируется в очереди ожидания и переходит в приостановленное состояние), что позволяет процессору выполнять некоторый другой код. Такой режим работы позволяет лучше использовать процессор, чем в случае спин-блокировок, так как при этом не тратится процессорное время на выполнение периодических проверок в цикле. Тем не менее использование семафоров, по сравнению со спин-блокировками, связано со значительно большими накладными расходами.
49
Как будет показано дальше, несколько процессов могут при необходимости одновременно удерживать один семафор.