Системное программирование в UNIX средствами Free Pascal

       

Клинч


Предположим, что два процесса, РА и РВ, работают с одним файлом. Допустим, что процесс РА блокирует участок файла

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. Придумайте и реализуйте условную схему блокировок нумерованных логических записей файла. (Совет: можно блокировать участки файла вблизи максимально возможного смещения файла, даже если там нет данных. Блокировки в этом участке файла могут иметь особое значение, например, каждый байт может соответствовать определенной логической записи. Блокировка в этой области может также использоваться для установки различных флагов.)


Содержание раздела