Вопрос «Насколько быстро можно установить и снять блокировку записи?» не имеет однозначного ответа, поскольку скорость этих операций зависит от структуры, используемой ядром для хранения таких блокировок, и от местоположения нашей блокировки внутри нее. Мы рассмотрим данную структуру чуть ниже, но сначала следует остановиться на требованиях, которые к ней предъявляются:
• ядро должно иметь возможность объединить новую блокировку с любыми существующими (удерживаемыми тем же процессом), если они имеют тот же режим и располагаются по любую сторону от нее;
• новая блокировка может полностью заменить собой одну или несколько существующих блокировок, удерживаемых вызывающим процессом. Все они должны быть доступны, чтобы ядро могло легко их найти;
• при создании новой блокировки посреди существующей, которая имеет другой режим, процедура разделения этой существующей блокировки (см. рис. 51.3) должна быть простой.
Структура данных ядра, используемая для хранения информации о блокировках, спроектирована специально, чтобы удовлетворить эти требования. У каждого открытого файла есть связанный список блокировок, содержимое которого упорядочено по идентификатору процесса, а затем по начальному сдвигу. Пример такого списка показан на рис. 51.6.
В данном списке также хранятся блокировки, созданные с помощью вызова flock(), и сведения об аренде открытого файла (мы коснемся темы аренды файлов в разделе 51.5, во время обсуждения файла /proc/locks). Однако блокировки таких типов обычно куда менее многочисленны и, следовательно, шансов повлиять на производительность у них тоже меньше, поэтому мы не станем останавливаться на них отдельно.
Рис. 51.6.
Каждый раз при добавлении в эту структуру данных новой блокировки ядро должно проверять наличие конфликтов с любыми существующими блокировками, установленными для того же файла. Поиск конфликтов выполняется последовательно, начиная с первого элемента списка. Большое количество блокировок может быть произвольным образом распределено между множеством процессов, поэтому время, которое уходит на добавление или удаление блокировок, можно считать прямо пропорциональным их общему количеству для конкретного файла.
51.3.8. Семантика наследования и снятия блокировок
Семантика наследования и снятия блокировок, созданных с помощью вызова fcntl(), существенно отличается от аналогичной семантики для блокировок на основе flock(). Обратите внимание на следующие моменты.
• Блокировки записей не наследуются дочерним процессом после выполнения fork(). Для сравнения, в случае с вызовом flock() потомок наследует ссылки на блокировки своего родителя и может их снять; если это произойдет, родитель тоже потеряет данные блокировки.
• Блокировки записей сохраняются на протяжении выполнения вызова exec() (при этом стоит учитывать влияние флага FD_CLOEXEC, описанное ниже).
• Все потоки одного процесса разделяют один и тот же набор блокировок.
• Блокировки записей связаны как с процессом, так и с индексным дескриптором (см. раздел 5.4). Неудивительно, что при завершении процесса снимаются все его блокировки. Менее очевидное следствие такой связи —
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
fd1 = open("testfile", O_RDWR);
fd2 = open("testfile", O_RDWR);
if (fcntl(fd1, cmd, &fl) == -1)
errExit("fcntl");
close(fd2);
Семантика, описанная в последнем пункте, действует независимо от способа получения тех или иных дескрипторов, ссылающихся на один и тот же файл, и от того, как они были закрыты. Например, для получения копии дескриптора открытого файла используются вызовы dup(), dup2() и fcntl(). А для его закрытия, помимо вызова close(), можно также применить операцию exec() с флагом FD_CLOEXEC или вызов dup2(), который закрывает дескриптор, указанный во втором аргументе, если тот является открытым.
Семантика наследования и снятия блокировок, установленных с помощью вызова fcntl(), — архитектурный изъян. Например, она делает проблематичным использование таких блокировок из библиотечных пакетов, так как функции библиотеки не могут исключить возможность того, что вызывающий процесс закроет дескриптор, который ссылается на заблокированный файл, и тем самым удалит установленную ими блокировку. В качестве альтернативы блокировку можно было бы связать с файловым, а не с индексным дескриптором. Но текущая семантика блокировки записей давно является устоявшейся и стандартизированной. К сожалению, это существенно ограничивает применение вызова fcntl().