Скачиваний:
56
Добавлен:
08.01.2014
Размер:
2.6 Mб
Скачать

Снятие блокировки при помощи вызова fcntl

Можно разблокировать участок файла, который был ранее заблокирован, присвоив при вызове переменной l_type значение F_UNLK. Снятие блокировки обычно используется через некоторое время после предыдущего запроса fcntl. Если еще какие-либо процессы собирались заблокировать освободившийся участок, то один из них прекратит ожидание и продолжит свою работу.

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

Например, в предыдущей программе lockit родительский процесс снял блокировку в момент выхода из программы, но вместо этого он мог осуществить эту операцию при помощи следующего кода:

(* Родительский процесс снимает блокировку перед выходом *)

writeln('Родительский процесс: снятие блокировки');

my_lock.l_type := F_UNLCK;

fcntl(fd, F_SETLK, longint(@my_lock));

if linuxerror <> 0 then

begin

perror('ошибка снятия блокировки в родительском процессе');

halt(1);

end;

Задача об авиакомпании acme Airlines

Теперь удастся разрешить конфликтную ситуацию в примере с авиакомпаний ACME Airlines. Для того, чтобы гарантировать целостность базы данных, нужно построить критический участок кода в программе acmebook следующим образом:

заблокировать соответствующий участок базы данных на запись

обновить участок базы данных

разблокировать участок базы данных

Если ни одна программа не обходит механизм блокировки, то запрос на блокировку гарантирует, что вызывающий процесс имеет исключительный доступ к критической части базы данных. Запрос на снятие блокировки снова делает доступной эту область для общего использования. Выполнение любой конкурирующей копии программы acmebook, которая пытается получить доступ к соответствующей части базы данных, будет приостановлено на стадии попытки наложения блокировки.

Текст критического участка программы можно реализовать следующим образом:

(* Набросок процедуры обновления программы acmebook *)

var

db_lock:flockrec;

.

.

.

(* Установить параметры блокировки *)

db_lock.l_type := F_WRLCK;

db_lock.l_whence := SEEK_SET;

db_lock.l_start := recstart;

db_lock.l_len := RECSIZE;

.

.

.

(* Заблокировать запись в базе, выполнение приостановится *)

(* если запись уже заблокирована *)

fcntl(fd, F_SETLKW, longint(@db_lock));

if linuxerror <> 0 then

fatal('Ошибка блокировки');

(* Код для проверки и обновления данных о заказах *)

.

.

.

(* Освободить запись для использования другими процессами *)

db_lock.l_type := F_UNLCK;

fcntl(fd, F_SETLK, longint(@db_lock));

Проверка блокировки

При неудачной попытке программы установить блокировку, задав параметр F_SETLK в вызове fcntl, вызов установит значение переменной linuxerror равным Sys_EAGAIN или Sys_EACCESS (в спецификации XSI определены оба эти значения). Если блокировка уже существует, то с помощью команды F_GETLK можно определить процесс, установивший эту блокировку:

uses linux, stdio;

.

.

.

fcntl(fd, F_SETLK, longint(@alock));

if linuxerror <> 0 then

begin

if (linuxerror = Sys EACCES) or (linuxerror = Sys_EAGAIN) then

begin

fcntl(fd, F_GETLK, longint(@b_lock));

writeln(stderr, 'Запись заблокирована процессом ', b_lock.l_pid);

end

else

perror('Ошибка блокировки');

end;

Клинч

Предположим, что два процесса, РА и РВ, работают с одним файлом. Допустим, что процесс РА блокирует участок файла SX, а процесс РВ – не пересекающийся с ним участок SY. Пусть далее процесс РА попытается заблокировать участок SY при помощи команды F_SETLKW, а процесс РВ попытается заблокировать участок SX, также используя команду F_SETLKW. Ни одна из этих попыток не будет успешной, так как процесс РА приостановит работу, ожидая, когда процесс РВ освободит участок SY, а процесс РВ также будет приостановлен в ожидании освобождения участка SX процессом РА. Если не произойдет вмешательства извне, то будет казаться, что два процесса обречены вечно находиться в этом «смертельном объятии».

Такая ситуация называется клинчем (deadlock) по очевидным причинам. Однако UNIX иногда предотвращает возникновение клинча. Если выполнение запроса F_SETLK приведет к очевидному возникновению клинча, то вызов завершается неудачей, и возвращается значение -1, а переменная linuxerror принимает значение Sys_EDEADLK. К сожалению, вызов fcntl может определять только клинч между двумя процессами, в то время как можно создать трехсторонний клинч.1 Во избежание такой ситуации сложные приложения, использующие блокировки, должны всегда задавать предельное время ожидания.

Следующий пример поможет пояснить изложенное. В точке /*А*/ программа блокирует с 0 по 9 байты файла locktest. Затем программа порождает дочерний процесс, который в точках, помеченных как /*В*/ и /*С*/, блокирует байты с 10 по 14 и пытается выполнить блокировку байтов с 0 по 9. Из-за того, что родительский процесс уже выполнил последнюю блокировку, работа дочернего будет приостановлена. В это время родительский процесс выполняет вызов sleep в течение 10 секунд. Предполагается, что этого времени достаточно, чтобы дочерний процесс выполнил два вызова, устанавливающие блокировку. После того, как родительский процесс продолжит работу, он пытается заблокировать байты с 10 по 14 в точке /*D*/, которые уже были заблокированы дочерним процессом. В этой точке возникнет опасность клинча, и вызов fcntl завершится неудачей.

(* Программа deadlock - демонстрация клинча *)

uses linux, stdio;

var

fd:longint;

first_lock, second_lock:flockrec;

begin

first_lock.l_type := F_WRLCK;

first_lock.l_whence := SEEK_SET;

first_lock.l_start := 0;

first_lock.l_len := 10;

second_lock.l_type := F_WRLCK;

second_lock.l_whence := SEEK_SET;

second_lock.l_start := 10;

second_lock.l_len := 5;

writeln(sizeof(flockrec));

fd := fdopen ('locktest', Open_RDWR);

fcntl (fd, F_SETLKW, longint(@first_lock));

if linuxerror>0 then (*A *)

fatal ('A');

writeln ('A: успешная блокировка (процесс ',getpid,')');

case fork of

-1:

(* ошибка *)

fatal ('Ошибка вызова fork');

0:

begin

(* дочерний процесс *)

fcntl (fd, F_SETLKW, longint(@second_lock));

if linuxerror>0 then (*B *)

fatal ('B');

writeln ('B: успешная блокировка (процесс ',getpid,')');

fcntl (fd, F_SETLKW, longint(@first_lock));

if linuxerror>0 then (*C *)

fatal ('C');

writeln ('C: успешная блокировка (процесс ',getpid,')');

halt (0);

end;

else

begin

(* родительский процесс *)

writeln ('Приостановка родительского процесса');

sleep (10);

fcntl (fd, F_SETLKW, longint(@second_lock));

if linuxerror>0 then (*D *)

fatal ('D');

writeln ('D: успешная блокировка (процесс ',getpid,')');

end;

end;

end.

Вот пример работы этой программы:

А: успешная блокировка (процесс 1410)

Приостановка родительского процесса

В: успешная блокировка (процесс 1411)

D: Deadlock situation detected/avoided

С: успешная блокировка (процесс 1411)

В данном случае попытка блокировки завершается неудачей в точке /*D*/, и процедура perror выводит соответствующее системное сообщение об ошибке. Обратите внимание, что после того, как родительский процесс завершит работу и его блокировки будут сняты, дочерний процесс сможет выполнить вторую блокировку.

Это пример использует процедуру fatal, которая была применена в предыдущих главах.

Упражнение 8.1. Напишите процедуры, выполняющие те же действия, что и вызовы fdread и fdwrite, но которые завершатся неудачей, если уже установлена блокировка нужного участка файла. Измените аналог вызова fdread так, чтобы он блокировал читаемый участок. Блокировка должна сниматься после завершения вызова fdread.

Упражнение 8.2. Придумайте и реализуйте условную схему блокировок нумерованных логических записей файла. (Совет: можно блокировать участки файла вблизи максимально возможного смещения файла, даже если там нет данных. Блокировки в этом участке файла могут иметь особое значение, например, каждый байт может соответствовать определенной логической записи. Блокировка в этой области может также использоваться для установки различных флагов.)

Соседние файлы в папке Полищук, Семериков. Системное программирование в UNIX средствами Free Pascal