Ловушки блокировок

Предыдущая  Содержание  Следующая V*D*V

Многолетний опыт работы с блокировками - опыт, который предшествует Linux - показал, что может быть очень трудно правильно работать с блокировками. Управление конкуренцией является по своей сути сложной задачей и существуют множество способов допустить ошибки. В этом разделе мы бросим быстрый взгляд на то, что может пойти не так.

Сомнительные правила

Как уже было сказано выше, правильная схема блокирования требует чётких и ясных правил. При создании ресурса, к которому может быть одновременный доступен, вы должны определить, какие блокировки будут контролировать этот доступ. Блокировка должна быть действительно заложена в начале; модификация кода потом может быть трудным делом. Время, потраченное в начале, обычно щедро возмещается во время отладки.

 

Когда вы пишете код, вам, несомненно, потребуются несколько функций, которые требуют доступа к структурам, защищённым определённой блокировкой. На этом этапе вы должны быть осторожны: если одна функция приобретает блокировку и затем вызывает другую функцию, которая также пытается забрать эту же блокировку, ваш код взаимоблокируется. Ни семафоры, ни спин-блокировки не позволят получить блокировку второй раз; если вы делаете такую попытку, всё просто зависнет.

 

Чтобы сделать ваши блокировки работающими правильно, вам придётся написать несколько функций в предположении, что вызвавший их уже приобрёл соответствующую блокировку(ки). Как правило, только внутренние, статические функции могут быть написаны таким образом; функции, вызываемые снаружи, должны обработать блокировку в явном виде. Когда вы пишете внутренние функции, которые делают предположения о блокировке, создайте себе (и всем остальным, кто работает с вашим кодом) справку и документ с этими предположениями в явном виде. Может быть очень трудно вернуться месяцы спустя и выяснить, требуется ли удерживать блокировку при вызове данной функции или нет.

 

В случае со scull, дизайнерским решением было принято требовать, чтобы все функции, вызываемые непосредственно системными вызовами, запрашивали для доступа семафор соответствующей структуры устройства. Все внутренние функции, которые только вызываются другими функциями scull, могут считать, что семафор был получен правильно.

Правила очерёдности блокировки

В системах с большим количеством блокировок (и ядро становится такой системой), необходимость проведения более чем одной блокировки за раз не является необычной для кода. Если какие-то операции должны быть выполнены с использованием двух различных ресурсов, каждый из которых имеет свою собственную блокировку, часто нет альтернативы, кроме получения обоих блокировок. Однако, получение множества блокировок может быть опасным. Если у вас есть две блокировки, названных Lock1 и Lock2, и коду необходимо получить их в одно и то же время, вы имеете потенциальную взаимоблокировку. Только представьте, один поток блокирует Lock1, а другой одновременного забирает Lock2. Затем каждый поток пытается получить ту, которую не имеет. Оба потоки будут блокированы.

 

Решение этой проблемы, как правило, простое: если требуется получить несколько блокировок, они всегда должны быть получены в одинаковом порядке. Придерживаясь этого правила, простых взаимоблокировок, похожих на описанную выше, можно избежать. Однако, следовать правилам порядку блокирования может быть легче сказать, чем сделать. Очень редко, когда такие правила не используются на самом деле где-то ещё. Часто лучшее, что вы можете сделать, это посмотреть, что делает другой код.

 

Могут помочь несколько эмпирических правил. Если вам необходимо получить блокировку, которая является локальной для вашего кода (скажем, блокировка устройства), наряду с блокировкой, принадлежащей к более центральной часть ядра, сделайте вашу блокировку первой. Если у вас есть комбинация семафоров и спин-блокировок, вы должны, конечно, получить семафор(ы) в первую очередь; вызов down (которая может заснуть) во время удержания спин-блокировки является серьёзной ошибкой. Но прежде всего постарайтесь избегать ситуаций, когда вам необходимо более одной блокировки.

Точечное блокирование против грубого

Первым ядром Linux, поддерживающим многопроцессорные системы, было 2.0; оно содержало ровно одну спин-блокировку. Большая блокировка ядра оборачивала всё ядро в одну большую критическую секцию; только один процессор мог выполнять код ядра в любой момент времени. Эта блокировка решала проблему конкуренции достаточно хорошо, чтобы позволить разработчикам ядра обратиться к решению всех других вопросов, связанных с поддержкой SMP. Но это не очень масштабируемо. Даже двухпроцессорная система могла бы потратить значительное количество времени просто в ожидании большой блокировки ядра. Производительность четырёхпроцессорной системы даже не приближалась к таковой у четырёх независимых машин.

 

Таким образом, последующие версии ядра включают более точечную блокировку. В версии 2.2 одна спин-блокировка контролировала доступ к блоку подсистемы ввода/вывода, другая работала для сети и так далее. Современное ядро может содержать тысячи блокировок, каждая защищает один небольшой ресурс. Такая точечная блокировка может быть хороша для масштабируемости; это позволяет каждому процессору выполнять свою собственную задачу не соперничая за блокировки, используемые другими процессорами. Мало кто пропустил большую блокировку ядра. (* Эта блокировка всё ещё существует в версии 2.6, хотя она охватывает сейчас очень малую часть ядра. Если вы случайно натолкнулись на вызов lock_kernel, вы нашли большую блокировку ядра. Однако, даже не думайте об её использовании в любом новом коде.)

 

Однако, точечное блокирование связано с определёнными затратами. В ядре тысячи блокировок, может быть очень трудно узнать, в каких блокировках вы нуждаетесь, и в каком порядке вы должны их получать для выполнения определённой операции. Помните, что ошибки блокировки может быть очень сложно найти; больше блокировок предоставляют больше возможностей для проползания в ядро действительно неприятных ошибок блокировки. Точечная блокировка может привести к такому уровню сложности, что в долгосрочном плане может иметь большие негативные последствия для сопровождения ядра.

 

Блокировка в драйвере устройства, как правило, сравнительно проста; вы можете иметь одну блокировку, охватывающую всё, что вы делаете, или вы можете создать одну блокировку для каждого управляемого устройства. Как правило, вам следует начать с относительно грубой блокировки, если есть реальные основания полагать, что конкуренция может быть проблемой. Боритесь с желанием оптимизировать преждевременно; реальные ограничения производительности часто появляются в неожиданных местах.

 

Если вы подозреваете, что соперничество за блокировку сказывается на производительности, вы можете найти полезным инструмент lockmeter. Этот патч (доступен на http://oss.sgi.com/projects/lockmeter/) - инструменты ядра для измерения времени, затраченного на ожидание в блокировках. Глядя на отчёт, вы сможете быстрее определить, действительно ли соперничество за блокировку является проблемой или нет.

Предыдущая  Содержание  Следующая