Клинч
Предположим, что два процесса, РА и РВ, работают с одним файлом. Допустим, что процесс РА блокирует участок файла
SX, а процесс РВ – не пересекающийся с ним участок SY. Пусть далее процесс РА попытается заблокировать участок SY при помощи команды F_SETLKW, а процесс РВ попытается заблокировать участок SX, также используя команду
F_SETLKW. Ни одна из этих попыток не будет успешной, так как процесс РА приостановит работу, ожидая, когда процесс РВ освободит участок SY, а процесс РВ также будет приостановлен в ожидании освобождения участка SX процессом РА. Если не произойдет вмешательства извне, то будет казаться, что два процесса обречены вечно находиться в этом «смертельном объятии».
Такая ситуация называется клинчем (deadlock) по очевидным причинам. Однако UNIX иногда предотвращает возникновение клинча. Если выполнение запроса F_SETLK приведет к очевидному возникновению клинча, то вызов завершается неудачей, и возвращается значение -1, а переменная linuxerror принимает значение Sys_EDEADLK. К сожалению, вызов fcntl может определять только клинч между двумя процессами, в то время как можно создать трехсторонний клинч.[14]
Во избежание такой ситуации сложные приложения, использующие блокировки, должны всегда задавать предельное время ожидания.
Следующий пример поможет пояснить изложенное. В точке /*А*/ программа блокирует с 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. Придумайте и реализуйте условную схему блокировок нумерованных логических записей файла. (Совет: можно блокировать участки файла вблизи максимально возможного смещения файла, даже если там нет данных. Блокировки в этом участке файла могут иметь особое значение, например, каждый байт может соответствовать определенной логической записи. Блокировка в этой области может также использоваться для установки различных флагов.)