netlib.narod.ru | < Назад | Оглавление | Далее > |
Последним примитивом параллельного программирования, который важен для описания материала этой главы, является блокировка в цикле. Принцип блокировки в цикле состоит в применении жесткого цикла для повторного осуществления попытки захватить ресурс (блокировку), пока не будет достигнут успех. Это часто осуществляется с применением циклических конструкций, то есть многократного повторения операций типа проверки и установки, до тех пор, пока не будет получена блокировка.
Этот примитив во многом напоминает двоичный семафор, и действительно, таковым и является. Единственное принципиальное различие между блокировкой в цикле и двоичным семафором состоит в том, что ожидание семафора не всегда выполняется в цикле; можно попытаться захватить семафор и просто отказаться на время от этой попытки, если она сразу же не удалась. Как следствие, блокировку в цикле можно реализовать, вложив код семафора в цикл. Однако, поскольку блокировки в цикле представляют собой частный случай семафоров, они имеют более эффективную реализацию.
Переменная блокировки в цикле, в которой выполняется проверка и установка бита, всегда имеет тип spinlock_t (строка 12785). Применяется только самый младший бит объекта spinlock_t; он равен 0, если блокировка доступна, и 1, если она занята. Блокировка в цикле в ее объявлении инициализируется в значение SPIN_LOCK_UNLOCKED (строка 12789); иначе, ее можно инициализировать с помощью функции spin_lock_init (строка 12791). В обоих этих случаях член блокировки spinlock_t устанавливается в 0, то есть он разблокирован.
Обратите внимание, что в комментарии в строке 12795 вкратце упоминается и тут же отвергается стремление к справедливому подходу, который исключает возможность только что описанного перевода на голодный паек (считается, что переводить на голодный паек процессор или процесс — «несправедливо»).
Макрокоманды блокировки и разблокировки примитива блокировки в цикле построены на основе функций spin_lock_string и spin_unlock_string, поэтому в настоящем разделе подробно рассматриваются только эти функции. В других макрокомандах только добавляется блокировка и разблокировка прерываний, если они применяются.
12805: Эта макрокоманда имеет код, общий для всех макрокоманд блокировки примитива блокировки в цикле. Она также используется в предназначенных для архитектуры х86 версиях функций lock_kernel и unlock_kernel (которые не включены в эту книгу, хотя их универсальные версии — включены; см. строки 10174 и 10182).
12807: Попытка проверить и установить младший бит блокировки в цикле при заблокированной шине памяти, чтобы эта операция была атомарной по отношению к любым другим попыткам доступа к той же блокировке в цикле.
12808: При успешном завершении этой операции управление передается дальше; в ином случае, в макрокоманде spin_lock_string выполняется переход вперед к строке 12810 (команда btsl помещает старое значение бита во флажок Carry процессора, и поэтому здесь применяется команда jc). Это тот же прием, с которым мы уже встречались три раза: цель перехода находится в отдельной секции ядра.
12811: Команда стоит в жестком цикле, повторно проверяя младший бит блокировки в цикле. Обратите внимание, что команды btsl и testb интерпретируют свой первый операнд по-разному: для btsl это — битовая позиция, а для testb — битовая маска. Следовательно, в строке 12811 происходит проверка того же бита, который макрокоманда spin_lock_string пыталась (и не смогла) установить в строке 12807, даже несмотря на то, что в первом случае используется операнд $0, а во втором — операнд $1.
12813: Бит был очищен, поэтому макрокоманда spin_lock_string должна выполнить еще одну попытку его захватить. Она переходит назад к строке 12806. Этот код можно было бы упростить только до двух команд и префикса lock:
1: lock ; btsl $0, %0 jc 1b
Однако при использовании этой более простой версии производительность системы значительно снизится, поскольку блокировка шины памяти будет происходить при каждой итерации цикла. Версия, применяемая в ядре, длиннее, но она позволяет другим процессорам работать более эффективно, поскольку в ней предусмотрена блокировка шины памяти только в том случае, если есть основание надеяться, что можно будет захватить блокировку.
12816: Тривиальный случай: просто сброс бита блокировки примитива блокировки в цикле.
Частным случаем примитива блокировки в цикле является блокировка в цикле для чтения/записи. Основной принцип состоит в том, что в некоторых случаях необходимо обеспечить нескольким процессам доступ для чтения к одному и тому же объекту, но на то время, пока к объекту осуществляется доступ для записи, к нему не должны быть допущены другие процессы чтения или записи.
По такому же принципу, который применялся в блокировках в цикле на основе объекта типа spinlock_t, блокировки в цикле для чтения/записи представлены объектом типа rwlock_t (строка 12853), который может быть инициализирован в объявлении с помощью RW_LOCK_UNLOCKED (строка 12858). Макрокомандами самого низкого уровня, предназначенными для работы с rwlock_t, являются read_lock, read_unlock, write_lock и write_unlock; они рассматриваются в настоящем разделе. Все макрокоманды, которые будут описаны вслед за этими макрокомандами, и которые строятся на их основе, станут вполне понятными после изучения этих первых четырех макрокоманд.
В комментарии в строке 12860 указано, что член lock объекта rwlock_t имеет отрицательное значение, если какой-то процесс владеет блокировкой записи. Он равен 0, если нет ни процессов чтения, ни процессов записи, и положителен, если есть процессы чтения, но нет процессов записи; в этом случае значение lock показывает число процессов чтения.
12867: Начинает работу с атомарного увеличения значения члена lock объекта rwlock_t. Это рискованная операция и она может быть отменена.
12868: Если значение после увеличения остается отрицательным, какой-то процесс держит эту блокировку записи или, по крайней мере, какой-то процесс пытается ее приобрести. Макрокоманда read_lock переходит вперед к строке 12870 (обратите внимание — в другую секцию ядра). В ином случае, не существует ни одного процесса записи (хотя могут быть или не быть другие процессы чтения — это просто не имеет значения), поэтому можно перейти к коду блокировки чтения.
12870: Имеется процесс записи. Макрокоманда read_lock отменяет результат увеличения в строке 12867.
12871: Циклическое повторение в ожидании, когда член lock объекта rwlock_t станет равным 0 или положительным.
12878: Тривиальный случай: просто уменьшение счетчика.
12883: Выдача сигнала о том, что какой-то процесс хочет приобрести блокировку записи: проверка и установка знакового бита блокировки для обеспечения отрицательного значения члена lock.
12884: Если знаковый бит уже установлен, какой-то еще процесс владеет блокировкой записи; макрокоманда write_lock переходит вперед к строке 12889 (которая, как и прежде, находится в другой секции ядра).
12885: Больше никто не пытался получать блокировку записи, но процессы чтения все еще могут существовать. Поскольку знаковый бит установлен, никакой иной процесс чтения не сможет приобрести эту блокировку чтения, но макрокоманда write_lock все еще обязана ждать, пока не исчезнут все прочие существующие процессы чтения. Она начинает работу с проверки того, установлен ли какой-либо из 31 бита в младших разрядах, что может служить свидетельством того, что значение lock перед этим было положительным. Если нет, то значение lock перед инверсией знакового бита было равно 0, а это значит, что нет процессов чтения, следовательно, данный процесс записи может безопасно продолжить свою работу, поэтому управление просто переходит дальше. Однако, если среди 31 бита в младших разрядах был установлен хоть один бит, это значит, что имеются процессы чтения, поэтому макрокоманда write_lock переходит к строке 12888, чтобы там ожидать завершения их работы.
12888: Данный процесс является единственным процессом записи, но есть еще процессы чтения. Макрокоманда write_lock на данный момент очищает знаковый бит (она снова захватит его позднее). Любопытно отметить, что эти манипуляции со знаковым битом не нарушают правильности операций процессов чтения с членом lock. Рассмотрим в качестве примера приведенную ниже последовательность событий:
Таким образом, попытки чтения и записи могут чередоваться в любом порядке, не влияя на правильность результата.
12889: Циклическое выполнение в ожидании, пока значение счетчика не упадет до 0, то есть в ожидании, пока не уйдут все процессы чтения. В действительности, нулевое значение показывает, что не только ушли все процессы чтения, но и никто иной еще не приобрел блокировку записи.
12891: По-видимому, ушли все процессы чтения и записи; макрокоманда write_lock начинает все сначала и снова захватывает блокировку записи.
12896: Тривиальный случай: просто сброс знакового бита.
netlib.narod.ru | < Назад | Оглавление | Далее > |