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

         

Для семейства вызовов linuxexecl аргументы


uses stdio;
(* Для семейства вызовов linuxexecl аргументы должны быть списком,
   заканчивающимся NULL *)
function linuxexecl(path:pchar;arg0:pchar;argv:array of const):integer;
function linuxexeclp(fname:pchar;arg0:pchar;argv:array of const):integer;
(* Вызову execl нужно передать полный путь к файлу программы *)
Procedure Execl(Path:pathstr);
(* Вызову execle нужно передать полный путь к файлу программы
   и массив указателей на строки окружения *)
Procedure Execle(Path:pathstr; Envp:ppchar);
(* Вызову ехесlp нужно только имя файла программы *)
Procedure Execlp(Path:pathstr);
(* Семейству вызовов execv нужно передать массив аргументов *)
(* Вызову execv нужно передать полный путь к файлу программы *)
Procedure Execv(Path:pathstr;argv:ppchar);
(* Вызову execvp нужно только имя файла программы *)
Procedure Execvp(Path:pathstr;argv:ppchar);
(* Вызову execve нужно передать полный путь к файлу программы
   и массив указателей на строки окружения *)
Procedure Execve(Path:pchar;argv:ppchar;envp:ppchar);
Procedure Execve(Path:pathstr;argv,envp:ppchar);



execl
execle
execlp
v
v
execv
execvp
execve

Рис. 5.2. Дерево семейства вызовов ехес
Все множество системных вызовов ехес выполняет одну и ту же функцию: они преобразуют вызывающий процесс, загружая новую программу в его пространство памяти. Если вызов ехес завершился успешно, то вызывающая программ полностью замещается новой программой, которая запускается с начала. Результат вызова можно рассматривать как запуск нового процесса, который при этом сохраняет идентификатор вызывающего процесса и по умолчанию наследует файловые дескрипторы (см. пункт 5.5.2).
Важно отметить, что вызов ехес не создает новый подпроцесс, который выполняется одновременно с вызывающим, а вместо этого новая программа загружается на место старой. Поэтому, в отличие от вызова fork, успешный вызов ехеc не возвращает значения.


Для простоты осветим только один из вызовов ехес, а именно linuxexecl.
Все аргументы функции linuxexecl являются указателями строк. Первый из них, аргумент path, задает имя файла, содержащего программу, которая будет запущена на выполнение; для вызова linuxexecl это должен быть полный путь к программе, абсолютный или относительный. Сам файл должен содержать программу или последовательность команд оболочки и быть доступным для выполнения. Система определяет, содержит ли файл программу, просматривая его первые байты (обычно первые два байта). Если они содержат специальное значение, называемое магическим числом (magic number), то система рассматривает файл как программу. Второй аргумент, arg0, является, по соглашению, именем программы или команды, из которого исключен путь к ней. Этот аргумент и оставшееся переменное число аргументов (массив args) доступны в вызываемой программе, аналогично аргументам командной строки при запуске программы из оболочки. В действительности командный интерпретатор сам вызывает команды, используя один из вызовов ехес совместно с вызовом fork. Так как список аргументов имеет произвольную длину, он должен заканчиваться нулевым указателем для обозначения конца списка.
Короткий пример ценнее тысячи слов – следующая программа использует вызов execl для запуска программы вывода содержимого каталога ls:
(* Программа runls - использование "execl" для запуска ls *)
uses linux,stdio;
begin
  writeln('Запуск программы ls');
  execl('/bin/ls -l');
  (* Если execl возвращает значение, то вызов был неудачным *)
  perror ('Вызов execl не смог запустить программу ls');
  halt(1);
end.
Работа этой демонстрационной программы показана на рис. 5.3. Часть До показывает процесс непосредственно перед вызовом execl. Часть После
показывает измененный процесс после вызова execl, который при этом выполняет программу ls. Программный счетчик PC указывает на первую строку программы ls, показывая, что вызов execl запускает программу с начала.



writeln(...);
< PC
execl('/bin/ls -l');
runls

exec
До
После
(* 1-ая строка ls*)
< PC
Команда ls

Рис. 5.3. Вызов exec
Обратите внимание, что в примере за вызовом execl следует безусловный вызов библиотечной процедуры perror. Это отражает то, что успешный вызов функции execl (и других родственных функций) стирает вызывающую программу. Если вызывающая программа сохраняет работоспособность и происходит возврат из вызова execl, значит, произошла ошибка. Поэтому возвращаемое значение execl и родственных функций всегда равно -1.

Системный вызов halt уже известен,


uses system;
procedure halt(status:word);
Системный вызов halt уже известен, но теперь следует дать его правильное описание. Этот вызов используется для завершения процесса, хотя это также происходит, когда управление доходит до конца тела главной программы или до процедуры exit в теле главной программы.
Единственный целочисленный аргумент вызова halt называется статусом завершения (exit status) процесса, младшие восемь бит которого доступны родительскому процессу при условии, если он выполнил системный вызов wait (подробнее об этом см. в следующем разделе). При этом возвращаемое вызовом halt значение обычно используется для определения успешного или неудачного завершения выполнявшейся процессом задачи. По принятому соглашению, нулевое возвращаемое значение соответствует нормальному завершению, а ненулевое значение говорит о том, что что-то случилось.
Кроме завершения вызывающего его процесса, вызов halt имеет еще несколько последствий: наиболее важным из них является закрытие всех открытых дескрипторов файлов. Если, как это было в последнем примере, родительский процесс выполнял вызов wait, то его выполнение продолжится.
Для полноты изложения следует также упомянуть системный вызов _exit, который отличается от вызова halt наличием символа подчеркивания в начале. Он используется в точности так же, как и вызов halt. Тем не менее он не включает описанные ранее действия по очистке. В большинстве случаев следует избегать использования вызова _exit.
Упражнение 5.7. Статус завершения программы можно получить в командном интерпретаторе при помощи переменной $?, например:
$ ls nonesuch
  nonesuch: No such file or directory
$ echo $?
  2
Напишите программу fake, которая использует целочисленное значение первого аргумента в качестве статуса завершения. Используя намеченный выше метод, выполните программу fake, задавая различные значения аргументов, включая большие и отрицательные. Есть ли польза от программы fake?


uses stdio;
function wait(status:pinteger):longint;
Как было уже обсуждено, вызов wait временно приостанавливает выполнение процесса, в то время как дочерний процесс продолжает выполняться. После завершения дочернего процесса выполнение родительского процесса продолжится. Если запущено более одного дочернего процесса, то возврат из вызова wait произойдет после выхода из любого из потомков.
Вызов wait часто осуществляется родительским процессом после вызова fork, например:
.
.
.
var
  status:integer;
  cpid:longint;
cpid := fork; (*Создать новый процесс *)
if cpid = 0 then
begin
  (* Дочерний процесс *)
  (* Выполнить какие-либо действия ... *)
end
else
begin
  /* Родительский процесс, ожидание завершения дочернего */
  cpid := wait(@status);
  writeln('Дочерний процесс ', cpid, ' завершился');
end;
.
.
.
Сочетание вызовов fork и wait наиболее полезно, если дочерний процесс предназначен для выполнения совершенно другой программы при помощи вызова ехеc.
Возвращаемое значение wait обычно является идентификатором дочернего процесса, который завершил свою работу. Если вызов
wait возвращает значение -1, это может означать, что дочерние процессы не существуют, и в этом случае переменная linuxerror будет содержать код ошибки Sys_ECHILD. Возможность определить завершение каждого из дочерних процессов по отдельности означает, что родительский процесс может выполнять цикл, ожидая завершения каждого из потомков, а после того, как все они завершатся, продолжать свою работу.
Вызов wait принимает один аргумент, status, – указатель на целое число. Если указатель равен nil, то аргумент просто игнорируется. Если же вызову wait передается допустимый указатель, то после возврата из вызова wait переменная status будет содержать полезную информацию о статусе завершения процесса. Обычно эта информация будет представлять собой код завершения дочернего процесса, переданный при помощи вызова halt.
Следующая программа status показывает, как может быть использован вызов wait:


 (* Программа status -- получение статуса завершения потомка *)
uses linux,stdio;
var
  pid:longint;
  status, exit_status:integer;
begin
  pid := fork;
  if pid < 0 then
    fatal ('Ошибка вызова fork ');
  if pid = 0 then            (* потомок *)
  begin
    (* Вызвать библиотечную процедуру sleep
     * для временного прекращения работы на 4 секунды
     *)
    sleep(4);
    halt(5);                (* выход с ненулевым значением *)
  end;
  (* Если мы оказались здесь, то это родительский процесс, *)
  (* поэтому ожидать завершения дочернего процесса         *)
  pid := wait(@status);
  if pid = -1 then
  begin
    perror ('Ошибка вызова wait ');
    halt(2);
  end;
  (* Проверка статуса завершения дочернего процесса *)
  if WIFEXITED (status) then
  begin
    exit_status := WEXITSTATUS (status);
    writeln ('Статус завершения ',pid,' равен ', exit_status);
  end;
  halt(0);
end.
Значение, возвращаемое родительскому процессу при помощи вызова halt, записывается в старшие восемь бит целочисленной переменной status. Чтобы оно имело смысл, младшие восемь бит должны быть равны нулю. Функция WIFEXITED (определенная в модуле stdio) проверяет, так ли это на самом деле. Если WIFEXITED возвращает false, то это означает, что выполнение дочернего процесса было остановлено (или прекращено) другим процессом при помощи межпроцессного взаимодействия, называемого сигналом (signal) и рассматриваемого в главе 6.
Упражнение 5.8. Переделайте процедуру docommand так, чтобы она возвращала статус вызова halt выполняемой команды. Что должно происходить, если вызов wait возвращает значение -1?

Первый аргумент pid определяет идентификатор


uses linux;
Function WaitPid(Pid:longint; Status:pointer; Options:Longint):Longint;
Первый аргумент pid определяет идентификатор дочернего процесса, завершения которого будет ожидать родительский процесс. Если этот аргумент установлен равным -1, а аргумент options установлен равным 0, то вызов waitpid ведет себя в точности так же, как и вызов wait, поскольку значение -1 соответствует любому дочернему процессу. Если значение pid больше нуля, то родительский процесс будет ждать завершения дочернего процесса с идентификатором процесса равным pid. Во втором аргументе status будет находиться статус дочернего процесса после возврата из вызова
waitpid.
Последний аргумент, options, может принимать константные значения, определенные в модуле linux. Наиболее полезное из них – константа WNOHANG. Задание этого значения позволяет вызывать waitpid в цикле без блокирования процесса, контролируя ситуацию, пока дочерний процесс продолжает выполняться. Если установлен флаг WNOHANG, то вызов waitpid будет возвращать 0 в случае, если дочерний процесс еще не завершился.
Функциональные возможности вызова waitpid с параметром
WNOHANG можно продемонстрировать, переписав предыдущий пример. На этот раз родительски процесс проверяет, завершился ли уже дочерний процесс. Если нет, он выводит сообщение, говорящее о том, что он продолжает ждать, затем делает секундную паузу и снова вызывает waitpid, проверяя, завершился ли дочерний процесс. Обратите внимание, что потомок получает свой идентификатор процесса при помощи вызова getpid. Об этом вызове расскажем в разделе 5.10.1.
(* Программа status2 - получение статуса завершения
 * дочернего процесса при помощи вызова waitpid
 *)
uses linux,stdio,crt;
var
  pid:longint;
  status, exit_status:integer;
begin
  pid := fork;
  if pid < 0 then
    fatal ('Ошибка вызова fork ');
  if pid = 0 then            (* потомок *)
  begin
    (* Вызов библиотечной процедуры sleep
     * для приостановки выполнения на 4 секунды


     *)
    writeln ('Потомок ',getpid,' пауза...');
    sleep(4);
    halt(5);                (* выход с ненулевым значением *)
  end;
  (* Если мы оказались здесь, то это родительский процесс *)
  (* Проверить, закончился ли дочерний процесс, и если нет,  *)
  (* то сделать секундную паузу, и потом проверить снова     *)
  while (waitpid (pid, @status, WNOHANG) = 0) do
  begin
    writeln ('Ожидание продолжается...\n');
    sleep(1);
  end;
  (* Проверка статуса завершения дочернего процесса *)
  if WIFEXITED (status) then
  begin
    exit_status := WEXITSTATUS (status);
    writeln ('Статус завершения ',pid,' равен ', exit_status);
  end;
  halt(0);
end.
При запуске программы получим следующий вывод:
Ожидание продолжается...
Потомок 12857 пауза...
Ожидание продолжается...
Ожидание продолжается...
Ожидание продолжается...
Статус завершения 12857 равен 5

Вызов setpgid устанавливает идентификатор группы


uses stdio;
function setpgid(pid, pgid:longint):longint;
Вызов setpgid устанавливает идентификатор группы процесса с идентификатором pid равным pgid. Если pid равен 0, то используется идентификатор вызывающего процесса. Если значения идентификаторов pid и pgid одинаковы, то процесс становится лидером группы процессов. В случае ошибки возвращает значение -1. Если идентификатор pgid равен нулю, то в качестве идентификатора группы процесса используется идентификатор процесса pid.

Если передать вызову getsid значение


uses stdio;
function getsid(pid:longint):longint;
Если передать вызову getsid значение 0, то он вернет идентификатор сеанса вызывающего процесса, в противном случае возвращается идентификатор сеанса процесса, заданного идентификатором pid.
Понятие сеанса полезно при работе с фоновыми процессами или процессами-демонами (daemon processes). Процессом-демоном называется просто процесс, не имеющий управляющего терминала. Примером такого процесса является процесс cron, запускающий команды в заданное время. Демон может задать для себя сеанс без управляющего терминала, переместившись в другой сеанс при помощи системного вызова setsid.


uses stdio;
function setsid:longint;cdecl;
Если вызывающий процесс не является лидером группы процессов, то создается новая группа процессов и новый сеанс, и идентификатор вызывающего процесса станет идентификатором созданного сеанса. Он также не будет иметь управляющего терминала. Процесс-демон теперь будет находиться в странном состоянии, так как он будет единственным процессом в группе процессов, содержащейся в новом сеансе, а его идентификатор процесса
pid – также идентификатором группы и сеанса.
Функция setsid может завершиться неудачей и возвратит значение -1, если вызывающий процесс уже является лидером группы.

Аргументом функции getenv является имя


uses dos;
Function GetEnv(EnvVar:String):String;
Аргументом функции getenv является имя искомой переменной. При успешном завершении поиска функция getenv возвращает указатель на строку переменной окружения, в противном случае – пустая строка. Следующий код показывает пример использования этой функции:
(* Найти значение переменной окружения PATH *)
uses dos;
begin
  writeln('PATH=',getenv('PATH'));
end.
Для изменения окружения существует парная процедура putenv. Она вызывается следующим образом:
putenv('НОВАЯ_ПЕРЕМЕННАЯ = значение');
В случае успеха процедура putenv возвращает нулевое значение. Она изменяет переменную окружения, на которую указывает глобальная переменная envp.

Переменная path указывает на путь,


uses stdio;
function chroot(path:pchar):longint;
Переменная path указывает на путь, обозначающий каталог. В случае успешного вызова chroot путь path становится начальной точкой при поиске в путях, начинающихся с символа / (только для вызывающего процесса, корневой каталог системы при этом не меняется). В случае неудачи вызов chroot не меняет корневой каталог и возвращает значение -1. Для изменения корневого каталога вызывающий процесс должен иметь соответствующие права доступа.
Упражнение 5.12. Добавьте к командному интерпретатору smallsh команду cd.
Упражнение 5.13. Напишите собственную версию функции getenv.

Для получения текущего максимального размера


uses stdio;
function ulimit(cmd:longint;args:array of const):longint;
Для получения текущего максимального размера файла можно вызвать ulimit, установив значение параметра cmd равным UL_GETFSIZE. Возвращаемое значение равно числу 512-байтных блоков.
Для изменения максимального размера файла можно присвоить переменной cmd значение UL_SETFSIZE и поместить новое значение максимального размера файла, также в 512-байтных блоках, в переменную newlimit, например:
if ulimit(UL_SETFSIZE, newlimit) < 0 then
  perror('Ошибка вызова ulimit');
В действительности увеличить максимальный размер файла таким способом может только суперпользователь. Процессы с идентификаторами других пользователей могут только уменьшать этот предел.

GetPriority возвращает приоритет процесса, определяемых


uses linux;
Function GetPriority(Which,Who:Integer):Integer;
Function SetPriority(Which,Who,Prio:Integer):Integer;
GetPriority возвращает приоритет процесса, определяемых переменными Which и Who. Which может принимать значения Prio_Process, Prio_PGrp и Prio_User для идентификаторов процесса, его группы и владельца соответственно.
SetPriority устанавливает приоритет процесса. Значение Prio может быть в диапазоне от -20 до 20.
Программа, демонстрирующая функции Nice и Get/SetPriority:
Uses linux;
begin
  writeln ('Setting priority to 5');
  setpriority (prio_process,getpid,5);
  writeln ('New priority = ',getpriority (prio_process,getpid));
  writeln ('Doing nice 10');
  nice (10);
  writeln ('New Priority = ',getpriority (prio_process,getpid));
end.

и sigfillset имеют единственный параметр


uses stdio;
(* Инициализация *)
function sigemptyset(__set:psigset_t):integer;
function sigfillset(__set:psigset_t):integer;
(* Добавление и удаление сигналов *)
function sigaddset(__set:psigset_t;__signo:integer):integer;
function sigdelset(__set:psigset_t;__signo:integer):integer;
Процедуры sigemptyset и sigfillset имеют единственный параметр – указатель на переменную типа sigset_t. Вызов sigemptyset инициализирует набор __set, исключив из него все сигналы. И наоборот, вызов sigfillset инициализирует набор, на который указывает __set, включив в него все сигналы. Приложения должны вызывать sigemptyset или sigfillset хотя бы один раз для каждой переменной типа sigset_t.
Процедуры sigaddset и sigdelset принимают в качестве параметров указатель на инициализированный набор сигналов и номер сигнала, который должен быть добавлен или удален. Второй параметр, signo, может быть символическим именем константы, таким как SIGINT, или настоящим номером сигнала, но в последнем случае программа окажется системно-зависимой.
В следующем примере создадим два набора сигналов. Первый образуется из пустого набора добавлением сигналов SIGINT и SIGQUIT. Второй набор изначально заполнен, и из него удаляется сигнал SIGCHLD.
uses stdio;
var
  mask1, mask2:sigset_t;
.
.
.
(* Создать пустой набор сигналов *)
sigemptyset(@mask1);
(* Добавить сигналы *)
sigaddset(@mask1, SIGINT);
sigaddset(@mask1, SIGQUIT);
(* Создать полный набор сигналов *)
sigfillset(@mask2);
(* Удалить сигнал *)
sigdelset(@mask2, SIGCHLD);
.
.
.

Как вскоре увидим, структура sigactionrec,


uses linux;
Procedure SigAction(Signo:Integer; Var Act,OAct:PSigActionRec);
Как вскоре увидим, структура sigactionrec, в свою очередь, также содержит набор сигналов. Первый параметр signo задает отдельный сигнал, для которого нужно определить действие. Чтобы это действие выполнялось, процедура sigaction должна быть вызвана до получения сигнала типа signo. Значением переменной signo может быть любое из ранее определенных имен сигналов, за исключений SIGSTOP и SIGKILL, которые предназначены только для остановки или завершения процесса и не могут обрабатываться по-другому.
Второй параметр, act, определяет обработчика сигнала signo. Третий параметр, oact, если не равен nil, указывает на структуру, куда будет помещено описание старого метода обработки сигнала. Рассмотрим структуру sigactionrec, определенную в файле linux:
SigActionRec  =  packed  record
  Handler    :  record
     case  byte  of
        0:  (Sh:  SignalHandler); (* Функция обработчика *)
        1:  (Sa:  TSigAction);
     end;
  Sa_Mask      :  SigSet;  (* Сигналы, которые блокируются
                              во время обработки сигнала *)
  Sa_Flags     :  Longint; /* Флаги, влияющие
                              на поведение сигнала */
  Sa_restorer  :  SignalRestorer;  {  Obsolete  -  Don't  use  }
end;
Эта структура кажется сложной, но давайте рассмотрим ее поля по отдельности. Первое поле, handler, задает обработчик сигнала signo. Это поле может иметь три вида значений:
–        SIG_DFL – константа, сообщающая, что нужно восстановить обработку сигнала по умолчанию (для большинства сигналов это завершение процесса);
–        SIG_IGN – константа, означающая, что нужно игнорировать данный сигнал (ignore the signal). He может использоваться для сигналов SIGSTOP и SIGKILL;
–        адрес функции, принимающей аргумент типа int. Если функция объявлена в тексте программы до заполнения sigaction, то полю handler.sh/handler.sa можно просто присвоить имя функции. Компилятор поймет, что имелся в виду ее адрес. Эта функция будет выполняться при получении сигнала signo, а само значение signo будет передано в качестве аргумента вызываемой функции. Управление будет передано функции, как только процесс получит сигнал, какой бы участок программы при этом не выполнялся. После возврата из функции управление будет снова передано процессу и продолжится с точки, в которой выполнение процесса было прервано. Этот механизм станет понятен из следующего примера.


Функция- обработчик может быть одного из двух типов:
type
  TSigAction = procedure(Sig: Longint; SigContext: SigContextRec);cdecl;
  SignalHandler = Procedure (Sig: Integer);cdecl;
Второе поле, sa_mask, демонстрирует первое практическое использование набора сигналов. Сигналы, заданные в поле
sa_mask, будут блокироваться во время выполнения функции, заданной полем handler. Это не означает, что эти сигналы будут игнорироваться, просто их обработка будет отложена до завершения функции. При входе в функцию перехваченный сигнал также будет неявно добавлен к текущей маске сигналов. Все это делает механизм сигналов более надежным механизмом межпроцессного взаимодействия, чем он был в ранних версиях системы UNIX.[9]
Тип поля sa_mask, sigset, ограниченно совместим с типом sigset_t. Размер типа sigset_t определяется константой
_SIGSET_NWORDS=1024 div (8 * sizeof (longint));
Это в четыре раза больше размера типа sigset, поэтому при переходе к нему необходимо использовать поле __val[0] типа sigset_t. К примеру,
sa_mask:=mask.__val[0];
Поле sa_flags может использоваться для изменения характера реакции на сигнал signo. Например, после возврата из обработчика можно вернуть обработчик по умолчанию SIG_DFL, установив значение поля sa_flags равным SA_RESETHAND. Если же значение поля sa_flags установлено равным SA_SIGINFO, то обработчику сигнала будет передаваться дополнительная информация.
Все это достаточно трудно усвоить, поэтому рассмотрим несколько примеров. В действительности все намного проще, чем кажется.

в объекте типа sigjmp_buf, определенном


uses stdio;
(* Сохранить текущее положение в программе *)
function sigsetjmp(var env:jmp_buf;savemask:longint):integer;
(* Вернуться в сохраненную позицию *)
procedure siglongjmp(var env:jmp_buf;val:integer);
Текущее состояние программы сохраняется в объекте типа sigjmp_buf, определенном в файле stdio. Если во время вызова sigsetjmp значение аргумента savemask не равно нулю, то вызов sigsetjmp сохранит помимо основного контекста программы также текущую маску сигналов (маску блокированных сигналов и действия, связанные со всеми сигналами). Возвращаемое функцией sigsetjmp значение является важным: если в точку sigsetjmp управление переходит из функции siglongjmp, то она возвращает ненулевое значение – аргумент val вызова siglongjmp. Если же функция
sigsetjmp вызывается в обычном порядке исполнения программы, то она возвращает значение 0.
Следующий пример демонстрирует технику использования этих функций:
 (* Пример использования процедур sigsetjmp и siglongjmp *)
uses linux,stdio;
var
  position:sigjmp_buf;
procedure domenu;
var
  choice:integer;
begin
  write('Choice menu entry:'#$a' menu 1'#$a' menu 2'#$a' menu 3'#$a'?>');
  scanf('%d',[@choice]);
end;
procedure goback(smth:longint);cdecl;
begin
  fprintf (stderr, #$a'Прерывание'#$a, []);
  (* Вернуться в сохраненную позицию *)
  siglongjmp (position, 1);
end;
var
  act:sigactionrec;
begin
  (*
     .
     .
     . *)
  (* Сохранить текущее положение *)
  if sigsetjmp(position, 1) = 0 then
  begin
    act.handler.sh := @goback;
    sigaction (SIGINT, @act, nil);
  end;
  domenu;
  (*
   .
   .
   . *)
end.
Если пользователь нажимает на клавишу прерывания задания после вызова sigaction, то управление передается в точку, положение которой было записано при помощи функции sigsetjmp. Поэтому выполнение программы продолжается, как если бы только что завершился соответствующий вызов sigsetjmp. В этом случае возвращаемое функцией sigsetjmp значение будет равно второму параметру процедуры siglongjmp.

какое действие он должен выполнять.


uses linux;
Procedure SigProcMask(How:Integer; SSet,OSSet:PSigSet);
Параметр how сообщает вызову
sigpromask, какое действие он должен выполнять. Например, этот параметр может иметь значение SIG_MASK, указывающее, что с этого момента будут блокироваться сигналы, заданные во втором параметр sset, то есть будет произведена установка маски блокирования сигналов. Третий параметр просто заполняется текущей маской блокируемых сигналов – если не нужно ее знать, просто присвойте этому параметру значение nil. Поясним это на примере:
var
  set1:sigset_t;
.
.
.
(* Создать полный набор сигналов *)
sigfillset (@set1);
(* Установить блокировку *)
sigprocmask (SIG_SETMASK, @set1, nil);
(* Критический участок кода .. *)
(* Отменить блокировку сигналов *)
sigprocmask (SIG_UNBLOCK, @set1, nil);
Обратите внимание на использование для отмены блокирования сигналов параметра SIG_UNBLOCK. Заметим, что если использовать в качестве первого параметра SIG_BLOCK вместо SIG_SETMASK, то это приведет к добавлению
заданных в переменной
set сигналов к текущему набору сигналов.
Следующий более сложный пример показывает, как сначала выполняется блокирование всех сигналов во время выполнения особенно важного участка программы, а затем, при выполнении менее критического участка, блокируются только сигналы SIGINT и SIGQUIT.
 (* Блокировка сигналов - демонстрирует вызов sigprocmask *)
uses linux,stdio;
var
  set1, set2:sigset_t;
begin
  (* Создать полный набор сигналов *)
  sigfillset (@set1);
  (* Создать набор сигналов, не включающий
   * сигналы SIGINT и SIGQUIT
   *)
  sigfillset (@set2);
  sigdelset (@set2, SIGINT);
  sigdelset (@set2, SIGQUIT);
  (* Некритический участок кода ... *)
  (* Установить блокировку всех сигналов *)
  sigprocmask (SIG_SETMASK, @set1, nil);
  (* Более критический участок кода ... *)
  (* Блокировка меньшего числа сигналов. *)
  sigprocmask (SIG_UNBLOCK, @set2, nil);
  (* Менее критический участок кода ... *)
  (* Отменить блокировку для всех сигналов *)
  sigprocmask (SIG_UNBLOCK, @set1, nil);
end.
Упражнение 6.2. Перепишите процедуру g_exit в примере 4 из раздела 6.2.2 так, чтобы во время ее выполнения игнорировались сигналы SIGINT и SIGQUIT.

Первый параметр pid определяет процесс


uses linux;
Function Kill(Pid:Longint; Sig:Integer):Integer;
Первый параметр pid определяет процесс или процессы, которым посылается сигнал sig. Обычно pid является положительным числом, и в этом случае он рассматривается как идентификатор процесса. Поэтому следующий оператор
kill(7421, SIGTERM);
означает «послать сигнал SIGTERM процессу с идентификатором 7421». Так как процесс, посылающий сигнал kill, должен знать идентификатор процесса, которому предназначен сигнал, то вызов kill чаще всего используется для обмена между тесно связанными процессами, например, родительским и дочерним. Заметим, что процесс может послать сигнал самому себе.
Существуют некоторые ограничения, связанные с правами доступа. Чтобы можно было послать сигнал процессу, действующий или истинный идентификатор пользователя посылающего процесса должен совпадать с действующим или истинным идентификатором пользователя процесса, которому сигнал адресован. Процессы суперпользователя, как обычно, могут посылать сигналы любым другим процессам. Если непривилегированный пользователь пытается послать сигнал процессу, который принадлежит другому пользователю, то вызов kill завершится неудачей, вернет значение –1 и поместит в переменную
linuxerror значение EPERM. (Другие возможные значения ошибок в переменной linuxerror после неудачного вызова kill – это значение Sys_ESRCH, указывающее, что процесс с заданным идентификатором не существует, и Sys_EINVAL, если sig содержит некорректный номер сигнала.)
Параметр pid вызова kill может также принимать определенные значения, которые имеют особый смысл:
–        если параметр pid равен нулю, то сигнал будет послан всем процессам, принадлежащим к той же группе, что и процесс, пославший сигнал, в том числе и самому процессу;
–        если параметр pid равен –1, и действующий идентификатор пользователя является идентификатором суперпользователя, то сигнал посылается всем процессам, истинный идентификатор пользователя которых равен действующему идентификатору пользователя, пославшего сигнал процесса, снова включая и сам процесс, пославший сигнал;


–        если параметр pid равен –1 и действующий идентификатор пользовать является идентификатором суперпользователя, то сигнал посылается всем процессам, кроме определенных системных процессов (последнее исключение относится ко всем попыткам послать сигнал группе процессов, но наиболее важно это в данном случае);
–        и, наконец, если параметр pid меньше нуля, но не равен –1, то сигнал посылается всем процессам, идентификатор группы которых равен модулю pid, включая пославший сигнал процесс, если для него также выполняется это условие.
Следующий пример – программа synchro создает два процесса, которые будут поочередно печатать сообщения на стандартный вывод. Они синхронизирут свою работу, посылая друг другу сигнал SIGUSR1 при помощи вызова kill.
 (* Программа synchro -- пример использования вызова kill *)
uses linux,stdio;
const
  ntimes:integer=0;
procedure p_action(sig:integer);cdecl;
begin
  inc(ntimes);
  writeln ('Родительский процесс получил сигнал ', ntimes, ' раз(а)');
end;
procedure c_action(sig:integer);cdecl;
begin
  inc(ntimes);
  writeln ('Дочерний процесс получил сигнал ', ntimes, ' раз(а)');
end;
var
  pid, ppid:longint;
  pact, cact:sigactionrec;
begin
  (* Задать обработчик сигнала SIGUSR1 в родительском процессе *)
  pact.handler.sh := @p_action;
  sigaction (SIGUSR1, @pact, nil);
  pid := fork;
  case pid of
    -1:               (* ошибка *)
    begin
      perror ('synchro');
      halt(1);
    end;
    0:                (* дочерний процесс *)
    begin
      (* Задать обработчик в дочернем процессе *)
      cact.handler.sh := @c_action;
      sigaction (SIGUSR1, @cact, nil);
      (* Получить идентификатор родительского процесса *)
      ppid := getppid;
      while true do
      begin
        sleep (1);
        kill (ppid, SIGUSR1);
        pause;
      end;
      (* Бесконечный цикл *)


    end;
    else              (* родительский процесс *)
      while true do
      begin
        pause;
        sleep (1);
        kill (pid, SIGUSR1);
      end;
      (* Бесконечный цикл *)
  end;
end.
Оба процесса выполняют бесконечный цикл, приостанавливая работу до получения сигнала от другого процесса. Они используют для этого системный вызов pause, который просто приостанавливает работу до получения сигнала (см. раздел 6.4.3). Затем каждый из процессов выводит сообщение и, в свою очередь, посылает сигнал при помощи вызова kill. Дочерний процесс начинает вывод сообщений (обратите внимание на порядок операторов в каждом цикле). Оба процесса завершают работу, когда пользователь нажимает на клавишу прерывания. Диалог с программой может выглядеть примерно так:
$ synchro
Родительский процесс получил сигнал #1
Дочерний процесс получил сигнал #1
Родительский процесс получил сигнал #2
Дочерний процесс получил сигнал #2
< прерывание >    (пользователь нажал на клавишу прерывания)
$

Вызывающему процессу посылается сигнал, определенный


uses linux;
Procedure SigRaise(Sig:integer);
Вызывающему процессу посылается сигнал, определенный параметром sig и в случае успеха функция sigraise возвращает нулевое значение. Например:
uses Linux;
Var
   oa,na : PSigActionRec;
  
Procedure DoSig(sig : Longint);cdecl;
begin
   writeln('Receiving signal: ',sig);
end;
begin
   new(na);
   new(oa);
   na^.handler.sh:=@DoSig;
   na^.Sa_Mask:=0;
   na^.Sa_Flags:=0;
   na^.Sa_Restorer:=Nil;
   SigAction(SigUsr1,na,oa);
   if LinuxError<>0 then
     begin
     writeln('Error: ',linuxerror,'.');
     halt(1);
     end;
   Writeln('Sending USR1 (',sigusr1,') signal to self.');
   SigRaise(sigusr1);
end.
Вызов alarm – это простой и полезный вызов, который устанавливает таймер процесса. При срабатывании таймера процессу посылается сигнал.


uses linux;
Function Alarm(Secs:longint):Longint;
Переменная secs задает время в секундах, на которое устанавливается таймер. После истечения заданного интервала времени процессу посылается сигнал SIGALRM. Поэтому вызов
alarm(60);
приводит к посылке сигнала SIGALRM через 60 секунд. Обратите внимание, что вызов alarm не приостанавливает выполнение процесса, как вызов sleep, вместо этого сразу же происходит возврат из вызова alarm, и продолжается нормальное выполнение процесса, по крайней мере, до тех пор, пока не будет получен сигнал SIGALRM. Установленный таймер будет продолжать отсчет и после вызова ехec, но вызов fork выключает таймер в дочернем процессе.
Выключить таймер можно при помощи вызова alarm с нулевым параметром:
(* Выключить таймер *)
alarm(0);
Вызовы alarm не накапливаются: другими словами, если вызвать alarm дважды, то второй вызов отменит предыдущий. Но при этом возвращаемое вызовом alarm значение будет равно времени, оставшемуся до срабатывания предыдущего таймера, и его можно при необходимости записать.
Вызов alarm может быть полезен, если нужно ограничить время выполнения какого-либо действия. Основная идея проста: вызывается alarm, и процесс начинает выполнение задачи. Если задача выполняется вовремя, то таймер сбрасывается. Если выполнение задачи отнимает слишком много времени, то процесс прерывается при помощи сигнала SIGTERM и выполняет корректирующие действия.
Следующая функция quickreply использует этот подход для ввода данных от пользователя за заданное время. Она имеет один аргумент, приглашение командной строки, и возвращает указатель на введенную строку, или нулевой указатель, если после пяти попыток ничего не было введено. Обратите внимание, что после каждого напоминания пользователю функция quickreply посылает на терминал символ Ctrl+G. На большинстве терминалов и эмуляторов терминала это приводит к подаче звукового сигнала.
Функция quickreply вызывает процедуру gets из стандартной библиотеки ввода/вывода (Standard I/O Library). Процедура gets помещает очередную строку из стандартного ввода в массив char. Она возвращает либо указатель на массив, либо нулевой указатель в случае достижения конца файла или ошибки. Обратите внимание на то, что сигнал SIGALRM перехватывается процедурой обработчика прерывания catch. Это важно, так как по умолчанию получение сигнала SIGALRM приводит к завершению процесса. Процедура catch устанавливает флаг timed_out. Функция quickreply проверяет этот флаг, определяя таким образом, не истекло ли заданное время.


uses linux,stdio;
const
  TIMEOUT=5;           (* время в секундах *)
  MAXTRIES=5;          (* число попыток *)
  LINESIZE=100;        (* длина строки *)
  CTRL_G=#7;           (* ASCII символ звукового сигнала *)
var
  (* Флаг, определяющий, истекло ли заданное время *)
  timed_out:boolean;
  (* Переменная, которая будет содержать введенную строку *)
  answer:array [0..LINESIZE-1] of char;
(* Выполняется при получении сигнала SIGALRM *)
procedure catch (sig:integer);cdecl;
begin
  (* Установить флаг timed_out *)
  timed_out := TRUE;
  (* Подать звуковой сигнал *)
  write(CTRL_G);
end;
function quickreply(prompt:pchar):pchar;
var
  ntries:integer;
  act, oact:sigactionrec;
begin
  (* Перехватить сигнал SIGALRM и сохранить старый обработчик *)
  act.handler.sh := @catch;
  sigaction (SIGALRM, @act, @oact);
  for ntries:=1 to MAXTRIES do
  begin
    timed_out := FALSE;
    writeln;
    write(prompt, ' > ');
    (* Установить таймер *)
    alarm (TIMEOUT);
    (* Получить введенную строку *)
    gets (answer);
    (* Выключить таймер *)
    alarm (0);
    (* Если флаг timed_out равен TRUE, завершить работу *)
    if not timed_out then
      break;
  end;
  (* Восстановить старый обработчик *)
  sigaction (SIGALRM, @oact, nil);
  (* Вернуть соответствующее значение *)
  if ntries = MAXTRIES then
    quickreply:=nil
  else quickreply:=answer;
end;
begin
  writeln;
  writeln(quickreply ('Reply'));
end.

Вызов pause приостанавливает выполнение вызывающего


uses linux;
Procedure Pause;
Вызов pause приостанавливает выполнение вызывающего процесса (так что процесс при этом не занимает процессорного времени) до получения любого сигнала, например, сигнала SIGALRM. Если сигнал вызывает нормальное завершена процесса или игнорируется процессом, то в результате вызова pause будет просто выполнено соответствующее действие (завершение работы или игнорирована сигнала).
Следующая программа tml (сокращение от «tell me later» – напомнить позднее) использует оба вызова alarm и pause для вывода сообщения в течение заданного числа минут. Она вызывается следующим образом:
$ tml число_минут текст_сообщения
Например:
$ tml 10 время идти домой
Перед сообщением выводятся три символа
Ctrl+G (звуковые сигналы) для привлечения внимания пользователя. Обратите внимание на создание в программе tml фонового процесса при помощи вызова fork. Фоновый процесс выполняет работу, позволяя пользователю продолжать выполнение других задач.
 (* tml - программа для напоминания *)
{$mode objfpc}
uses linux, stdio, sysutils;
const
  BELLS=#7#7#7;   (* звуковой сигнал ASCII *)
  alarm_flag:boolean = FALSE;
(* Обработчик сигнала SIGALRM *)
procedure setflag(sig:integer);cdecl;
begin
  alarm_flag := TRUE;
end;
var
  nsecs, j:integer;
  pid:longint;
  act:sigactionrec;
begin
  if paramcount < 2 then
  begin
    writeln (stderr, 'Применение: tml число_минут сообщение');
    halt(1);
  end;
  try
    nsecs := strtoint(paramstr(1)) * 60;
  except
    on e:econverterror do
    begin
      writeln (stderr, 'Введено нечисловое значение');
      halt(2);
    end; 
  end;
  if nsecs <= 0 then
  begin
    writeln (stderr, 'tml: недопустимое время');
    halt(3);
  end;
  (* Вызов fork, создающий фоновый процесс *)
  pid := fork;
  case pid of
    -1:               (* ошибка *)
    begin
      perror ('tml');
      halt(1);
    end;
    0:                (* дочерний процесс *)


      ;
    else         (* родительский процесс *)
    begin
      writeln('Процесс tml с идентификатором ', pid);
      halt(0);
    end;
  end;
  (* Установить обработчик таймера *)
  act.handler.sh := @setflag;
  sigaction (SIGALRM, @act, nil);
  (* Установить таймер *)
  alarm (nsecs);
  (* Приостановить выполнение до получения сигнала ... *)
  pause;
  (* Если был получен сигнал SIGALRM, вывести сообщение *)
  if alarm_flag then
  begin
    write(BELLS);
    for j := 2 to paramcount do
      write(paramstr(j),' ');
    writeln;
  end;
  halt(0);
end.
Из этого примера можно получить представление о том, как работает подпрограмма sleep, вызывая вначале
alarm, а затем pause.
Упражнение 6.3. Напишите свою версию подпрограммы sleep. Она должна сохранить предыдущее состояние таймера и восстанавливать его при выходе. (Посмотрите полное описание процедуры sleep в справочном руководстве системы.)
Упражнение 6.4. Перепишите программу tml, используя свою версию процедуры sleep.

SigPending позволяет узнать, какие из


uses linux;
Function SigPending:SigSet;
Procedure SigSuspend(Mask:SigSet);
SigPending позволяет узнать, какие из временно заблокированных сигналов необходимо обработать. Возвращаемое значение – маска отложенных сигналов.
SigSuspend временно заменяет маску сигналов для процесса на Mask, приостанавливая процесс до получения сигнала.

и pipe_out содержат дескрипторы файлов,


uses linux;
Function AssignPipe(var pipe_in,pipe_out:longint):boolean;
Function AssignPipe(var pipe_in,pipe_out:text):boolean;
Function AssignPipe(var pipe_in,pipe_out:file):boolean;
Переменные pipe_in и pipe_out содержат дескрипторы файлов, обозначающие канал. После успешного вызова pipe_in будет открыт для чтения из канала, a pipe_out для записи в канал.
В случае неудачи вызов pipe вернет значение false. Это может произойти, если в момент вызова произойдет превышение максимально возможного числа дескрипторов файлов, которые могут быть одновременно открыты процессами пользователя (в этом случае переменная linuxerror будет содержать значение Sys_EMFILE), или если произойдет переполнение таблицы открытых файлов в ядре (в этом случае переменная linuxerror будет содержать значение Sys_ENFILE).
После создания канала с ним можно работать просто при помощи вызовов fdread и fdwrite. Следующий пример демонстрирует это: он создает канал, записывает в него три сообщения, а затем считывает их из канала:
uses linux,stdio;
(* Первый пример работы с каналами *)
const
  MSGSIZE=16;
(* Эти строки заканчиваются нулевым символом *)
  msg1:array [0..MSGSIZE-1] of char = 'hello, world #1';
  msg2:array [0..MSGSIZE-1] of char = 'hello, world #2';
  msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';
var
  inbuf:array [0..MSGSIZE-1] of char;
  fdr,fdw,j:longint;
begin
  (* Открыть канал *)
  if not assignpipe(fdr,fdw) then
  begin
    perror ('Ошибка вызова pipe');
    halt (1);
  end;
  (* Запись в канал *)
  fdwrite (fdw, msg1, MSGSIZE);
  fdwrite (fdw, msg2, MSGSIZE);
  fdwrite (fdw, msg3, MSGSIZE);
  (* Чтение из канала *)
  for j := 1 to 3 do
  begin
    fdread (fdr, inbuf, MSGSIZE);
    writeln(inbuf);
  end;
  halt (0);
end.
На выходе программы получим:
hello, world #1
hello, world #2
hello, world #3
Обратите внимание, что сообщения считываются в том же порядке, в каком они были записаны. Каналы обращаются с данными в порядке «первый вошел – первым вышел» (first-in first-out, или сокращенно FIFO). Другими словами, данные, которые помещаются в канал первыми, первыми и считываются на другом конце канала. Этот порядок нельзя изменить, поскольку вызов fdseek не работает с каналами.


Размеры блоков при записи в канал и чтении из него необязательно должны быть одинаковыми, хотя в нашем примере это и было так. Можно, например, писать в канал блоками по 512 байт, а затем считывать из него по одному символу, так же как и в случае обычного файла. Тем не менее, как будет показано в разделе 7.2, использование блоков фиксированного размера дает определенные преимущества.

Процесс
fdwrite()
fdw > >
v
fdread()
fdr < <

Рис. 7.1. Первый пример работы с каналами
Работа примера показана графически на рис. 7.1. Эта диаграмма позволяет более ясно представить, что процесс только посылает данные сам себе, используя канал в качестве некой разновидности механизма o6paтной связи. Это может показаться бессмысленным, поскольку процесс общается только сам с собой.
Настоящее значение каналов проявляется при использовании вместе с системным вызовом fork, тогда можно воспользоваться тем фактом, что файловые дескрипторы остаются открытыми в обоих процессах. Следующий пример демонстрирует это. Он создает канал и вызывает
fork, затем дочерний процесс обменивается несколькими сообщениями с родительским:
(* Второй пример работы с каналами *)
uses linux, stdio;
const
  MSGSIZE=16;
  msg1:array [0..MSGSIZE-1] of char = 'hello, world #1';
  msg2:array [0..MSGSIZE-1] of char = 'hello, world #2';
  msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';
var
  inbuf:array [0..MSGSIZE-1] of char;
  fdr,fdw,j,pid:longint;
begin
  (* Открыть канал *)
  if not assignpipe (fdr,fdw) then
  begin
    perror ('Ошибка вызова pipe ');
    halt (1);
  end;
  pid := fork;
  case pid of
    -1:
    begin
      perror ('Ошибка вызова fork');
      halt (2);
    end;
    0:
    begin
      (* Это дочерний процесс, выполнить запись в канал *)
      fdwrite (fdw, msg1, MSGSIZE);
      fdwrite (fdw, msg2, MSGSIZE);


  msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';
var
  inbuf:array [0..MSGSIZE-1] of char;
  fdr,fdw,j,pid:longint;
begin
  (* Открыть канал *)
  if not assignpipe (fdr,fdw) then
  begin
    perror ('Ошибка вызова pipe ');
    halt (1);
  end;
  pid := fork;
  case pid of
    -1:
    begin
      perror ('Ошибка вызова fork');
      halt (2);
    end;
    0:
    begin
      (* Дочерний процесс, закрывает дескриптор файла,
       * открытого для чтения и выполняет запись в канал
       *)
      fdclose (fdr);
      fdwrite (fdw, msg1, MSGSIZE);
      fdwrite (fdw, msg2, MSGSIZE);
      fdwrite (fdw, msg3, MSGSIZE);
    end;
    else
    begin
      (* Родительский процесс, закрывает дескриптор файла,
       * открытого для записи и выполняет чтение из канала
       *)
      fdclose (fdw);
      for j := 1 to 3 do
      begin
        fdread (fdr, inbuf, MSGSIZE);
        writeln (inbuf);
      end;
      wait(nil);
    end;
  end;
  halt (0);
end.
В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 7.3.

Дочерний процесс
Родительский процесс
> > fdr
fdread()
^
fdwrite()
fdw > >

Рис. 7.3. Третий пример работы с каналами
Упражнение 7.1. В последнем примере канал использовался для установления связи между родительским и дочерним процессами. Но дескрипторы файлов канала могут передаваться и сквозь через несколько вызовов fork. Это означает, что несколько процессов могут писать в канал и несколько процессов могут читать из него. Для демонстрации этого поведения напишите программу, которая создает три процесса, два из которых выполняют запись в канал, а один – чтение из него. Процесс, выполняющей чтение, должен выводить все получаемые им сообщения на свой стандартный вывод.
Упражнение 7.2. Для установления двусторонней связи между процессами можно создать два канала, работающих в разных направлениях. Придумайте возможный диалог между процессами и реализуйте его при помощи двух каналов.

Первый параметр nfds задает число


uses linux;
Function Select(Nfds:Longint; var readfds,writefds, errorfds:PFDset;
                Var Timeout): Longint;
Первый параметр nfds задает число дескрипторов файлов, которые могут представлять интерес для сервера. Например, если дескрипторы файлов с номерами 0, 1 и 2 присвоены потокам stdin, stdout и stderr соответственно, и открыты еще два файла с дескрипторами 3 и 4, то можно присвоить параметру nfds значение 5. Программист может определять это значение самостоятельно или воспользоваться постоянной FD_SETSIZE, которая определена в файле stdio. Значение постоянной FD_SETSIZE равно максимальному числу дескрипторов файлов, которые могут быть использованы вызовом select.
Второй, третий и четвертый параметры вызова select являются указателями на битовые маски (bit mask), в которых каждый бит соответствует дескриптору файла. Если бит включен, то это обозначает интерес к соответствующему дескриптору файла. Набор readfds определяет дескрипторы, для которых сервер ожидает возможности чтения; набор writefds – дескрипторы, для которых ожидается возможность выполнить запись; набор
errorfds определяет дескрипторы, для которых сервер ожидает появление ошибки или исключительной ситуации, например, по сетевому соединению могут поступить внеочередные данные. Так как работа с битами довольно неприятна и приводит к немобильности программ, существует абстрактный тип данных fdset, а также макросы или функции (в зависимости от конкретной реализации системы) для работы с объектами этого типа. Вот эти макросы для работы с битами файловых дескрипторов:
uses linux;
(* Инициализация битовой маски, на которую указывает fds *)
Procedure FD_ZERO(var fds:fdSet);
(* Установка бита fd в маске, на которую указывает fds *)
Procedure FD_Set(fd:longint;var fds:fdSet);
(* Установлен ли бит fd в маске, на которую указывает fds? *)
Function FD_IsSet(fd:longint;var fds:fdSet):boolean;
(* Сбросить бит fd в маске, на которую указывает fds *)
Procedure FD_Clr(fd:longint;var fds:fdSet);


Следующий пример демонстрирует, как отслеживать состояние двух открытых дескрипторов файлов:
uses linux;
.
.
.
var
  fd1, fd2:longint;
  readset:fdset;
fd1 := fdopen('file1', Open_RDONLY);
fd2 := fdopen('file2', Open_RDONLY);
FD_ZERO(readset);
FD_SET(fd1, readset);
FD_SET(fd2, readset);
case select(5, @readset, nil, nil, nil) of
(* Обработка ввода *)
end;
Пример очевиден, если вспомнить, что переменные fd1 и fd2 представляют собой небольшие целые числа, которые можно использовать в качестве индексов битовой маски. Обратите внимание на то, что аргументам writefds и errorfds в вызове select присвоено значение nil. Это означает, что представляет интерес только чтение из fd1 и fd2.
Пятый параметр вызова select, timeout, является указателем на следующую структуру timeval:
uses linux;
TimeVal  =  Record
   sec,               (* Секунды *)
   usec  :  Longint;  (* и микросекунды *)
end;
Если указатель является нулевым, как в этом примере, то вызов select будет заблокирован до тех пор, пока не произойдет интересующее процесс событие. Если в структуре timeout задано нулевое время, то вызов select завершится немедленно (без блокирования). И, наконец, если структура timeout содержит ненулевое значение, то возврат из вызова select произойдет через заданное число секунд или микросекунд, если файловые дескрипторы неактивны.
Возвращаемое вызовом select значение равно -1 в случае ошибки, нулю – после истечения временного интервала или целому числу, равному числу «интересующих» программу дескрипторов файлов. Следует сделать предостережение: при возврате из вызова select он переустанавливает битовые маски, на которые указывают переменные readfds, writefds или errorfds, сбрасывая маску и снова задавая в ней дескрипторы файлов, содержащие искомую информацию. Поэтому необходимо сохранять копию исходных масок.[11]
Приведем более сложный пример, в котором используются три канала, связанные с тремя дочерними процессами. Родительский процесс должен также отслеживать стандартный ввод.


 (* Программа server - обслуживает три дочерних процесса *)
uses linux,stdio;
const
  MSGSIZE=6;
  msg1:array [0..MSGSIZE-1] of char = 'hello';
  msg2:array [0..MSGSIZE-1] of char = 'bye!!';
type
  tp1=array [0..1] of longint;
  tp3=array [0..2] of tp1;
(* Родительский процесс ожидает сигнала в трех каналах *)
procedure parent(p:tp3);           (* код родительского процесса *)
var
  ch:char;
  buf:array [0..MSGSIZE-1] of char;
  _set, master:fdset;
  i:integer;
begin
  (* Закрыть все ненужные дескрипторы, открытые для записи *)
  for i:=0 to 2 do
    fdclose (p[i][1]);
  (* Задать битовые маски для системного вызова select *)
  FD_ZERO (master);
  FD_SET (0, master);
  for i:=0 to 2 do
    FD_SET (p[i][0], master);
  (* Лимит времени для вызова select не задан, поэтому он
   * будет заблокирован, пока не произойдет событие *)
  _set := master;
  while select (p[2][0] + 1, @_set, nil, nil, nil) > 0 do
  begin
    (* Нельзя забывать и про стандартный ввод,
     * т.е. дескриптор файла fd=0. *)
    if FD_ISSET (0, _set) then
    begin
      write('Из стандартного ввода...');
      fdread (0, ch, 1);
      writeln(ch);
    end;
    for i:=0 to 2 do
    begin
      if FD_ISSET (p[i][0], _set) then
      begin
        if fdread (p[i][0], buf, MSGSIZE) > 0 then
        begin
          writeln('Сообщение от потомка', i);
          writeln('MSG=', buf);
        end;
      end;
    end;
    (* Если все дочерние процессы прекратили работу,
     * то сервер вернется в основную программу
     *)
    if waitpid (-1, nil, WNOHANG) = -1 then
      exit;
  _set := master;
  end;
end;
function child (p:tp1):integer;
var
  count:integer;
begin
  fdclose (p[0]);
  for count:=1 to 2 do
  begin
    fdwrite (p[1], msg1, MSGSIZE);
    (* Пауза в течение случайно выбранного времени *)
    sleep (getpid mod 4);
  end;
  (* Послать последнее сообщение *)
  fdwrite (p[1], msg2, MSGSIZE);


  halt (0);
end;
var
  pip:tp3;
  i:integer;
begin
  (* Создать три канала связи, и породить три процесса. *)
  for i:=0 to 2 do
  begin
    if not assignpipe (pip[i][0],pip[i][1]) then
      fatal ('Ошибка вызова pipe');
    case fork of
      -1:        (* ошибка *)
        fatal ('Ошибка вызова fork');
      0:         (* дочерний процесс *)
        child (pip[i]);
    end;
  end;
  parent (pip);
  halt (0);
end.
Результат данной программы может быть таким:
Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello
d   (пользователь нажимает клавишу d, а затем клавишу Return)
Из стандартного ввода d (повторение символа d)
Из стандартного ввода (повторение символа Return)
Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello
Сообщение от потомка 0
MSG=bye
Сообщение от потомка 1
MSG=bye
Сообщение от потомка 2
MSG=bye
Обратите внимание, что в этом примере пользователь нажимает клавишу d, а затем символ перевода строки (Enter или Return), и это отслеживается в стандартном вводе в вызове select.
Функция SelectText является модификацией Select, предназначенной для работы с текстовыми файлами:


uses linux;
Function SelectText(var T:Text; TimeOut:PTime):Longint;
SelectText выполняет системный вызов Select для файлов типа Text. Время ожидания может быть указано в параметре TimeOut. Вызов SelectText самостоятельно определяет необходимость проверки чтения и записи в зависимости от того, в каком режиме был открыт файл. При Reset выполняется проверка на чтение, при Rewrite и Append – на запись.
Пример использования SelectText:
Uses linux;
Var tv : TimeVal;
   
begin
  Writeln ('Press the <ENTER> to continue the program.');
  { Wait until File descriptor 0 (=Input) changes }
  SelectText (Input,nil);
  { Get rid of <ENTER> in buffer }
  readln;
  Writeln ('Press <ENTER> key in less than 2 seconds...');
  tv.sec:=2;
  tv.usec:=0;
  if SelectText (Input,@tv)>0 then
    Writeln ('Thank you !')
  else
    Writeln ('Too late !');
end.
Связать SelectText и Select можно с помощью функции GetFS, позволяющей из любой файловой переменной получить дескриптор файла.


uses linux;
Function GetFS(Var F:Any File Type):Longint;
Например:
Uses linux;
begin
  Writeln (' File descriptor of input  ',getfs(input));
  Writeln ('File descriptor of output ',getfs(output));
  Writeln ('File descriptor of stderr ',getfs(stderr));
end.
Пример использования SelectText:
Uses linux;
Var tv : TimeVal;
   
begin
  Writeln ('Press the <ENTER> to continue the program.');
  { Wait until File descriptor 0 (=Input) changes }
  SelectText (Input,nil);
  { Get rid of <ENTER> in buffer }
  readln;
  Writeln ('Press <ENTER> key in less than 2 seconds...');
  tv.sec:=2;
  tv.usec:=0;
  if SelectText (Input,@tv)>0 then
    Writeln ('Thank you !')
  else
    Writeln ('Too late !');
end.

Системный вызов mkfifo создает файл


uses linux;
Function MkFifo(PathName:String; Mode:Longint):Boolean;
Системный вызов mkfifo создает файл FIFO с именем, заданным первым параметром pathname. Канал FIFO будет иметь права доступа, заданные параметром mode и измененные в соответствии со значением umask процесса.
После создания канала FIFO он должен быть открыт при помощи вызова fdореn. Поэтому, например, фрагмент кода
uses linux;
.
.
mkfifo('/tmp/fifo', octal(0666));
.
.
fd := fdopen('/tmp/fifo', Open_WRONLY);
открывает канал FIFO для записи. Вызов fdopen будет заблокирован до тех пор, пока другой процесс не откроет канал FIFO для чтения (конечно же, если канал FIFO уже был открыт для чтения, то возврат из вызова open произойдет немедленно).
Можно выполнить не блокирующий вызов fdopen для канала FIFO. Для этого во время вызова должен быть установлен флаг Open_NONBLOCK (определенный в файле linux) и один из флагов Open_RDONLY или Open_WRONLY, например:
fd := fdopen('/tmp/fifo', Open_WRONLY or Open_NONBLOCK);
if fd = -1 then
  perror('Ошибка вызова open для канала FIFO');
Если не существует процесс, в котором канал FIFO открыт для чтения, то этот вызов fdopen вернет значение -1 вместо блокировки выполнения, а переменная linuxerror будет содержать значение Sys_ENXIO. В случае же успешного вызова fdopen последующие вызовы fdwrite для канала FIFO также будут не блокирующими.
Наступило время привести пример. Представим две программы, которые показывают, как можно использовать канал FIFO для реализации системы обмена сообщениями. Эти программы используют тот факт, что вызовы fdread или fdwrite для каналов FIFO, как и для программных каналов, являются неделимыми (для небольших порций данных). Если при помощи канала FIFO пересылаются сообщения фиксированного размера, то отдельные сообщения будут сохраняться, даже если несколько процессов одновременно выполняют запись в канал.
Рассмотрим вначале программу sendmessage, которая посылает отдельные сообщения в канал FIFO с именем fifo. Она вызывается следующим образом:


$ sendmessage 'текст сообщения 1' 'текст сообщения 2'
Обратите внимание на то, что каждое сообщение заключено в кавычки и поэтому считается просто одним длинным аргументом. Если не сделать этого, то каждое слово будет рассматриваться, как отдельное сообщение. Программа sendmessage имеет следующий исходный текст:
(* Программа sendmessage - пересылка сообщений через FIFO *)
uses linux,stdio,strings;
const
  MSGSIZ=63;
  fifo  = 'fifo';
var
  fd,j:integer;
  nwrite:longint;
  msgbuf:array [0..MSGSIZ] of char;
begin
  if paramcount=0 then
  begin
    writeln (stderr, 'Применение: sendmessage сообщение');
    halt (1);
  end;
  (* Открыть канал fifo, установив флаг Open_NONBLOCK *)
  fd := fdopen (fifo, Open_WRONLY or Open_NONBLOCK);
  if fd < 0 then
    fatal ('Ошибка вызова open для fifo');
  (* Посылка сообщений *)
  for j := 1 to paramcount do
  begin
    if length(paramstr(j)) > MSGSIZ then
    begin
      writeln('Слишком длинное сообщение ', paramstr(j));
      continue;
    end;
    strpcopy(msgbuf, paramstr(j));
    nwrite := fdwrite (fd, msgbuf, MSGSIZ + 1);
    if nwrite = -1 then
      fatal ('Ошибка записи сообщения');
  end;
  halt(0);
end.
И снова для вывода сообщений об ошибках использована процедура fatal. Сообщения посылаются блоками по 64 байта при помощи не блокируемого вызова fdwrite. В действительности текст сообщения ограничен 63 символами, а последний символ является нулевым.
Программа rcvmessage принимает сообщения при помощи чтения из канала FIFO. Она не выполняет никаких полезных действий и служит только демонстрационным примером:
(* Программа rcvmessage - получение сообщений из канала fifo *)
uses linux,stdio;
const
  MSGSIZ=63;
  fifo  = 'fifo';
var
  fd:integer;
  msgbuf:array [0..MSGSIZ] of char;
begin
  (* Создать канал fifo, если он еще не существует *)
  if not mkfifo (fifo, octal(0666)) then
    if linuxerror <> Sys_EEXIST then


      fatal ('Ошибка приемника: вызов mkfifo');
  (* Открыть канал fifo для чтения и записи. *)
  fd := fdopen (fifo, Open_RDWR);
  if fd < 0 then
    fatal ('Ошибка при открытии канала fifo');
  (* Прием сообщений *)
  while true do
  begin
    if fdread (fd, msgbuf, MSGSIZ + 1) < 0 then
      fatal ('Ошибка при чтении сообщения');
    (*
     * вывести сообщение; в настоящей программе
     * вместо этого могут выполняться какие-либо
     * полезные действия.
     *)
    writeln('Получено сообщение: ', msgbuf);
  end;
end.
Обратите внимание на то, что канал
FIFO открывается одновременно для чтения и записи (при помощи задания флага Open_RDWR). Чтобы понять, для чего это сделано, предположим, что канал FIFO был открыт только для чтения при помощи задания флага Open_RDONLY. Тогда выполнение программы rcvmessage будет сразу заблокировано в момент вызова fdopen. Когда после старта программы sendmessage в канал FIFO будет произведена запись, вызов fdopen будет разблокирован, программа rcvmessage будет читать все посылаемые сообщения. Когда же канал FIFO станет пустым, а процесс sendmessage завершит работу, вызов fdread начнет возвращать нулевое значение, так как канал
FIFO уже не будет открыт на запись ни в одном процессе. При этом программа rcvmessage войдет в бесконечный цикл. Использование флага Open_RDWR позволяет гарантировать, что, по крайней мере, в одном процессе, то есть самом процессе программы rcvmessage, канал FIFO будет открыт для записи. В результате вызов open всегда будет блокироваться то тех пор, пока в канал
FIFO снова не будут записаны данные.
Следующий диалог показывает, как можно использовать эти две программы. Программа rcvmessage выполняется в фоновом режиме для получения сообщений от разных процессов, выполняющих программу sendmessage.
$ rcvmessage &
40
$ sendmessage 'сообщение 1' 'сообщение 2'
Получено сообщение: сообщение 1
Получено сообщение: сообщение 2
$ sendmessage 'сообщение номер 3'


Получено сообщение: сообщение номер 3
Упражнение 7.6. Программы sendmessage и rcvmessage образуют основу простой системы обмена данными. Сообщения, посылаемые программе rcvmessage, могут, например, быть именами файлов, которые нужно обработать. Проблема заключается в том, что текущие каталоги программ sendmessage и rcvmessage могут быть различными, поэтому относительные пути будут восприняты неправильно. Как можно разрешить эту проблему? Можно ли создать, скажем, спулер печати в большой системе, используя только каналы FIFO?
Упражнение 7.7. Если программу rcvmessage нужно сделать настоящей серверной программой, то потребуется гарантия того, что в произвольный момент времени выполняется только одна копия сервера. Существует несколько способов достичь этого. Один из методов состоит в создании файла блокировки. Рассмотрим следующую процедуру:
uses linux;
const
  lck = '/tmp/lockfile';
function makelock:integer;
var
  fd:integer;
begin
  fd := fdopen (lck, Open_RDWR or Open_CREAT or Open_EXCL, octal(0600));
  if fd < 0 then
  begin
    if linuxerror = SYS_EEXIST then
      halt (1)         (* файл занят другим процессом *)
    else
      halt (127);      (* неизвестная ошибка *)
  end;
  (* Файл блокировки создан, выход из процедуры *)
  fdclose (fd);
  makelock:=0;
end;
Эта процедура использует тот факт, что вызов open осуществляется за один шаг. Поэтому, если несколько процессов пытаются выполнить процедуру makelock, одному из них это удастся первым, и он создаст файл блокировки и «заблокирует» работу остальных. Добавьте эту процедуру к программе sendmessage. При этом, если выполнение программы sendmessage завершается при помощи сигнала SIGHUP или SIGTERM, то она должна удалять файл блокировки перед выходом. Как вы думаете, почему мы использовали в процедуре makelock вызов fdopen, а не fdcreat?

Как обычно, аргумент filedes должен


uses linux, stdio;
Procedure Fcntl(filedes:longint; Cmd:longint; ldata: pflockrec);
Как обычно, аргумент filedes должен быть допустимым дескриптором открытого файла. Для блокировки чтения дескриптор filedes должен быть открыт при помощи флагов Open_RDONLY или Open_RDWR, поэтому в качестве него не подойдет дескриптор, возвращаемый вызовом fdcreat. Для блокировки записи дескриптор filedes должен быть открыт при помощи флагов Open_WRONLY или Open_RDWR.
Как уже упоминалось, параметр вызова cmd определяет выполняемое действие, кодируемое одним из значений, определенных в файле linux. Следуют три команды относятся к блокировке записей:

F_GETLK
Получить описание блокировки на основе данных, передаваемых в аргументе ldata. (Возвращаемая информация описывает первую блокировку, которая препятствует наложению блокировки, описанной структурой ldata)
F_SETLK
Попытаться применить блокировку к файлу и немедленно возвратить управление, если это невозможно. Используется также для удаления активной блокировки
F_SETLKW
Попытаться применить блокировку к файлу и приостановить работу, если блокировка уже наложена другим процессом. Ожидание процесса внутри вызова fcntl можно прервать при помощи сигнала

ldata содержит описание блокировки. Структура flockrec определена в файле stdio и включает следующие элементы:
flockrec=record
  l_type:word;  (*Описывает тип блокировки: F_RDLCK, F_WRLCK, F_UNLCK. *)
  l_whence:word;   (* Тип смещения, как и в вызове lseek *)
  l_start:longint; (* Смещение в байтах *)
  l_len:longint;   (*Размер сегмента данных; 0 означает до конца файла *)
  l_pid:longint;   (*Устанавливается командой F_GETLK *)
end;
Три элемента, l_whence, l_start и l_len, определяют участок файла, который будет заблокирован, проверен или разблокирован. Переменная l_whence идентична третьему аргументу вызова lseek. Она принимает одно из трех значений: SEEK_SET, SEEK_CUR или SEEK_END, обозначая, что смещение должно вычисляться от начала файла, от текущей позиции указателя чтения-записи или конца файла. Элемент l_start устанавливает начальное положение участка файла по отношению к точке, заданной элементом l_whence. Элемент l_len является длиной участка в байтах; нулевое значение обозначает участок с заданной начальной позиции до максимально возможного смещения. На рис. 8.1. показано, как это работает для случая, если значение поля l_whence равно SEEK_CUR. Структура tsemid_ds определена в файле linux.



Указатель файла
|
|
|
|
v
Блокируемый участок
/
\
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
^
|
|
|
l_start
l_len
<–––––––>
<–––––––>
l_whence=
SEEK_CUR

Рис. 8.1. Параметры блокировки
Тип l_type определяет тип блокировки. Он может принимать одно из трех значений, определенных в файле stdio:
F_RDLCK          Выполняется блокировка чтения
F_WRLCK          Выполняется блокировка записи
F_UNLCK          Снимается блокировка заданного участка
Поле l_pid существенно только при выборе команды F_GETLK в вызове fcntl. Если существует блокировка, препятствующая установке блокировки, описанной полями структуры ldata, то значение поля l_pid будет равно значению идентификатора процесса, установившего ее. Другие элементы структуры также будут переустановлены системой. Они будут содержать параметры блокировки, наложенной другим процессом.

Эта процедура возвращает номер ключа


uses ipc;
Function ftok(Path:String; ID:char):TKey;
Эта процедура возвращает номер ключа на основе информации, связанной с файлом path. Параметр id также учитывается и обеспечивает еще один уровень уникальности – «версию проекта»; другими словами, для одного имени path будут получены разные ключи при разных значениях id. Процедура ftok не слишком удобна: например, если удалить файл, а затем создать другой с таким же именем, то возвращаемый после этого ключ будет другим. Она завершится неудачей и вернет значение -1 и в случае, если файл path не существует. Процедуру ftok можно применять в приложениях, использующих функции межпроцессного взаимодействия для работы с определенными файлами или при применении для генерации ключа файла, являющегося постоянной и неотъемлемой частью приложения.

Этот вызов лучше всего представить


uses ipc;
Function msgget(key:TKey; permflags:longint):longint;
Этот вызов лучше всего представить как аналог вызова fdopen или fdcreat. Как уже упоминалось в разделе 8.3.1, параметр key, который, в сущности, является простым числом, идентифицирует очередь сообщений в системе. В случае успешного вызова, после создания новой очереди или доступа к уже существующей, вызов msgget вернет ненулевое целое значение, которое называется идентификатором очереди сообщений (message queue identifier).
Параметр permflags указывает выполняемое вызовом msgget действие, которое задается при помощи двух констант, определенных в файле ipc; они могут использоваться по отдельности или объединяться при помощи операции побитового ИЛИ:
IPC_CREAT      При задании этого флага вызов msgget создает новую очередь сообщений для данного значения, если она еще не существует. Если продолжить аналогию с файлами, то при задании этого флага вызов msgget выполняется в соответствии с вызовом creat, хотя очередь сообщений и не будет «перезаписана», если она уже существует. Если же флаг
IPC_CREAT не установлен и очередь с этим ключом существует, то вызов msgget вернет идентификатор существующей очереди сообщений
IPC_EXCL        Если установлен этот флаг и флаг IPC_CREAT, то вызов предназначается только для создания очереди сообщений. Поэтому, если очередь с ключом key уже существует, то вызов msgget завершится неудачей и вернет значение -1. Переменная linuxerror будет при этом содержать значение Sys_EEXIST
При создании очереди сообщений младшие девять бит переменной permflags используются для задания прав доступа к очереди сообщений аналогично коду доступа к файлу. Они хранятся в структуре tipc_perm, создаваемой одновременно с самой очередью.
Теперь можно вернуться к примеру из раздела 8.3.1.
mqid := msgget(octal(0100), octal(0644) or IPC_CREAT or IPC_EXCL);
Этот вызов предназначен для создания (и только создания) очереди сообщений для значения ключа равного octal(0100). В случае успешного завершения вызова очередь будет иметь код доступа octal(0644). Этот код интерпретируется таким же образом, как и код доступа к файлу, обозначая, что создатель очереди может отправлять и принимать сообщения, а члены его группы и все остальные могут выполнять только чтение. При необходимости для изменения прав доступа или владельца очереди может использоваться вызов msgctl.

Первый из вызовов, msgsnd, используется


uses ipc;
Function msgsnd(mqid:longint; message:PMSGBuf; size:longint;
                msg_type:longint; flags:longint): Boolean;
Function msgrcv(mqid:longint; message:PMSGBuf; size:longint;
                msg_type:longint; flags:longint): Boolean;
Первый из вызовов, msgsnd, используется для добавления сообщения в очередь, обозначенную идентификатором mqid.
Сообщение содержится в структуре message – шаблоне, определенном пользователем и имеющем следующую форму:
PMSGbuf=^TMSGbuf;
TMSGbuf=record
  mtype:longint;                (* Тип сообщения *)
  mtext:array [0..SOMEVALUE-1] of char;   (* Текст сообщения *)
end;
Значение поля mtype может использоваться программистом для разбиения сообщений на категории. При этом значимыми являются только положительные значения; отрицательные или нулевые не могут использоваться (это будет видно из дальнейшего описания операций передачи сообщений). Массив mtext служит для хранения данных сообщения (постоянная SOMEVALUE выбрана совершенно произвольно). Длина посылаемого сообщения задается параметром size вызова msgsnd и может быть в диапазоне от нуля до меньшего из двух значений SOMEVALUE и максимального размера сообщения, определенного в системе.
Параметр flsgs вызова msgsnd может нести только один флаг: IPC_NOWAIT. При неустановленном параметре IPC_NOWAIT вызывающий процесс приостановит работу, если для посылки сообщения недостаточно системных ресурсов. На практике это произойдет, если полная длина сообщений в очереди превысит максимум, заданный для очереди или всей системы. Если флаг IPC_NOWAIT установлен, тогда при невозможности послать сообщение возврат из вызова произойдет немедленно. Возвращаемое значение будет равно -1, и переменная ipcerror будет иметь значение Sys_EAGAIN, означающее необходимость повторения попытки.
Вызов msgsnd также может завершиться неудачей из-за установленных прав доступа. Например, если ни действующий идентификатор пользователя, ни действующий идентификатор группы процесса не связаны с очередью, и установлен код доступа к очереди octal(0660), то вызов msgsnd для этой очереди завершится неудачей. Переменная ipcerror получит значение Sys_EACCES.


Перейдем теперь к чтению сообщений. Для чтения из очереди, заданной идентификатором mqid, используется вызов msgrcv. Чтение разрешено, если процесс имеет права доступа к очереди на чтение. Успешное чтение сообщения приводит к удалению его из очереди.
На этот раз переменная message используется для хранения полученного сообщения, а параметр size задает максимальную длину сообщений, которые могут находиться в этой структуре. Успешный вызов возвращает длину полученного сообщения.
Параметр msg_type определяет тип принимаемого сообщения, он помогает выбрать нужное из находящихся в очереди сообщений. Если параметр msg_type равен нулю, из очереди считывается первое сообщение, то есть то, которое было послано первым. При ненулевом положительном значении параметра msg_type считывается первое сообщение из очереди с заданным типом сообщения. Например, если очередь содержит сообщения со значениями mtype 999, 5 и 1, а параметр msg_type в вызове msgrcv имеет значение 5, то считывается сообщение типа 5. И, наконец, если параметр msg_type имеет ненулевое отрицательное значение, то считывается первое сообщение с наименьшим значением mtype, которое меньше или равно модулю параметра msg_type. Этот алгоритм кажется сложным, но выражает простое правило: если вернуться к нашему предыдущему примеру с тремя сообщениями со значениями mtype 999, 5 и 1, то при значении параметра msg_type в вызове msgrcv равном -999 и троекратном вызове сообщения будут получены в порядке 1, 5, 999.
Последний параметр flags содержит управляющую информацию. В этом параметре могут быть независимо установлены два флага – IPS_NOWAIT и MSG_NOERROR. Флаг IPC_NOWAIT имеет обычный смысл – если он не задан, то процесс будет приостановлен при отсутствии в очереди подходящих сообщений, и возврат из вызова произойдет после поступления сообщения соответствующего типа. Если же этот флаг установлен, то возврат из вызова при любых обстоятельствах произойдет немедленно.
При установленном флаге MSG_NOERROR сообщение будет усечено, если его длина больше, чем size байт, без этого флага попытка чтения длинного сообщения приводит к неудаче вызова msgrcv. К сожалению, узнать о том, что усечение имело место, невозможно.
Этот раздел может показаться сложным: формулировка средств межпроцессного взаимодействия несколько не соответствует по своей сложности и стилю природе ОС UNIX. В действительности же процедуры передачи сообщений просты в применении и имеют множество потенциальных применений, что попробуем продемонстрировать на следующем примере.

Переменная mqid должна быть допустимым


uses ipc;
Function msgctl(mqid:longint; cmd:longint; msg_stat:PMSQid_ds): Boolean;
Переменная mqid должна быть допустимым идентификатором очереди. Пропуская пока параметр cmd, обратимся к третьему параметру msg_stat, который содержит адрес структуры TMSQid_ds. Эта структура определяется в файле ipc и содержит следующие элементы:
PMSQid_ds = ^TMSQid_ds;
TMSQid_ds = record
  msg_perm   : TIPC_perm; (* Владелец/права доступа *)
  msg_first  : PMsg;    
  msg_last   : PMsg;    
  msg_stime  : Longint;   (* Время посл. вызова msgsnd *)
  msg_rtime  : Longint;   (* Время посл. вызова msgrcv *)
  msg_ctime  : Longint;   (* Время посл. изменения *)
  wwait      : Pointer;
  rwait      : pointer;
  msg_cbytes : word;     
  msg_qnum   : word;      (* Число сообщений в очереди *)
  msg_qbytes : word;      (* Макс. число байтов в очереди *)
  msg_lspid  : word;      (* Идентификатор процесса,
                             последним вызвавшего msgsnd *)
  msg_lrpid  : word;      (* Идентификатор процесса,
                             последним вызвавшего msgrcv *)
end;
Структура TIPC_perm, с которой уже встречались ранее, содержит связанную с очередью информацию о владельце и правах доступа. Переменные msg_stime, msg_rtime, msg_ctime содержат число секунд, прошедшее с 00:00 по гринвичскому времени 1 января 1970 г. (Следующий пример покажет, как можно преобразовать такие значения в удобочитаемый формат.)
Параметр cmd в вызове msgctl сообщает системе, какую операцию она должна выполнить. Существуют три возможных значения этого параметра, каждое из которых может быть применено к одному из трех средств межпроцессного взаимодействия. Они обозначаются следующими константами, определенными в файле ipc.

IPC_STAT
Сообщает системе, что нужно поместить информацию о статусе объекта в структуру msg_stat
IPC_SET
Используется для задания значений управляющих параметров очереди сообщений, содержащихся в структуре msg_stat. При этом могут быть изменены только следующие поля:
msq_stat.msg_perm.uid
msq_stat.msg_perm.gid
msq_stat.msg_perm.mode
msq_stat.msg_qbytes
Операция IPC_SET завершится успехом только в случае ее выполнения суперпользователем или текущим владельцем очереди, заданным параметром msq_stat.msg_perm.uid. Кроме того, только суперпользователь может увеличивать значение msg_qbytes – максимальное количество байтов, которое может находиться в очереди
IPC_RMID
Эта операция удаляет очередь сообщений из системы. Она также может быть выполнена только суперпользователем или владельцем очереди. Если параметр command принимает значение IPC_RMID, to параметр msg_stat задается равным nil
<


Следующий пример, программа show_msg, выводит часть информации о статусе объекта очереди сообщений. Программа должна вызываться так:
$ show_msg значение_ключа
Программа show_msg использует библиотечную процедуру ctime для преобразования значений структуры time_t в привычную запись. (Процедура ctime и другие функции для работы с временными значениями будут обсуждаться в главе 12.) Текст программы show_msg:
(* Программа showmsg - выводит данные об очереди сообщений *)
{$mode objfpc}
uses ipc,stdio,sysutils;
procedure mqstat_print(mkey:tkey; msq_id:longint; mstat:pmsqid_ds);
begin
  writeln (#$a'Ключ ', mkey, ', msg_qid ', msq_id, #$a);
  writeln(mstat^.msg_qnum, ' сообщений в очереди'#$a);
  writeln('Последнее сообщение послано процессом ', mstat^.msg_lspid, ' в ',
         ctime(mstat^.msg_stime));
  writeln('Последнее сообщение принято процессом ', mstat^.msg_lrpid, ' в ',
         ctime(mstat^.msg_rtime));
end;
var
  mkey:tkey;
  msq_id:longint;
  msq_status:tmsqid_ds;
begin
  if paramcount<>1 then
  begin
    writeln(stderr, 'Применение: showmsg значение_ключа');
    halt(1);
  end;
  (* Получаем идентификатор очереди сообщений *)
  try
    mkey:=tkey(strtoint(paramstr(1)));
  except
    on e:econverterror do
    begin
      writeln(stderr, 'Нечисловой идентификатор очереди сообщений');
      halt (2);
    end;
  end;
 
  msq_id := msgget(mkey, 0);
  if msq_id = -1 then
  begin
    perror('Ошибка вызова msgget');
    halt(2);
  end;
  (* Получаем информацию о статусе *)
  if not msgctl(msq_id, IPC_STAT, @msq_status) then
  begin
    perror('Ошибка вызова msgctl');
    halt(3);
  end;
  (* Выводим информацию о статусе *)
  mqstat_print(mkey, msq_id, @msq_status);
  halt(0);
end.
Упражнение 8.4. Измените процедуру show_msg так, чтобы она выводила информацию о владельце и правах доступа очереди сообщений.
Упражнение 8.5. Взяв за основу программу chmod, напишите программу msg_chmod, которая изменяет связанные с очередью права доступа. Очередь сообщений также должна указываться значением ее ключа.

Вызов semget аналогичен вызову msgget.


uses ipc;
Function semget(key:Tkey; nsems:longint; permflags:longint):longint;
Вызов semget аналогичен вызову msgget. Дополнительный параметр nsems задает требуемое число семафоров в наборе семафоров; это важный момент – семафорные операции в
System V IPC приспособлены для работы с наборами семафоров, а не с отдельными объектами семафоров. На рис. 8.2 показан набор семафоров. Ниже увидим, что использование целого набора семафоров усложняет интерфейс процедур работы с семафорами.

Индекс 0
Индекс 1
Индекс 2
Индекс 3
semid
semval = 2
semval = 4
semval = 1
semval = 3

nsems=4

Рис.8.2. Набор семафоров
Значение, возвращаемое в результате успешного вызова semget, является идентификатором набора семафоров (semaphore set identifier), который ведет себя почти так же, как идентификатор очереди сообщений. Идентификатор набора семафоров обозначен на рис. 8.2 как semid. Следуя обычной практике, индекс семафора в наборе может принимать значения от 0 до nsems-1.
С каждым семафором в наборе связаны следующие значения:
semval        Значение семафора, положительное целое число. Устанавливается при помощи системных вызовов работы с семафорами, то есть к значениям семафоров нельзя получить прямой доступ из программы, как к другим объектам данных
sempid        Идентификатор процесса, который последним работал с семафором
semcnt        Число процессов, ожидающих увеличения значения семафора
semzcnt      Число процессов, ожидающих обнуления значения семафора


uses ipc;
Function semctl(semid:longint; sem_num:longint; command:longint;
                var ctl_arg:tsemun):longint;
Из определения видно, что функция semctl намного сложнее, чем msgctl. Параметр semid должен быть допустимым идентификатором семафора, возвращенным вызовом semget. Параметр command имеет тот же смысл, что и в вызове msgctl,
– задает требуемую команду. Команды распадаются на три категории: стандартные команды управления средством межпроцессного взаимодействия (такие как IPC_STAT); команды, которые воздействуют только на один семафор; и команды, действующие на весь набор семафоров. Все доступные команды приведены в табл. 8.1.
Таблица 8.1. Коды функций вызова semctl
Стандартные функции межпроцессного взаимодействия
IPC_STAT        Поместить информацию о статусе в поле ctl_arg.stat
IPC_SET          Установить данные о владельце/правах доступа
IPC_RMID        Удалить набор семафоров из системы
Операции над одиночными семафорами
(относятся к семафору sem_num, значение возвращается вызовом semctl)
GETVAL            Вернуть значение семафора (то есть setval)
SETVAL            Установить значение семафора равным ctl_arg.val
GETPID            Вернуть значение sempid
GETNCNT          Вернуть semncnt (см. выше)
GETZCNT          Вернуть semzcnt (см. выше)
Операции над всеми семафорами
GETALL            Поместить все значения setval в массив ctl_arg.array
SETALL            Установить все значения setval из массива ctl_arg.array
Параметр sem_num используется со второй группой возможных операций вызова semctl для задания определенного семафора. Последний параметр ctl_arg является объединением
(записью с вариантами), определенным следующим образом:
PSEMun = ^TSEMun;
TSEMun = record
  case longint of
    0 : (val    : longint);
    1 : (buf    : PSEMid_ds);
    2 : (arr    : PWord);
    3 : (padbuf : PSeminfo);
    4 : (padpad : pointer);
  end;
Каждый элемент объединения представляет некоторый тип значения, передаваемого вызову semctl при выполнении определенной команды. Например, если значение command равно SETVAL, то будет использоваться элемент ctl_arg.val.


Одно из важных применений функции setval заключается в установке начальных значений семафоров, так как вызов semget не позволяет процессу сделать это. Приведенная в качестве примера функция initsem может использоваться для создания одиночного семафора и получения связанного с ним идентификатора набора семафоров. После создания семафора (если семафор еще не существовал) функция semctl присваивает ему начальное значение, равное единице.
{$i pv.inc}
(* Функция initsem - инициализация семафора *)
function initsem(semkey:tkey):longint;
var
  status, semid:longint;
  arg:tsemun;
begin
  status := 0;
  semid := semget (semkey, 1,
                  SEMPERM or IPC_CREAT or IPC_EXCL);
  if semid = -1 then
  begin
    if ipcerror = Sys_EEXIST then
      semid := semget (semkey, 1, 0);
  end
  else
    (* если семафор создается ... *)
  begin
    arg.val := 1;
    status := semctl (semid, 0, SETVAL, arg);
  end;
  if (semid = -1) or (status = -1) then
  begin
    perror ('ошибка вызова initsem');
    initsem:=-1;
    exit;
  end;
  (* Все в порядке *)
  initsem:=semid;
end;
Включаемый файл pv.inc содержит следующие определения:
(* Заголовочный файл для примера работы с семафорами *)
const
  SEMPERM=6 shl 6{0600};
Функция initsem будет использована в примере следующего раздела.

Переменная semid является идентификатором набора


uses ipc;
Function semop(semid:longint;op_array:pointer;num_ops:cardinal):Boolean;
Переменная semid является идентификатором набора семафоров, полученным с помощью вызова semget. Параметр op_array является массивом структур TSEMbuf, определенных в файле ipc. Каждая структура TSEMbuf содержит описание операций, выполняемых над семафором.
И снова основной акцент делается на операции с наборами семафоров, при этом функция semop позволяет выполнять группу операций как атомарную операцию. Это означает, что пока не появится возможность одновременного выполнения всех операций с отдельными семафорами набора, не будет выполнена ни одна из этих операций. Если не указано обратного, процесс приостановит работу до тех пор, пока он не сможет выполнить все операции сразу.
Рассмотрим структуру TSEMbuf. Она включает в себя следующие элементы:
TSEMbuf=record
  sem_num : word;
  sem_op  : integer;
  sem_flg : integer;
end;
Поле sem_num содержит индекс семафора в наборе. Если, например, набор содержит всего один элемент, то значение sem_num должно быть равно нулю. Поле sem_op содержит целое число со знаком, значение которого сообщает функции semop, что необходимо сделать. При этом возможны три случая:
Случай 1: отрицательное значение sem_op
Это обобщенная форма команды для работы с семафорами р(), которая обсуждалась ранее. Действие функции semop можно описать при помощи псевдокода следующим образом (обратите внимание, что ABS() обозначает модуль переменной):
if semval >= ABS(sem_op) then
begin
  semval := semval - ABS(sem_op)
end
else
begin
  if (sem_flg and IPC_NOWAIT) <> 0 then
    немедленно вернуть -1
  else
  behin
    ждать, пока semval не станет больше или равно ABS(sem_op)
    затем, как и выше, вычесть ABS(sem_op)
  end;
end;
Основная идея заключается в том, что функция semop вначале проверяет значение semval, связанное с семафором sem_num. Если значение semval достаточно велико, то оно сразу уменьшается на указанную величину. В противном случае процесс будет ждать, пока значение semval не станет достаточно большим. Тем не менее, если в переменной sem_flg установлен флаг
IPC_NOWAIT, то возврат из вызова sem_op произойдет немедленно, и переменная ipcerror будет содержать код ошибки Sys_EAGAIN.
Случай 2: положительное значение sem_op
Это соответствует традиционной операции
v(). Значение переменной sem_op просто прибавляется к соответствующему значению semval. Если есть процессы, ожидающие изменения значения этого семафора, то они могут продолжить выполнение, если новое значение семафора удовлетворит их условия.
Случай 3: нулевое значение sem_op
В этом случае вызов sem_op будет ждать, пока значение семафора не станет равным нулю; значение semval этим вызовом не будет изменяться. Если в переменной sem_flg установлен флаг
IPC_NOWAIT, а значение semval еще не равно нулю, то функция semop сразу же вернет сообщение об ошибке.

Этот вызов аналогичен вызовам msgget


uses ipc;
Function shmget(key:Tkey; Size:longint; permflags:longint):longint;
Этот вызов аналогичен вызовам msgget и semget. Наиболее интересным параметром вызова является size, который задает требуемый минимальный размер (в байтах) сегмента памяти. Параметр key является значением ключа сегмента памяти, параметр permflags задает права доступа к сегменту памяти и, кроме того, может содержать флаги
IPC_CREAT и IPC_EXCL.

Вызов shmat связывает участок памяти,


uses ipc;
Function shmat(shmid:longint; daddr:pchar; shmflags:longint):pchar;
Вызов shmat связывает участок памяти, обозначенный идентификатором shmid (который был получен в результате вызова shmget) с некоторым допустимым адресом логического адресного пространства вызывающего процесса. Этот адрес является значением, возвращаемым вызовом shmat.
Параметр daddr позволяет программисту до некоторой степени управлять выбором этого адреса. Если этот параметр равен nil, то участок подключается к первому доступному адресу, выбранному системой. Это наиболее простой случай использования вызова shmat. Если параметр daddr не равен nil, то участок будет подключен к содержащемуся в нем адресу или адресу в ближайшей окрестности в зависимости от флагов, заданных в аргументе shmflags. Этот вариант сложнее, так как при этом необходимо знать расположение программы в памяти.
Аргумент shmflag может содержать два флага, SHM_RDONLY и SHM_RND, определенные в заголовочном файле ipc. При задании флага SHM_RDONLY участок памяти подключается только для чтения. Флаг SHM_RND определяет, если это возможно, способ обработки в вызове shmat ненулевого значения daddr.
В случае ошибки вызов shmat вернет значение:
(pchar) -1
Вызов shmdt противоположен вызову shmat и отключает участок разделяемой памяти от логического адресного пространства процесса (это означает, что процесс больше не может использовать его). Он вызывается очень просто:
retval := shmdt(memptr);
Возвращаемое значение retval является логическим значением и равно true в случае успеха и false – в случае ошибки.


uses ipc;
Function shmctl(shmid:longint; command:longint; shm_stat: pshmid_ds):
         Boolean;
Этот вызов в точности соответствует вызову msgctl, и параметр command может, наряду с другими, принимать значения IPC_STAT, IPC_SET и IPC_RMID. В следующем примере этот вызов будет использован с аргументом command равным IPC_RMID.

является дескриптором открытого файла либо


uses linux;
Function TTYName(var f):String;
Function IsATTY(var f):Boolean;
В обоих случаях параметр f
является дескриптором открытого файла либо файловой переменной. Если f
не соответствует терминалу, то функция ttyname вернет пустую строку.
Следующий пример – процедура what_tty выводит имя терминала, связанного с дескриптором файла, если это возможно:
(* Процедура what_tty - выводит имя терминала *)
procedure what_tty(fd:longint);
begin
  if isatty(fd) then
    writeln('fd ',fd,' =>> ', ttyname(fd));
  else
    writeln ('fd ',fd, ' не является терминалом!');
end;
Упражнение 9.2. Измените процедуру ttyopen предыдущего раздела так, чтобы она возвращала дескриптор файла только для терминалов, а не для дисковых файлов или других типов файлов. Для выполнения проверки используйте функцию isatty. Существуют ли еще какие-либо способы сделать это?

Эта функция сохраняет текущее состояние


uses linux;
Function TCGetAttr(ttyfd:longint; var tsaved:TermIOS):Boolean;
Эта функция сохраняет текущее состояние терминала, связанного с дескриптором файла ttyfd в структуре tsaved типа termios. Параметр ttyfd должен быть дескриптором файла, описывающим терминал.


uses linux;
Function TCSetAttr(ttyfd:longint; actions:longint; var tnew:TermIOS):
         Boolean;
Вызов tcsetattr установит новое состояние дисциплины связи, заданное структурой tnew. Второй параметр вызова tcsetattr, переменная actions, определяет, как и когда будут установлены новые атрибуты терминала. Существует три возможных варианта, определенных в файле linux:

ТСSANOW
Немедленное выполнение изменений, что может вызвать проблемы, если в момент изменения флагов драйвер терминала выполняет вывод на терминал
TCSADRAIN
Выполняет ту же функцию, что и TCSANOW, но перед установкой новых параметров ждет опустошения очереди вывода
TCSAFLUSH
Аналогично TCSADRAIN ждет, пока очередь вывода не опустеет, а затем также очищает и очередь ввода перед установкой для параметров дисциплины линии связи значений, заданных в структуре tnew

Следующие две функции используют описанные вызовы. Функция tsave сохраняет текущие параметры, связанные с управляющим терминалом процесса, а функция tback восстанавливает последний набор сохраненных параметров. Флаг saved используется для предотвращения восстановления установок функцией tback, если перед этим не была использована функция tsave.
(* Структура tsaved будет содержать параметры терминала *)
var
  tsaved:termios;
(* Равно TRUE если параметры сохранены *)
const
  saved:boolean=false;
function tsave:boolean;
begin
  if isatty(0) and tcgetattr(0,tsaved) then
  begin
    saved := true;
    tsave := true;
    exit;
  end;
  tsave := false;
end;
function tback:boolean; (* Восстанавливает состояние терминала *)
begin
  if not isatty(0) or not saved then
    tback:=false
  else
    tback:=tcsetattr(0, TCSAFLUSH, tsaved);
end;
Между этими двумя процедурами может быть заключен участок кода, который временно изменяет состояние терминала, например:
uses linux;
begin
  if not tsave then
  begin
    writeln(stderr, 'Невозможно сохранить параметры терминала');
    halt(1);
  end;
  (* Интересующий нас участок *)
  tback;
  halt(0);
end.

в структуре tdes значение скорости


uses linux;
(* Установить скорость ввода *)
Procedure CFSetISpeed(var tdes:TermIOS; Speed:Longint);
(* Установить скорость вывода *)
Procedure CFSetOSpeed(var tdes:TermIOS; Speed:Longint);
uses stdio;
(* Получить скорость ввода *)
function cfgetispeed(var tdes:TermIOS):longint;
(* Получить скорость вывода *)
function cfgetospeed(var tdes:TermIOS):longint;
Следующий пример устанавливает в структуре tdes значение скорости терминала равное 9600 бод. Постоянная В9600 определена в файле stdio.
var
  tdes:termios;
(* Получает исходные настройки терминала * )
tcgetattr(0, tdes);
(* Изменяет скорость ввода и вывода *)
cfsetispeed(tdes, В9600);
cfsetospeed(tdes, В9600);
Конечно, эти изменения не будут иметь эффекта, пока не будет выполнен вызов tcsetattr:
tcsetattr(0, TCSAFLUSH, tdes);
Следующий пример устанавливает режим контроля четности, напрямую устанавливая необходимые биты:
tdes.c_cflag := tdes.c_cflag or PARENB or PARODD;
tcsetattr(0, TCSAFLUSH, tdes);
В этом примере установка флага PARENB включает проверку четности. Установленный флаг PARODD сообщает, что ожидаемый контроль – контроль нечетности. Если флаг PARODD сброшен и установлен флаг PARENB, то предполагается, что используется контроль по четности. (Термин четность, parity, относится к использованию битов проверки при передаче данных. Для каждого символа задается один такой бит. Это возможно благодаря тому, что набор символов ASCII занимает только семь бит из восьми, используемых для хранения символа на большинстве компьютеров. Значение бита проверки может использоваться для того, чтобы полное число битов в байте было либо четным, либо нечетным. Программист также может полностью выключить проверку четности.)

в состояние, соответствующее переводу терминала


uses linux;
Procedure CFMakeRaw(var Tios:TermIOS);
CFMakeRaw устанавливает флаги в структуре Termios в состояние, соответствующее переводу терминала в неканонический режим. Пример:
uses Linux;
procedure ShowTermios(var tios:Termios);
begin
  WriteLn('Input Flags  : $',hexstr(tios.c_iflag,8)+#13);
  WriteLn('Output Flags : $',hexstr(tios.c_oflag,8));
  WriteLn('Line Flags   : $',hexstr(tios.c_lflag,8));
  WriteLn('Control Flags: $',hexstr(tios.c_cflag,8));
end;
var
  oldios,
  tios : Termios;
begin
  WriteLn('Old attributes:');
  TCGetAttr(1,tios);
  ShowTermios(tios);
  oldios:=tios;
  Writeln('Setting raw terminal mode'); 
  CFMakeRaw(tios);
  TCSetAttr(1,TCSANOW,tios);
  WriteLn('Current attributes:');
  TCGetAttr(1,tios);
  ShowTermios(tios);
  TCSetAttr(1,TCSANOW,oldios);
end.
Упражнение 9.3. Напишите программу ttystate, которая выводит текущее состояние терминала, связанного со стандартным вводом. Эта программа должна использовать имена констант, описанные в этом разделе (ICANON, ЕСНОЕ, и т.д.). Найдите в справочном руководстве системы полный список этих имен.
Упражнение 9.4. Напишите программу ttyset, которая распознает выходной формат программы ttystate и настраивает терминал, связанный с ее стандартным выводом в соответствии с описанным состоянием. Есть ли какая-то польза от программ ttystate и ttyset, вместе или по отдельности?

Вызов tcflush очищает заданную очередь.


uses linux;
Function TCFlush(ttyfd, queue:longint):Boolean;
Function TCDrain(ttyfd:longint):Boolean;
Function TCFlow(ttyfd, actions:longint):Boolean;
Function TCSendBreak(ttyfd, duration:longint):longint;
Function TCGetPGrp(Fd:longint;var Id:longint):boolean;
Function TCSetPGrp(Fd,Id:longint):boolean;
Вызов tcflush очищает заданную очередь. Если параметр queue имеет значение TCIFLUSH (определенное в файле stdio), то очищается очередь ввода. Это означает, что все символы в очереди ввода сбрасываются. Если параметр queue имеет значение
TCOFLUSH, то очищается очередь вывода. При значении TCIOFLUSH параметра queue очищаются и очередь ввода, и очередь вывода.
Вызов tcdrain приводит к приостановке работы процесса до тех пор, пока текущий вывод не будет записан в терминал ttyfd.
Вызов tсflow обеспечивает старт/стопное управление драйвером терминала. Если параметр actions равен TCOOFF, то вывод приостанавливается. Он может быть возобновлен при помощи еще одного вызова tсflow со значением параметра actions равным TCOON. Вызов tcflow также может использоваться для посылки драйверу терминала специальных символов START и STOP, это происходит при задании значения параметра actions равного TCIOFF или TCION соответственно. Специальные символы START и STOP служат для приостановки и возобновления ввода с терминала.
Вызов TCSendBreak используется для посылки сигнала прерывания сеанса связи, которому соответствует посылка нулевых битов в течение времени, заданного параметром duration. Если параметр
duration равен 0, то биты посылаются в течение не менее четверти секунды и не более полсекунды. Если параметр duration не равен нулю, то биты будут посылаться в течение некоторого промежутка времени, длительность которого зависит от значения параметра duration и конкретной реализации.
TCSetPGrp устанавливает, а TCGetPGrp – получает идентификатор группы фоновых процессов, сохраняя его в Id.

Процедура inet_addr принимает IP адрес


uses stdio;
function inet_addr(ip_address:pchar):in_addr_t;
Процедура inet_addr принимает IP адрес в форме строки вида 1.2.3.4 и возвращает адрес в виде структуры соответствующего типа. Если вызов процедуры завершается неудачей из-за неверного формата IP адреса, то возвращаемое значение будет равно in_addr_t(-1), например:
var
  server:in_addr_t;
server := inet_addr('197.124.10.1');
Для того чтобы процесс мог ссылаться на адрес своего компьютера, в заголовочном файле stdio определена постоянная INADDR_ANY, содержащая локальный адрес компьютера в формате in_addr_t.

Параметр domain определяет коммуникационный домен,


uses sockets;
Function Socket(Domain,SocketType,Protocol:Longint):Longint;
Параметр domain определяет коммуникационный домен, в котором будет использоваться сокет. Например, значение AF_INET определяет, что будет использоваться домен
Internet. Интерес может представлять также другой домен, AF_UNIX, который используется, если процессы находятся на одном и том же компьютере.
Параметр SocketType определяет тип создаваемого сокета. Значение SOCK_STREAM указывается при создании сокета для работы в режиме виртуальных соединений, а значение
SOCK_DGRAM – для работы в режиме пересылок дейтаграмм. Последний параметр protocol определяет используемый протокол. Этот параметр обычно задается равным нулю, при этом по умолчанию сокет типа SOCK_STREAM будет использовать протокол TCP, а сокет типа SOCK_DGRAM – протокол UDP. Оба данных протокола являются стандартными протоколами
UNIX. Поэтому виртуальное соединение часто называют TCP-соединением, а пересылку дейтаграмм – работой с UDP-сокетами.
Системный вызов socket обычно возвращает неотрицательное целое число, которое является дескриптором файла сокета, что позволяет считать механизм сокетов разновидностью обобщенного файлового ввода/вывода UNIX.

Первый параметр, sockfd, является дескриптором


uses sockets;
Function Bind(sockfd:Longint; Var address; add_len:Longint):Boolean;
Function Bind(sockfd:longint; const address:string):boolean;
Первый параметр, sockfd, является дескриптором файла сокета, созданным с помощью вызова socket, а второй – указателем на обобщенную структуру адреса сокета или адрес в форме строки. В рассматриваемом примере данные пересылаются по сети, поэтому в действительности в качестве этого параметра будет задан адрес структуры TInetSockAddr, содержащей информацию об адресе нашего сервера. Последний параметр содержит размер указанной структуры адреса сокета. В случае успешного завершения вызова bind он возвращает значение 0. В случае ошибки, например, если сокет для этого адреса уже существует, вызов bind возвращает значение -1. Переменная linuxerror будет иметь при этом значение Sys_EADDRINUSE.

Параметр sockfd имеет то же


uses sockets;
Function Listen(sockfd, queue_size:Longint):Boolean;
Параметр sockfd имеет то же значение, что и в предыдущем вызове. В очереди сервера может находиться не более queue_size запросов на соединение. (Спецификация XSI определяет минимальное ограничение сверху на длину очереди равное пяти.)

Системному вызову accept передается дескриптор


uses sockets;
Function Accept(sockfd:Longint;Var address;Var add_len:Longint):Longint;
Function Accept(sockfd:longint;var address:string;
                var SockIn,SockOut:text):Boolean;
Function Accept(sockfd:longint;var address:string;
                var SockIn,SockOut:File):Boolean;
Function Accept(sockfd:longint;var address:TInetSockAddr;
                var SockIn,SockOut:File):Boolean;
Системному вызову accept передается дескриптор сокета, для которого ведется прием соединений. Возвращаемое значение соответствует идентификатору нового сокета, который будет использоваться для связи. Параметр address заполняется информацией о клиенте. Так как связь использует соединение, адрес клиента знать не обязательно, поэтому можно присвоить параметру address значение nil. Если значение address не равно nil, то переменная, на которую указывает параметр add_len, первоначально должна содержать размер структуры адреса, заданной параметром address. После возврата из вызова accept переменная add_len будет содержать реальный размер записанной структуры.
Вторая, третья и четвертая формы вызова accept эквивалентны вызову первой с последующим использованием функции Sock2Text, преобразующей сокет sockfd в две файловые переменные типа Text, одна из которых отвечает за чтение из сокета (SockIn), а другая – за запись в сокет (SockOut).
После подстановки вызовов bind, listen и accept текст программы сервера примет вид:
(* Серверный процесс *)
uses sockets,stdio,linux;
const
  SIZE=sizeof(tinetsockaddr);
  (* Инициализация сокета Internet с номером порта 7000
   * и локальным адресом, заданным в постоянной INADDR_ANY *)
  server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);
var
  newsockfd:longint;
  sockfd:longint;
begin
  (* Создает сокет *)
  sockfd := socket (AF_INET, SOCK_STREAM, 0);
  if sockfd = -1 then
  begin
    perror ('Ошибка вызова socket');
    halt (1);
  end;
  (* Связавает адрес с сокетом *)


  if not bind (sockfd, server, SIZE) then
  begin
    perror ('Ошибка вызова bind');
    halt (1);
  end;
  (* Включает прием соединений *)
  if not listen (sockfd, 5) then
  begin
    perror ('ошибка вызова listen');
    halt (1);
  end;
  while true do
  begin
    (* Принимает очередной запрос на соединение *)
    newsockfd := accept (sockfd, client, clientaddrlen);
    if newsockfd = -1 then
    begin
      perror ('Ошибка вызова accept');
      continue;
    end;
    (*
    Создает дочерний процесс для работы с соединением.
    Если это дочерний процесс,
      то в цикле принимает данные от клиента
      и посылает ему ответы.
    *)
  end;
end.
Обратите внимание на то, что сервер использует константу INADDR_ANY, coответствующую адресу локального компьютера.
Теперь имеется серверный процесс, способный переходить в режим приёма соединений и принимать запросы на установку соединений. Рассмотрим, как клиент должен обращаться к серверу.

Первый параметр csockfd является дескриптором


uses sockets;
Function Connect(csockfd:Longint; Var address; add_len:Longint): Longint;
Function Connect(csockfd:longint; const address:string;
                 var SockIn,SockOut:text):Boolean;
Function Connect(csockfd:longint; const address:string;
                 var SockIn,SockOut:file):Boolean;
Function Connect(csockfd:longint; const address:TInetSockAddr;
                 var SockIn,SockOut:file):Boolean;
Первый параметр csockfd является дескриптором сокета клиента и не имеет отношения к дескриптору сокета на сервере. Параметр address указывает на структуру, содержащую адрес сервера, либо на адрес в формате строки. Параметр add_len определяет размер используемой структуры адреса.
Вторая, третья и четвертая формы вызова connect эквивалентны вызову первой с последующим использованием функции Sock2Text, преобразующей сокет sockfd в две файловые переменные типа Text, одна из которых отвечает за чтение из сокета (SockIn), а другая – за запись в сокет (SockOut).
Продолжая составление рассматриваемого примера, запишем следующий вариант текста программы клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;
const
  SIZE=sizeof(tinetsockaddr);
  server:tinetsockaddr=(family:AF_INET; port:7000);
var
  sockfd:longint;
begin
  (* Преобразовать и сохранить IP address сервера *)
  server.addr := inet_addr ('127.0.0.1');
  (* Создать сокет *)
  sockfd := socket (AF_INET, SOCK_STREAM, 0);
  if sockfd = -1 then
  begin
    perror ('Ошибка вызова socket');
    halt (1);
  end;
  (* Соединяет сокет с сервером *)
  if not connect (sockfd, server, SIZE) then
  begin
    perror ('Ошибка вызова connect');
    halt (1);
  end;
  (* Обмен данными с сервером *)
end.
Адрес сервера преобразуется в нужный формат при помощи вызова inet_addr. Адреса известных компьютеров локальной сети обычно можно найти в файле /etc/hosts.

Вызов recv имеет четыре параметра:


uses sockets;
Function Recv(sockfd:Longint; Var buffer; length,Flags:Longint):Longint;
Function Send(sockfd:Longint; Var buffer; length,Flags:Longint):Longint;
Вызов recv имеет четыре параметра: дескриптор файла filedes, из которого читаются данные, буфер buffer, в который они помещаются, размер буфера length и поле флагов flags.
Параметр flags указывает дополнительные опции получения данных. Его возможные значения определяются комбинациями следующих констант:

MSG_PEEK
Процесс может просматривать данные, не «получая» их
MSG_OOB
Обычные данные пропускаются. Процесс принимает только срочные данные, например, сигнал прерывания
MSG_WAITALL
Возврат из вызова recv произойдет только после получения всех данных

При аргументе flags равном нулю вызов send работает точно так же, как и вызов write, пересылая массив данных буфера buffer в сокет sockfd. Параметр length задает размер массива данных. Аналогично вызову recv параметр flags определяет опции передачи данных. Его возможные значения определяются комбинациями следующих констант:

MSG_OOB
Передать срочные (out of band) данные
MSG_DONTROUTE
При передаче сообщения игнорируются условия маршрутизации протокола более низкого уровня. Обычно это означает, что сообщение посылается по прямому, а не по самому быстрому маршруту (самый быстрый маршрут не обязательно прямой и может зависеть от текущего распределения нагрузки сети)

Теперь с помощью этих вызовов можно реализовать обработку данных на серверной стороне:
(* Серверный процесс *)
var
  c:char;
  client:tinetsockaddr;
  clientaddrlen:longint;
begin
  (* Приведенная выше инициализация сокета *)
  .
  .
  .
  while true do
  begin
    (* Принимает запрос на установку соединения *)
    newsockfd := accept (sockfd, client, clientaddrlen);
    if newsockfd = -1 then
    begin
      perror ('Ошибка вызова accept');
      continue;
    end;
    (* Создает дочерний процесс для работы с соединением *)


    if fork = 0 then
    begin
      (* Принимает данные *)
      while recv (newsockfd, c, 1, 0) > 0 do
      begin
        (* Преобразовывает строчный символ в прописной *)
        c := upcase (c);
        (* Пересылает символ обратно *)
        send (newsockfd, c, 1, 0);
      end;
    end;
  end;
end.
Напомним, что использование вызова fork позволяет серверу обслуживать несколько клиентов. Цикл работы клиентского процесса может быть реализован так:
(* Клиентский процесс *)
var
  sockfd:longint;
  c,rc:char;
begin
  (* Приведенная выше инициализация сокета и запрос
   * на установку соединения *)
  (* Обмен данными с сервером *)
  rc := #$a;
  while true do
  begin
    if rc = #$a then
      writeln ('Введите строчный символ');
    c:=char(getchar);
    send (sockfd, c, 1, 0);
    recv (sockfd, rc, 1, 0);
    write (rc)
  end;
end.

Если параметр send_addr равен nil,


uses stdio;
function recvfrom(sockfd:longint; var message; length, flags:longint;
                  var send_addr:tsockaddr; var add_len:longint):longint;
function sendto(sockfd:longint; var message; length, flags:longint;
                var dest_addr:tsockaddr; dest_len:longint):longint;
Если параметр send_addr равен nil, то вызов recvfrom работает точно так же, как и вызов recv. Параметр message указывает на буфер, в который помещается принимаемая дейтаграмма, а параметр length задает число байтов, которые должны быть считаны в буфер. Параметр flags принимает те же самые значения, что и в вызове recv. Два последних параметра помогают установить двустороннюю связь с помощью UDP-сокета. В структуру send_addr будет помещена информация об адресе и порте, откуда пришел прочитанный пакет. Это позволяет принимающему процессу направить ответ пославшему пакет процессу. Последний параметр является указателем на целочисленную переменную типа longint, в которую помещается длина записанного в структуру send_addr адреса.
Вызов sendto противоположен вызову recvfrom. В этом вызове параметр dest_addr задает адрес узла сети и порт, куда должно быть передано сообщение, а параметр dest_len определяет длину адреса.
Адаптируем пример для модели дейтаграммных посылок.
(* Сервер *)
uses sockets,linux,stdio;
const
  SIZE=sizeof(tinetsockaddr);
  (* Локальный серверный порт *)
  server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);
  client_len:longint=SIZE;
var
  sockfd:longint;
  c:char;
  (* Структура, которая будет содержать адрес процесса 2 *)
  client:tinetsockaddr;
begin
  (* Установить абонентскую точку сокета *)
  sockfd := socket (AF_INET, SOCK_DGRAM, 0);
  if sockfd = -1 then
  begin
    perror ('Ошибка вызова socket');
    halt (1);
  end;
  (* Связать локальный адрес с абонентской точкой *)
  if not bind (sockfd, server, SIZE) then
  begin
    perror ('Ошибка вызова bind');
    halt (1);


  end;
  (* Бесконечный цикл ожидания сообщений *)
  while true do
  begin
    (* Принимает сообщение и записывает адрес клиента *)
    if recvfrom (sockfd, c, 1, 0, tsockaddr(client), client_len) = -1 then
    begin
      perror ('Сервер: ошибка при приеме');
      continue;
    end;
    c := upcase (c);
    (* Посылает сообщение обратно *)
    if sendto (sockfd, c, 1, 0, tsockaddr(client), client_len) = -1 then
    begin
      perror ('Сервер: ошибка при передаче');
      continue;
    end;
  end;
end.
Новый текст клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;
const
  SIZE=sizeof(tinetsockaddr);
 
  (* Локальный порт на клиенте *)
  client:tinetsockaddr = (family:AF_INET; port:INADDR_ANY; addr:INADDR_ANY);
  (* Адрес удаленного сервера *)
  server:tinetsockaddr = (family:AF_INET; port:7000);
var
  sockfd:longint;
  c:char;
begin
  (* Преобразовать и записать IP адрес *)
  server.addr := inet_addr ('127.0.0.1');
  (* Установить абонентскую точку сокета *)
  sockfd := socket (AF_INET, SOCK_DGRAM, 0);
  if sockfd = -1 then
  begin
    perror ('Ошибка вызова socket');
    halt (1);
  end;
  (* Связать локальный адрес с абонентской точкой сокета. *)
  if not bind (sockfd, client, SIZE) then
  begin
    perror ('Ошибка вызова bind');
    halt (1);
  end;
  (* Считать символ с клавиатуры *)
  while fdread (0, c, 1) <> 0 do
  begin
    (* Передать символ серверу *)
    if sendto (sockfd, c, 1, 0, tsockaddr(server), SIZE) = -1 then
    begin
      perror ('Клиент: ошибка передачи');
      continue;
    end;
    (* Принять вернувшееся сообщение *)
    if recv (sockfd, c, 1, 0) = -1 then
    begin
      perror ('Клиент: ошибка приема');
      continue;
    end;
    fdwrite (1, c, 1);
  end;
end.
Упражнение 10.4. Запустите сервер и несколько клиентов. Как сервер определяет, от какого клиента он принимает сообщение?

и fclose являются эквивалентами вызовов


uses stdio;
function fopen(filename:pchar; _type:pchar):pfile;
function fclose(_stream:pfile):integer;
Библиотечные процедуры fopen и fclose являются эквивалентами вызовов fdopen и fdclose. Процедура fopen открывает файл, заданный параметром filename, и связывает с ним структуру TFILE. В случае успешного завершения процедура fopen возвращает указатель на структуру TFILE, идентифицирующую открытый файл, объект PFILE также часто называют открытым потоком ввода/вывода (эта структура FILE является элементом внутренней таблицы). Процедура fclose закрывает файл, заданный параметром _stream, и, если этот файл использовался для вывода, также сбрасывает на диск все данные из внутреннего буфера.
В случае неудачи процедура fopen возвращает нулевой указатель nil, определенный в файле system. В этом случае, так же как и для вызова fdopen, переменная linuxerror будет содержать код ошибки, указывающий на ее причину.
Второй параметр процедуры fopen указывает на строку, определяющую режим доступа. Она может принимать следующие основные значения:

r
Открыть файл filename только для чтения. (Если файл не существует, то вызов завершится неудачей и процедура fopen вернет нулевой указатель nil)
w
Создать файл filename и открыть его только для записи. (Если файл уже существует, то он будет усечен до нулевой длины)
а
Открыть файл filename только для записи. Все данные будут добавляться в конец файла. Если файл не существует, он создается

Файл может быть также открыт для обновления, то есть программа может выполнять чтение из файла и запись в него. Другими словами, программа может одновременно выполнять для файла и операции ввода, и операции вывода без необходимости открывать его заново. В то же время из-за механизма буферизации такой ввод/вывод будет более ограниченным, чем режим чтения/записи, поддерживаемый вызовами fdread и fdwrite. В частности, после вывода нельзя осуществить ввод без вызова одной из стандартных процедур ввода/вывода fseek или rewind. Эти процедуры изменяют положение внутреннего указателя чтения/ записи и обсуждаются ниже. Аналогично нельзя выполнить вывод после ввода без вызова процедур fseek или rewind или процедуры ввода, которая перемещает указатель в конец файла. Режим обновления обозначается символом +


в конце аргумента, передаваемого процедуре fopen. Вышеприведенные режимы можно дополнить следующим образом:

r+
Открыть файл filename для чтения и записи. Если файл не существует, то вызов снова завершится неудачей
w+
Создать файл filename и открыть его для чтения и записи. (Если файл уже существует, то он будет усечен до нулевой длины)
а+
Открыть файл filename для чтения и записи. При записи данные будут добавляться в конец файла. Если файл не существует, то он создается

В некоторых системах для доступа к двоичным, а не текстовым файлам, к строке также нужно добавлять символ b, например, rb.
Если файл создается при помощи процедуры fopen, для него обычно устанавливается код доступа octal(0666). Это позволяет всем пользователям выполнять чтение из файла и запись в него. Эти права доступа по умолчанию могут быть изменены установкой ненулевого значения атрибута процесса umask. (Системный вызов umask был изучен в главе 3.)
Следующий пример программы показывает использование процедуры fopen и ее связь с процедурой fclose. При этом, если файл indata существует, то он открывается для чтения, а файл outdata создается (или усекается до нулевой длины, если он существует). Процедура fatal предназначена для вывода сообщения об ошибке, ее описание было представлено в предыдущих главах. Она просто передает свой аргумент процедуре perror, а затем вызывает halt для завершения работы программы.
uses stdio;
const
  inname:pchar = 'indata';
  outname:pchar = 'outdata';
function fatal(s:pchar):integer;
begin
  perror (s);
  halt (1);
end;
var
  inf,outf:pfile;
begin
  inf := fopen (inname, 'r');
  if inf = nil then
    fatal ('Невозможно открыть входной файл');
  outf := fopen (outname, 'w');
  if outf = nil then
    fatal ('Невозможно открыть выходной файл');
  (* Выполняются какие-либо действия ... *)
  fclose (inf);
  fclose (outf);
  halt (0);
end.
На самом деле, в данном случае оба вызова fсlose не нужны. Дескрипторы, связанные с файлами inf и outf, будут автоматически закрыты при завершении работы процесса, и вызов halt автоматически сбросит данные из буфера указателя outf на диск, записав их в файл outdata.
С процедурой fclose тесно связана процедура fflush:


uses stdio;
function fflush(_stream:pfile):integer;
Выполнение этой процедуры приводит к сбросу на диск содержимого буфера вывода, связанного с потоком _stream. Другими словами, данные из буфера записываются в файл немедленно, независимо от того, заполнен буфер или нет. Это гарантирует, что содержимое файла на диске будет соответствовать тому, как он выглядит с точки зрения процесса. (Процесс считает, что данные записаны в файл с того момента, как они оказываются в буфере, поскольку механизм буферизации прозрачен.) Любые данные из буфера ввода этим вызовом предусмотрительно отбрасываются.
Поток _stream остается открытым после завершения процедуры fflush. Как и процедура fclose, процедура fflush возвращает постоянную EOF в случае ошибки и нулевое значение – в случае успеха. (Значение постоянной EOF задано в файле
stdio равным –1. Оно обозначает конец файла, но может также использоваться для обозначения ошибок.)


uses stdio;
function getc(inf:pfile):integer;
function putc(c:integer; outf:pfile):integer;
Наиболее простыми из процедур стандартной библиотеки ввода/вывода являются процедуры getc и putc. Процедура getc возвращает очередной символ из входного потока inf. Процедура putc помещает символ, обозначенный параметром с, в выходной поток outf.
В обеих процедурах символ с имеет тип integer, а не char, что позволяет процедурам использовать наборы 16-битовых «широких» символов. Это также позволяет процедуре getc возвращать значение –1, находящееся вне диапазона возможных значений типа char. Постоянная EOF используется процедурой getc для обозначения того, что либо достигнут конец файла, либо произошла ошибка. Процедура putc также может возвращать значение EOF в случае ошибки.
Следующий пример является новой версией процедуры
copyfile, представленной в главе 2; в данном случае вместо использования вызовов
fdread и fdwrite используются процедуры getc и putc:
uses stdio;
(* Скопировать файл f1 в файл f2
 * при помощи стандартных процедур ввода/вывода
 *)
function copyfile(const f1, f2:pchar):integer;
var
  inf, outf:pfile;
  c:longint;
begin
  inf := fopen (f1, 'r');
  if inf = nil then
  begin
    copyfile:=-1;
    exit;
  end;
  outf := fopen (f2, 'w');
  if outf = nil then
  begin
    fclose (inf);
    copyfile:=-2;
    exit;
  end;
  c := getc (inf);
  while c <> EOF do
  begin
    putc (c, outf);
    c := getc (inf);
  end;
  fclose (inf);
  fclose (outf);
  copyfile:=0;
end;
Копирование выполняет внутренний цикл while. Снова обратите внимание на то, что переменная с
имеет тип longint, а не char.
Упражнение 11.1. В упражнениях 2.4 и 2.5 мы описали программу count, которая выводит число символов, слов и строк во входном файле. (Напомним, что слово определялось, как любая последовательность алфавитно-цифровых символов или одиночный пробельный символ.) Перепишите программу count, используя процедуру getc.
Упражнение 11.2. Используя процедуру getс, напишите программу, выводящую статистику распределения символов в файле, то есть число раз, которое встречается в файле каждый символ. Один из способов сделать это состоит в использовании массива целых чисел типа long, который будет содержать счетчики числа символов, а затем рассматривать значение каждого символа в качестве индекса увеличиваемого счетчика массива. Программа также должна рисовать простую гистограмму полученного распределения при помощи процедур printf и putc.


uses stdio;
function ungetc(c:integer; _stream:pfile):integer;
Процедура ungetc возвращает символ с в поток _stream. Это всего лишь логическая операция. Входной файл не будет при этом изменяться. В случае успешного завершения процедуры ungetc символ с будет следующим символом, который будет считан процедурой getc. Гарантируется возврат только одного символа. В случае неудачной попытки вернуть символ с процедура ungetc возвращает значение EOF. Попытка вернуть сам символ EOF должна всегда завершаться неудачей. Но это обычно не представляет проблемы, так как все последующие вызовы процедуры getc после достижения конца файла приведут к возврату символа EOF.
Обычно процедура ungetc используется для восстановления исходного состояния входного потока после чтения лишнего символа для проверки условия. Следующая процедура getword применяет это простой подход для ввода строки, которая содержит либо непрерывную последовательность алфавитно-цифровых символов, либо одиночный нетекстовый символ. Конец файла кодируется возвращенным значением nil. Процедура getword принимает в качестве аргумента указатель на структуру TFILE. Она использует для проверки два макроса, определенные в файле stdio. Первый из них, isspace, определяет, является ли символ пробельным символом, таким как символ пробела, табуляции или перевода строки. Второй, isalnum, проверяет, является ли символ алфавитно-цифровым, то есть цифрой или буквой.
(* В этом файле определены isspace и isalnum *)
uses stdio;
const
  MAXTOK=256;
var
  inbuf:array [0..MAXTOK] of char;
function getword (inf:pfile):pchar;
var
  c,count:integer;
begin
  count:=0;
  (* Удалить пробельные символы *)
  repeat
    c := getc (inf);
  until not isspace (c);
  if c = EOF then
  begin
    getword:=nil;
    exit;
  end;
  if not isalnum (c) then    (* символ не является алфавитно-цифровым *)
  begin
    inbuf[count] := char(c);
    inc(count);
  end
  else
  begin
    (* Сборка "слова" *)


    repeat
      if count < MAXTOK then
      begin
        inbuf[count] := char(c);
        inc(count);
      end;
      c := getc (inf);
    until not isalnum (c);
    ungetc (c, inf);         (* вернуть символ *)
  end;
  inbuf[count] := #0;
  (* нулевой символ в конце строки *)
  getword:=inbuf;
end;
var
  word:pchar;
begin
  while true do
  begin
    word := getword (stdin);
    if word <> nil then
      puts (word)
    else
      break;
  end;
end.
Если подать на вход программы следующий ввод
Это данные
  на входе
 программы!!!
то процедура getword вернет следующую последовательность строк:
Это
данные
на
входе
программы
!
!
!
Упражнение 11.3. Измените процедуру getword так, чтобы она распознавала также числа, которые могут начинаться со знака минус или плюс и могут содержать десятичную точку.

Функция ferror является предикатом, который


uses stdio;
function ferror(_stream:pfile):integer;
function feof(_stream:pfile):integer;
procedure clearerr(_stream:pfile);
function fileno(_stream:pfile):longint;
Функция ferror является предикатом, который возвращает ненулевое значение, если в потоке _stream возникла ошибка во время последнего запроса на ввод или вывод. Ошибка может возникать в результате вызова примитивов доступа к файлам (fdread, fdwrite и др.) внутри процедуры стандартного ввода/вывода. Если же функция ferror возвращает нулевое значение, значит, ошибок не было. Функция ferror может использоваться следующим образом:
if ferror(_stream) <> 0 then
begin
  (* Обработка ошибок *)
end
else
begin
  (* Ошибок нет *)
end;
Функция feof является предикатом, возвращающим ненулевое значение, если для потока _stream достигнут конец файла. Возврат нулевого значения просто означает, что этого еще не произошло.
Функция clearerr используется для сброса индикаторов ошибки и флага достижения конца файла для потока _stream. При этом гарантируется, что последующие вызовы функций ferror и feof для этого файла вернут нулевое значение, если за это время не произошло что-нибудь еще. Очевидно, что функция clearerr бывает необходима редко.
Функция fileno является вспомогательной и не связана с обработкой ошибок. Она возвращает целочисленный дескриптор файла, содержащийся в структуре TFILE, на которую указывает параметр _stream. Это может быть полезно, если нужно передать какой-либо процедуре дескриптор файла, а не идентификатора потока TFILE. Однако не следует использовать процедуру fileno для смешивания вызовов буферизованного и небуферизованного ввода/вывода. Это почти неизбежно приведет к хаосу.
Следующий пример – процедура egetc использует функцию ferror, чтобы отличить ошибку от достижения конца файла при возврате процедурой getc значения ЕОF.
(* Процедура egetc - getc с проверкой ошибок *)
uses stdio;
function egetc (stream:pfile):longint;
var
  c:longint;
begin
  c := getc (stream);
  if c = EOF then
  begin
    if ferror (stream) <> 0 then
    begin
      writeln (stderr, 'Фатальная ошибка: ошибка ввода');
      halt (1);
    end
    else
      writeln (stderr, 'Предупреждение: EOF');
  end;
  egetc:=c;
end;

Процедура gets считывает последовательность символов


uses stdio;
function gets(buf:pchar):pchar;
function fgets(buf:pchar; nsize:integer; inf:pfile):pchar;
Процедура gets считывает последовательность символов из потока стандартного ввода (stdin), помещая все символы в буфер, на который указывает аргумент buf. Символы считываются до тех пор, пока не встретится символ перевода строки или конца файла. Символ перевода строки newline отбрасывается, и вместо него в буфер buf помещается нулевой символ, образуя завершенную строку. В случае возникновения ошибки или при достижении конца файла возвращается значение nil.
Процедура fgets является обобщенной версией процедуры gets. Она считывает символы из потока inf в буфер buf до тех пор, пока не будет считано nsize-1 символов или не встретится раньше символ перевода строки newline или не будет достигнут конец файла. В процедуре fgets символы перевода строки newline не отбрасываются, а помещаются в конец буфера (это позволяет вызывающей функции определить, в результате чего произошел возврат из процедуры fgets). Как и процедура gets, процедура fgets возвращает указатель на буфер buf в случае успеха и nil – в противном случае.
Процедура gets является довольно примитивной. Так как она не знает размер передаваемого буфера, то слишком длинная строка может привести к возникновению внутренней ошибки в процедуре. Чтобы избежать этого, можно использовать процедуру fgets (для стандартного ввода stdin).
Следующая процедура yesno использует процедуру fgets для получения положительного или отрицательного ответа от пользователя; она также вызывает макрос isspace для пропуска пробельных символов в строке ответа:
(* Процедура yesno - получить ответ от пользователя *)
uses stdio;
const
  YES=1;
  NO=0;
  ANSWSZ=80;
  pdefault:pchar = 'Наберите "y" (YES), или "n" (NO)';
  error:pchar = 'Неопределенный ответ';
function yesno (prompt:pchar):integer;
var
  buf:array [0..ANSWSZ-1] of char;
  p_use, p:pchar;
begin
  (* Вывести приглашение, если он не равно nil.


uses stdio;
function puts(str:pchar):integer;
function fputs(str:pchar; outf:pfile):integer;
Процедура puts записывает все символы (кроме завершающего нулевого символа) из строки str на стандартный вывод (stdout). Процедура fputs записывает строку str в поток outf. Для обеспечения совместимости со старыми версиями системы процедура puts добавляет в конце символ перевода строки, процедура же fputs не делает этого. Обе функции возвращают в случае ошибки значение EOF.
Следующий вызов процедуры puts приводит к выводу сообщения Hello, world на стандартный вывод, при этом автоматически добавляется символ перевода строки newline:
puts('Hello, world');


uses stdio;
function fread(buffer:pointer; size, nitems:longint; inf:pfile):longint;
function fwrite(buffer:pointer; size,nitems:longint; outf:pfile):longint;
Эти две полезные процедуры обеспечивают ввод и вывод произвольных нетекстовых данных. Процедура fread считывает nitems объектов данных из входного файла, соответствующего потоку inf. Считанные байты будут помещены в массив buffer. Каждый считанный объект представляется последовательностью байтов длины size. Возвращаемое значение дает число успешно считанных объектов.
Процедура fwrite является точной противоположностью процедуры fread. Она записывает данные из массива buffer в поток outf. Массив buffer содержит nitems объектов, размер которых равен size. Возвращаемое процедурой значение дает число успешно записанных объектов.
Эти процедуры обычно используются для чтения и записи содержимого произвольных структур данных языка Паскаль. При этом параметр size часто содержит конструкцию sizeof, которая возвращает размер структуры в байтах.
Следующий пример показывает, как все это работает. В нем используется шаблон структуры dict_elem. Экземпляр этой структуры может представлять собой часть записи простой базы данных. Используя терминологию баз данных, структура dict_elem представляет собой запись, или атрибут, базы данных. Мы поместили определение структуры dict_elem в заголовочный файл dict.inc, который выглядит следующим образом:
(* dict.inc - заголовочный файл для writedict и readdict *)
uses stdio;
 (* Структура dict_elem элемент данных *)
 (* (соответствует полю базы данных) *)
type dict_elem=record
  d_name:array [0..14] of char;  (* имя элемента словаря *)
  d_start:integer;               (* начальное положение записи *)
  d_length:integer;              (* длина поля *)
  d_type:integer;                (* обозначает тип данных *)
end;
pdict_elem=^dict_elem;
const
  ERROR=-1;
  SUCCESS=0;
He вдаваясь в смысл элементов структуры, введем две процедуры writedict и readdict, которые соответственно выполняют запись и чтение массива структур dict_elem. Файлы, создаваемые при помощи этих двух процедур, можно рассматривать как простые словари данных для записей в базе данных.


Процедура writedict имеет два параметра, имя входного файла и адрес массива структур dict_elem. Предполагается, что этот список заканчивается первой структурой массива, в которой элемент d_length равен нулю.
{$i dict.inc}
function writedict (const dictname:pchar; elist:pdict_elem):integer;
var
  j:integer;
  outf:pfile;
begin
  (* Открыть входной файл *)
  outf := fopen (dictname, 'w');
  if outf = nil then
  begin
    writedict:=ERROR;
    exit;
  end;
  (* Вычислить размер массива *)
  j:=0;
  while elist[j].d_length <> 0 do
    inc(j);
  (* Записать список структур dict_elem *)
  if fwrite (elist, sizeof (dict_elem), j, outf) < j then
  begin
    fclose (outf);
    writedict:=ERROR;
    exit;
  end;
  fclose (outf);
  writedict:=SUCCESS;
end;
Обратите внимание на использование sizeof(dict_elem) для сообщения процедуре fwrite размера структуры dict_elem в байтах.
Процедура readdict использует процедуру fread для считывания списка структур из файла. Она имеет три параметра: указатель на имя файла словаря indictname, указатель inlist на массив структур dict_elem, в который будет загружен список структур из файла, и размер массива maxlength.
function readdict (const indictname:pchar;inlist:pdict_elem;
                   maxlength:integer):pdict_elem;
var
  i:integer;
  inf:pfile;               
begin
  (* Открыть входной файл *)
  inf := fopen (indictname, 'r');
  if inf = nil then
  begin
    readdict:=nil;
    exit;
  end;
  (* Считать структуры dict_elem из файла *)
  for i:=0 to maxlength - 1 do
    if fread (@inlist[i], sizeof (dict_elem), 1, inf) < 1 then
      break;
  fclose (inf);
  (* Обозначить конец списка *)
  inlist[i].d_length := 0;
  (* Вернуть начало списка *)
  readdict:=inlist;
end;
const
  delem1:array [0..1] of dict_elem=(
    (d_name:('d','n','a','m','e', #0,#0,#0,#0,#0,#0,#0,#0,#0,#0);
     d_start:2; d_length:15; d_type:3),
    (d_name:(#0, #0, #0, #0, #0, #0, #0, #0, #0, #0, #0, #0, #0, #0, #0)


     d_start:0; d_length:0; d_type:0)
  );
var
  delem2:array [0..1] of dict_elem;
begin
  printf ('delem1: d_name=%s, d_start=%d, d_length=%d, d_type=%d'#$a,
         [pchar(delem1[0].d_name), delem1[0].d_start, delem1[0].d_length,
         delem1[0].d_type]);
  writedict ('dictionary', @delem1[0]);
  if readdict ('dictionary', @delem2[0], 2)<>nil then
    printf ('delem2: d_name=%s, d_start=%d, d_length=%d, d_type=%d'#$a,
         [pchar(delem2[0].d_name), delem2[0].d_start, delem2[0].d_length,
         delem2[0].d_type]);
end.
И снова обратите внимание на приведение типа и использование конструкции sizeof.
Необходимо сделать важную оговорку. Бинарные данные, записываемые в файл при помощи процедуры fwrite, отражают внутреннее представление данных в системной памяти. Так как это представление зависит от архитектуры компьютера и различается порядком байтов в слове и выравниванием слов, то данные, записанные на одном компьютере, могут не читаться на другом, если не предпринять специальные усилия для того, чтобы они были записаны в машинно-независимом формате. По тем же причинам почти всегда бессмысленно выводить значения адресов и указателей.
И последний момент: можно было бы получить практически тот же результат, напрямую используя вызовы fdread или fdwrite, например:
fdwrite(fd, ptr, sizeof(dict_elem));
Основное преимущество версии, основанной на стандартной библиотеке ввода/вывода, снова заключается в ее лучшей эффективности. Данные при этом будут читаться и записываться большими блоками, независимо от размера структуры dict_elem.
Упражнение 11.6. Представленные версии процедур writedict и readdict работают с файлами словаря, которые могут содержать только один тип записей. Измените их так, чтобы в одном файле можно было хранить информацию о нескольких типах записей. Другими словами, нужно, чтобы файл словаря мог содержать несколько независимых именованных списков структур dict_elem. (Совет: включите в начало файла «заголовок»; содержащий информацию о числе записей и типе полей.)

Процедура fseek аналогична низкоуровневой функции


use stdio;
function fseek(_stream:pfile; offset:longint; direction:integer):longint;
procedure rewind(_stream:pfile);
function ftell(_stream:pfile):longint;
Процедура fseek аналогична низкоуровневой функции lseek, она устанавливает указатель файла, связанный с потоком _stream, изменяя позицию следующей операции ввода или вывода. Параметр direction определяет начальную точку, от которой отсчитывается новое положение указателя. Если значение этого параметра равно SEEK_SET (обычно 0), то отсчет идет от начала файла; если оно равно SEEK_CUR (обычно 1), то отсчет идет от текущего положения; для значения SEEK_END (обычно 2) отсчет ведется от конца файла.
Процедура rewind(stream) равносильна оператору:
fseek(stream, 0, SEEK_SET);
Другими словами, она устанавливает указатель чтения/записи на начало файла.
Процедура ftell сообщает текущее положение указателя в файле – число байтов от начала файла (началу файла соответствует нулевая позиция).


uses stdio;
function printf(fmt:pchar; args:array of const):integer;
function fprintf(outf:pfile; fmt:pchar; args:array of const):integer;
function sprintf(str:pchar; fmt:pchar; args:array of const):integer;
Каждая из этих процедур получает строку формата вывода fmt и переменное число аргументов произвольного типа (обозначенных как массив констант args), используемых для формирования выходной строки вывода. В выходную строку выводится информация из параметров args согласно формату; заданному аргументом fmt. В случае процедуры printf эта строка затем копируется в stdout. Процедура fprintf направляет выходную строку в файл outf. Процедура sprintf вывода не производит, а копирует строку в символьный массив, заданный указателем str. Процедура sprintf также автоматически добавляет в конец строки нулевой символ.
Строка формата fmt похожа на строки, задающие формат вывода языка Fortran. Она состоит из обычных символов, которые копируются без изменений, и набора спецификаций формата (conversion specifications). Это подстроки, которые начинаются с символа % (если нужно напечатать сам символ процента, то нужно записать два таких символа: %%).
Для каждого из аргументов args должна быть задана своя спецификация формата, которая указывает тип соответствующего аргумента и способ его преобразования в выходную последовательность символов ASCII.
Прежде чем обсудить общую форму этих спецификаций, рассмотрим пример, демонстрирующий использование формата процедуры printf в двух простых случаях. В первом из них нет других аргументов, кроме строки fmt. Во втором есть один параметр форматирования: целочисленная переменная iarg.
var
  iarg:integer=34;
.
.
.
printf('Hello, world!'#$a, []);
printf('Значение переменной iarg равно %d'#$a, [iarg]);
Так как в первом вызове нет аргументов, которые нужно было бы преобразовать, то в строке формата не заданы спецификации формата, а массив констант пуст. Этот оператор просто приводит к выводу сообщения
Hello, world!


на стандартный вывод, за которым следует символ перевода строки (символ #$a в строке интерпретируется в языке Паскаль как символ перевода строки). Второй оператор printf содержит еще один аргумент iarg и поэтому в строке формата есть спецификация %d. Это сообщает процедуре printf, что дополнительный аргумент является целым числом, которое должно быть выведено в десятичной форме (поэтому используется символ d). Вывод этого оператора будет выглядеть так:
Значение переменной iarg равно 34
Приведем возможные типы спецификаций (кодов) формата:
Целочисленные форматы
%d        Как уже было видно из примеров, это общеупотребительный код формата для значений типа integer. Если значение является отрицательным, то будет автоматически добавлен знак минуса
%u    Аргумент имеет тип word и будет выводиться в десятичной форме
%o    Аргумент имеет тип word и будет выводиться как восьмеричное число без знака
%х    Аргумент имеет тип word и будет выводиться как шестнадцатеричное число без знака. В качестве дополнительных шестнадцатеричных цифр будут использоваться символы a, b, c, d, e и f. Если задан код %Х, то будут использоваться символы А, В, С, D, Е
и F
%ld   Аргумент имеет тип longint со знаком и будет выводиться в десятичной форме. Можно также использовать спецификации %lo, %lu, %lх, %lX
Форматы вещественных чисел
%f        Аргумент имеет тип single или double и будет выводиться в стандартной десятичной форме
%е    Аргумент имеет тип single или double и будет выводиться в экспоненциальной форме, принятой в научных приложениях. Для обозначения экспоненты будет использоваться символ е. Если задана спецификация %Е, то будет использоваться символ Е
%g    Это объединение спецификаций %е
и %f. Аргумент имеет тип single или double. В зависимости от величины числа, оно будет выводиться либо в обычном формате, либо в формате экспоненциальной записи (как для спецификации %е). Если задана спецификация
%G, то экспонента будет обозначаться, как при задании спецификации %Е
Форматирование строк и символов


%с    Аргумент имеет тип char и будет выводиться без изменений, даже если он является «непечатаемым» символом. Численное значение символа можно вывести, используя код формата для целых чисел. Это может понадобится при невозможности отображения символа на терминале
%s        Соответствующий аргумент считается строкой (то есть указателем на массив символов). Содержимое строки передается дословно в выходной поток. Строка должна заканчиваться нулевым символом
Следующий пример, процедура warnuser, демонстрирует использование кодов %с
и %s. Она использует процедуру fprintf для вывода предупреждения на стандартный вывод – поток stdout. Если stdout соответствует терминалу, то процедура также пытается подать три звуковых сигнала, послав символ Ctrl+G (символ ASCII BEL, который имеет шестнадцатеричное значение $7). Эта процедура использует функцию isatty, определяющую, соответствует ли дескриптор файла терминалу, и процедуру fileno, возвращающую дескриптор файла, связанный с потоком. Функция isatty является стандартной функцией UNIX, представленной в главе 9, а процедура fileno является частью стандартной библиотеки ввода/вывода и описана в разделе 11.7.
(* Процедура warnuser — вывод сообщения и звукового сигнала *)
uses stdio, linux;
(* Этот код на большинстве терминалов вызывает *)
(* подачу звукового сигнала *)
const
  bel:char=$7;
procedure warnuser (const str:pchar);
begin
  (* Это терминал?? *)
  if isatty(fileno(stderr)) then
    fprintf(stdout, '%c%c%c', [bel, bel, bel]);
  fprintf(stdout, 'Предупреждение: %s'#$a, [string]);
end;

Все параметры массива args являются


uses stdio;
/* Все параметры массива args являются указателями.
 * Переменные, на которые они указывают, могут
 * иметь произвольный тип.
 */
function scanf(fmt:pchar; args:array of const):integer;
function fscanf(inf:pfile; fmt:pchar; args:array of const):integer;
function sscanf(str:pchar; fmt:pchar; args:array of const):integer;
Процедуры семейства scanf противоположны по смыслу процедурам семейства printf. Все они принимают ввод из файла (или из строки в случае процедуры sscanf), декодируют его в соответствии с информацией формата fmt и помещают полученные данные в переменные, заданные указателями в массиве args. Указатель файла перемещается на число обработанных символов.
Процедура scanf всегда выполняет чтение из stdin; процедура fscanf выполняет чтение из потока inf; а процедура sscanf выделяется в этом семействе процедур тем, что декодирует строку str и не осуществляет ввода данных. Поскольку последняя процедура работает со строкой в памяти, то она особенно полезна, если некоторую строку ввода нужно анализировать несколько раз.
Строка формата fmt имеет ту же структуру, что и строка формата процедуры printf. Например, следующий оператор считывает очередное целое число из потока стандартного ввода:
var
  inarg:integer;
scanf('%d', [@inarg]);
Важно, что функции scanf передается адрес переменной inarg. Это связано с тем, что, если нужно, чтобы процедура scanf изменяла переменную, которая находится в вызывающей процедуре, следует передать указатель, содержащий адрес этой переменной. Можно очень легко забыть про символ @, что приведет к ошибке записи в память. Новичкам также приходится бороться с искушением помещать знак @
перед всеми указателями, такими как имена символьных массивов.
В общем случае строка формата процедуры scanf может содержать:
–        пробельные символы, то есть пробелы, символы табуляции, перевода строки и страницы. Обычно они соответствуют любым пробельным символам с текущей позиции во входном потоке, до первого не пробельного символа;


–        обычные, не пробельные символы. Они должны точно совпадать с соответствующими символами во входном потоке;
–        спецификации формата. Как упоминалось ранее, они в основном аналогичны спецификациям, используемым в процедуре printf.
Следующий пример показывает использование процедуры scanf с несколькими переменными различных типов:
(* Демонстрационная программа для процедуры scanf *)
uses stdio;
var
  i1, i2:integer;
  fit:float;
  str1, str2:array [0..9] of char;
begin
  scanf('%2d %2d %f %s %s', [@i1, @i2, @flt, pchar(str1), pchar(str2)]);
  .
  .
  .
end.
Первые две спецификации в строке формата сообщают процедуре scanf, что она должна считать два целых числа (в десятичном формате). Так как в обоих случаях ширина поля равна двум символам, предполагается, что первое число должно находиться в двух считанных первыми символах, а второе – в двух следующих (в общем случае ширина поля обозначает максимальное число символов, которое может занимать значение). Спецификации %f
соответствует переменная типа single. Спецификация %s
означает, что ожидается строка, ограниченная пробельными символами. Поэтому, если подать на вход программы последовательность
11 12 34.07 keith ben
то в результате получится следующее:
переменная i1
будет иметь значение 11
переменная i2
будет иметь значение 12
переменная fit будет иметь значение 34.07
строка str1 будет содержать значение keith
строка str2 будет содержать значение ben
Обе строки будут заканчиваться нулевым символом. Обратите внимание, что переменные str1 и str2 должны иметь достаточно большую длину, чтобы в них поместились вводимые строки и нулевой символ в конце. Нельзя передавать процедуре scanf неинициализированный указатель.
Если задана спецификация формата %s, то предполагается, что строка должна быть ограничена пробельными символами. Для считывания строки целиком, включая пробельные символы, необходимо использовать код формата %с. Например, оператор


scanf ('%10c', [pchar(s1)]);
считает любые 10 символов из входного потока и поместит их в массив символов s1. Так как код формата с соответствует пробельным символам, то для получения следующего не пробельного символа должна использоваться спецификация %1s, например:
(* Считать 2 символа, начиная с первого не пробельного*)
scanf('%1s%1c',[@c1,@c2])
Другим способом задания формата строчных данных, не имеющим аналога в формате процедуры printf, является шаблон (scan set). Это последовательность символов, заключенных в квадратные скобки: [ и ]. Входное поле составляется их максимальной последовательности символов, которые попадают в шаблон (в этом случае пробельные символы не игнорируются и не попадают в поле, если они не являются частью шаблона). Например, оператор
scanf('%[ab12]%s',[str1,str2]);
при задании входной строки
2bbaa1other
поместит в строку str1 значение 2bbaa1, а в строку str2 – значение other.
Существует несколько соглашений, используемых при создании шаблона, которые должны быть знакомы пользователям grep или ed. Например, диапазон символов задается строкой вида a-z, то есть [a-d] равносильно [abcd]. Если символ тире (-) должен входить в шаблон, то он должен быть первым или последним символом. Аналогично, если в шаблон должна входить закрывающая квадратная скобка ], то она должна быть первым символом после открывающей квадратной скобки [. Если первым символом шаблона является знак ^, то при этом выбираются только символы, не
входящие в шаблон.
Для присваивания переменных типа longint или double после символа процента в спецификации формата должен находиться символ l. Это позволяет процедуре scanf определять размер параметра, с которым она работает. Следующий фрагмент программы показывает, как можно считать из входного потока переменные обоих типов:
var
  l:longint;
  d:double;
scanf('%ld %lf', [@l, @d]);
Другая ситуация часто возникает, если входной поток содержит больше данных, чем необходимо. Для обработки этого случая спецификация формата может содержать символ (*) сразу же после символа процента, обозначающий данное поле лишним. В результате поле ввода, соответствующее спецификации, будет проигнорировано. Вызов


scanf('%d %*s %*d %s', [@ivar, str]);
для строки ввода
131 cat 132 mat
приведет к присвоению переменной ivar значения 131, пропуску следующих двух полей, а затем присвоению строке str значения mat.
И, наконец, какое значение возвращают функции семейства scanf? Они обычно возвращают число преобразованных и присвоенных полей. Возвращаемое значение может быть равно нулю в случае несоответствия строки формата и вводимых данных. Если ввод прекращается до первого успешно (или неуспешно) введенного поля, то возвращается значение EOF.[19]
Упражнение 11.7. Напишите программу, выводящую в шестнадцатеричной и восьмеричной форме свои аргументы, которые должны быть десятичными целыми числами.
Упражнение 11.8. Напишите программу savematrix, которая должна сохранять в файле матрицу целых чисел произвольного размера в удобочитаемом формате, и программу readmatrix, которая загружает матрицу из файла. Используйте для этого только процедуры fprintf и fscanf. Постарайтесь свести к минимуму число пробельных символов (пробелы, символы табуляции и др.) в файле. Совет: используйте для задания формата записи в файл символ переменной ширины (*).

Функция runshell из файла stdio,


uses stdio;
function runshell(comstring:pchar):longint;
uses linux;
function shell(comstring:pchar):longint;
Функция runshell из файла stdio, как и shell из linux, выполняет команду, заданную строкой comstring. Вначале она создает дочерний процесс, который, в свою очередь, осуществляет вызов exec для запуска стандартного командного интерпретатора UNIX с командной строкой comstring. В это время процедура runshell в первом процессе выполняет вызов wait, гарантируя тем самым, что выполнение продолжится только после того, как запущенная команда завершится. Возвращаемое после этого значение retval содержит статус выхода командного интерпретатора, по которому можно определить, было ли выполнение программы успешным или нет. В случае неудачи любого из вызовов fork или exec значение переменной retval будет равно –1.
Поскольку в качестве посредника выступает командный интерпретатор, строка comstring может содержать любую команду, которую можно набрать на терминале. Это позволяет программисту воспользоваться такими преимуществами командного интерпретатора, как перенаправление ввода/вывода, поиск файлов в пути и т.д. Следующий оператор использует процедуру runshell для создания подкаталога при помощи программы mkdir:
retval := runshell('mkdir workdir');
if retval <> 0 then
  writeln(stderr, 'Процедура runshell вернула значение ', retval);
Остановимся на некоторых важных моментах. Во-первых, процедура runshell в вызывающем процессе будет игнорировать сигналы SIGINT и SIGQUIT. Это позволяет пользователю прерывать выполнение команды, не затрагивая родительский процесс. Во-вторых, команда, выполняемая процедурой runshell, будет наследовать из вызывающего процесса некоторые открытые дескрипторы файлов. В частности, стандартный ввод команды будет получен из того же источника, что и в родительском процессе. При вводе из файла могут возникнуть проблемы, если процедура
runshell используется для запуска интерактивной программы, так как ввод программы также будет производиться из файла.
Процедура runshell имеет один серьезный недостаток. Он не позволяет программе получать доступ к выводу запускаемой программы. Для этого можно использовать две другие процедуры из стандартной библиотеки ввода/вывода: pipeopen/popen и pipeclose/pclose.


uses stdio;
function pipeopen(comstring, _type:pchar):pfile;
function pipeclose(strm:pfile):integer;
Procedure POpen(Var F:FileType; comstring:pathstr; _type:char);
Function PClose(Var F:FileType):longint;
Как и процедура runshell, процедуры popen и pipeopen создает дочерний процесс командного интерпретатора для запуска команды, заданной параметром comstring. Но, в отличие от процедуры runshell, она также создает канал между вызывающим процессом и командой. При этом pipeopen возвращает структуру TFILE, связанную с этим каналом, а popen – переменную файлового типа. Если значение параметра _type равно w, то программа может выполнять запись в стандартный ввод при помощи структуры TFILE. Если же значение параметра _type равно r, то программа сможет выполнять чтение из стандартного вывода программы. Таким образом, процедуры popen и pipeopen представляют простой и понятный метод взаимодействия с другой программой.
Для закрытия потока, открытого при помощи процедуры popen, должна всегда использоваться процедура pclose. Она будет ожидать завершения команды, после чего вернет статус ее завершения.
Пример использования POpen:
uses linux;
var f : text;
    i : longint;
   
begin
  writeln ('Creating a shell script to which echoes its arguments');
  writeln ('and input back to stdout');
  assign (f,'test21a');
  rewrite (f);
  writeln (f,'#!/bin/sh');
  writeln (f,'echo this is the child speaking.... ');
  writeln (f,'echo got arguments \*"$*"\*');
  writeln (f,'cat');
  writeln (f,'exit 2');
  writeln (f);
  close (f);
  chmod ('test21a',octal (755));
  popen (f,'./test21a arg1 arg2','W');
  if linuxerror<>0 then
     writeln ('error from POpen : Linuxerror : ', Linuxerror);
  for i:=1 to 10 do
    writeln (f,'This is written to the pipe, and should appear on stdout.');
  Flush(f);
  Writeln ('The script exited with status : ',PClose (f));
  writeln;
  writeln ('Press <return> to remove shell script.');


  readln;
  assign (f,'test21a');
  erase (f)
end.
Следующий пример, процедура getlist, использует процедуру popen и команду ls для вывода списка элементов каталога. Каждое имя файла затем помещается в двухмерный массив символов, адрес которого передается процедуре getlist в качестве параметра.
(* getlist - процедура для получения списка файлов в каталоге *)
uses stdio, strings;
const
  MAXLEN=255; (* Максимальная длина имени файла *)
  MAXCMD=100; (* Максимальная длина команды *)
  ERROR=-1;
  SUCCESS=0;
type
  sarray=array [0..MAXLEN] of char;
  darray=array [0..MAXCMD] of sarray;
 
function getlist(namepart:pchar; var dirnames:darray;
                 maxnames:integer):integer;
var
  cmd:array [0..MAXCMD] of char;
  in_line:array [0..MAXLEN+1] of char;
  i:integer;
  lsf:pfile;
begin
  (* Основная команда *)
  strcopy(cmd, 'ls ');
  (* Дополнительные параметры команды *)
  if namepart <> nil then
    strlcat(cmd, namepart, MAXCMD - strlen(cmd));
  lsf := pipeopen(cmd, 'r'); (* Запускаем команду *)
  if lsf = nil then
  begin
    getlist:=ERROR;
    exit;
  end;
  for i:=0 to maxnames-1 do
  begin
    if fgets(in_line, MAXLEN+2, lsf) = nil then
      break;
    (* Удаляем символ перевода строки *)
    if in_line[strlen(in_line)-1] = #$a then
      in_line[strlen(in_line)-1] := #0;
    strcopy(dirnames[i], in_line);
  end;
  if i < maxnames then
    dirnames[i][0] := #0;
  pipeclose (lsf);
  getlist:=SUCCESS;
end;
var
  namebuf:darray;
  i:integer;
begin
  getlist('*.pas', namebuf, 100);
  i:=0;
  while namebuf[i][0]<>#0 do
  begin
    writeln(namebuf[i]);
    inc(i);
  end;
end.
Процедура getlist может быть вызвана следующим образом:
getlist('*.pas', namebuf, 100);
при этом в переменную namebuf будут помещены имена всех Паскаль-программ в текущем каталоге.
Следующий пример разрешает обычную проблему, с который часто сталкиваются администраторы UNIX: как быстро «освободить» терминал, который был заблокирован какой-либо программой, например, неотлаженной программой, работающей с экраном. Программа unfreeze принимает в качестве аргументов имz терминала и список программ. Затем она запускает команду вывода списка процессов ps при помощи процедуры popen для получения списка связанных с терминалом процессов и выполняет поиск указанных программ в этом списке процессов. Далее программа unfreeze запрашивает разрешение пользователя на завершение работы каждого из процессов, удовлетворяющих критерию.


Программа ps
сильно привязана к конкретной системе. Это связано с тем, что она напрямую обращается к ядру (через специальный файл, представляющий образ системной памяти) для получения системной таблицы процессов. На системе, использованной при разработке этого примера, команда ps
имеет синтаксис
$ ps -t ttyname
где ttyname является именем специального файла терминала в каталоге /dev, например, tty1, console, pts/8 и др. Выполнение этой команды ps
дает следующий вывод:
PID TTY TIME  COMMAND
29  со  0:04  sh
39  со  0:49  vi
42  со  0:00  sh
43  со  0:01  ps
Первый столбец содержит идентификатор процесса. Второй – имя терминала, в данном случае со
соответствует консоли. В третьем столбце выводится суммарное время выполнения процесса. В последнем, четвертом, столбце выводится имя выполняемой программы. Обратите внимание на первую строку, которая является заголовком. В программе unfreeze, текст которой приведен ниже, нам потребуется ее пропустить.
(* Программа unfreeze - освобождение терминала *)
uses stdio,linux,strings;
const
  LINESZ =150;
  SUCCESS=0;
  ERROR  =(-1);
const
  killflag:integer=0;
  (* Инициализация этой переменной зависит от вашей системы *)
  pspart:pchar = 'ps t ';
  fmt:pchar = '%d %*s %*s %*s %s';
var
  comline, inbuf, header, name:array [0..LINESZ-1] of char;
  f:pfile; 
  j:integer;
  pid:longint;
begin
  if paramcount <2 then
  begin
    writeln (stderr, 'синтаксис: ',paramstr(0),' терминал программа ...');
    halt (1);
  end;
  (* Сборка командной строки *)
  strcopy (comline, pspart);
  strcat (comline, argv[1]);
  (* Запуск команды ps *)
  f := pipeopen (comline, 'r');
  if f = nil then
  begin
    writeln (stderr, paramstr(0),': не могу запустить команду ps ');
    halt (2);
  end;
  (* Получить первую строку от ps и игнорировать ее *)
  if fgets (header, LINESZ, f) = nil then
  begin
    writeln (stderr, paramstr(0),': нет вывода от ps?');
    halt (3);


  end;
  (* Поиск программы, которую нужно завершить *)
  while fgets (inbuf, LINESZ, f) <> nil do
  begin
    if sscanf (inbuf, fmt, [@pid, pchar(name)]) < 2 then
      break;
    for j := 2 to argc-1 do
    begin
      if strcomp (name, argv[j]) = 0 then
      begin
        if dokill (pid, inbuf, header) = SUCCESS then
          inc(killflag);
      end;
    end;
  end;
  (* Это предупреждение, а не ошибка *)
  if killflag=0 then
    writeln(stderr, paramstr(0),': работа программы не завершена ',
            paramstr(1));
  pipeclose(f);
  halt (0);
end.
Ниже приведена реализация процедуры dokill, вызываемой программой unfreeze. Обратите внимание на использование процедуры readln для чтений первого не пробельного символа (вместо нее можно было бы использовать и функцию
yesno, представленную в разделе 11.8).
(* Получить подтверждение, затем завершить работу программы *)
function dokill(procid:longint;line,hd:pchar):integer;
var
  c:char;
begin
  writeln (#$a'Найден процесс, выполняющий заданную программу :');
  writeln (#9,hd,#9,line);
  writeln ('Нажмите `y` для завершения процесса ', procid);
  write (#$a'Yes\No? > ');
  (* Введите следующий не пробельный символ *)
  readln (c);
  if (c = 'y') or (c = 'Y') then
  begin
    kill (procid, SIGKILL);
    dokill:=SUCCESS;
    exit;
  end;
  dokill:=ERROR;
end;
Упражнение 11.9. Напишите свою версию процедуры getcwd, которая возвращает строку с именем текущего рабочего каталога. Назовите вашу программу wdir. Совет: используйте стандартную команду pwd.
Упражнение 11.10. Напишите программу arrived, которая запускает программу who при помощи процедуры popen для проверки (с 60-секундными интервалами), находится ли в системе пользователи из заданного списка. Список пользователей должен передаваться программе arrived в качестве аргумента командной строки. При обнаружении кого-нибудь из перечисленных в списке пользователей, программа arrived должна выводить сообщение. Программа должна быть достаточно эффективной, для этого используйте вызов sleep между выполнением проверок. Программа who должна быть описана в справочном руководстве системы.

а затем открывает его для


uses stdio;
function freopen(filename:pchar; _type:pchar; oldstream:pfile):pfile;
function fdopen(fildes:longint; _type:pchar):pfile;
Процедура freopen закрывает поток
oldstream, а затем открывает его для ввода из файла filename. Параметр _type определяет режим доступа к новой структуре TFILE и принимает те же значения, что и аналогичный аргумент процедуры fopen (строки r, w и др.). Процедура freopen обычно используется для перенаправления stdin, stdout или stderr, например:
if freopen('new.input', 'r', stdin) = nil then
  fatal('Невозможно перенаправить stdin');
Процедура fdopen связывает новую структуру TFILE с целочисленным дескриптором файла filedes, полученным при выполнении одного из системных вызовов fdcreat, fdopen, assignpipe или dup2.
В случае ошибки обе процедуры возвращают nil.


uses stdio;
procedure setbuf(stream:pfile; buf1:pchar);
function setvbuf(stream:pfile; buf1:pchar; _type:longint; size:longint):
         integer;
Эти процедуры позволяют программисту в некоторой степени управлять буферами потоков. Они должны использоваться после открытия файла, но до первых операций чтения или записи.
Процедура setbuf подставляет буфер buf1 вместо буфера, выделяемого стандартной библиотекой ввода/вывода. Размер сегмента памяти, на который указывает параметр buf1, должен быть равен константе BUFSIZ, определенной в файле stdio.
Процедура setvbuf позволяет осуществлять более тонкое управление буферизацией, чем процедура setbuf. Параметр buf2 задает адрес нового буфера, а параметр size – его размер. Если вместо адреса буфера передается значение nil, то используется буферизация по умолчанию. Параметр _type в процедуре setvbuf определяет метод буферизации потока stream. Он позволяет настроить поток для использования с конкретным типом устройства, например, для дисковых файлов или терминальных устройств. Возможны три значения _type, они определены в файле stdio:

_IOFBF
Поток файла буферизуется полностью. Этот режим включен по умолчанию для всех потоков ввода/вывода, не связанных с терминалом. Данные при этом будут записываться или считываться блоками размером BUFSIZ байтов для обеспечения максимальной эффективности
_IOLBF
Вывод буферизируется построчно, и буфер сбрасывается при записи символа перевода строки. Он также очищает буфер вывода при его заполнении или при поступлении запроса на ввод. Этот режим включен по умолчанию для терминалов и служит для поддержки интерактивного использования
_IOBNF
Ввод и вывод не буферизуются. В этом случае параметры buf2 и size игнорируются. Этот режим иногда необходим, например, для записи диагностических сообщений в файл протокола

Обратите внимание, что при задании недопустимого значения любого из параметров type или size процедура setvbuf возвращает ненулевое значение. В случае успеха возвращается 0.

В результате этого вызова функция


uses stdio;
function malloc(nbytes:longint):pointer;
В результате этого вызова функция malloc обычно возвращает указатель на участок памяти размером nbytes. При этом программа получает указатель на массив байтов, которые она может использовать по своему усмотрению. Если памяти недостаточно и система не может выделить запрошенный вызовом malloc объем памяти, то вызов возвращает нулевой указатель.
Обычно вызов malloc используется для выделения памяти под одну или несколько структур данных, например:
var
  p:^item;
  р := malloc(sizeof(item));
В случае успеха вызов malloc создает новую структуру item, на которую ссылается указатель р. Заметим, что возвращаемое вызовом malloc значение приводится к соответствующему типу указателя. Это помогает избежать вывода предупреждений компилятором или такими программами, как
lint. Приведение типа в данном случае целесообразно, поскольку вызов malloc реализован так, чтобы отводить память под объекты любого типа при условии, что запрашивается достаточный для хранения объекта ее объем. Такие задачи, как выравнивание по границе слова, решаются самим алгоритмом функции malloc. Обратите внимание на то, что размер структуры item задается при помощи конструкции sizeof, которая возвращает размер объекта в байтах.
Функция free противоположна по своему действию функции malloc и возвращает отведенную память, позволяя использовать ее повторно. Функции free передается указатель, который был получен во время вызова malloc:
var
  ptr:^item;
.
.
.
ptr := malloc(sizeof(item));
(* Выполнить какие-либо действия .. *)
free(ptr);
После подобного вызова функции free освобожденный участок памяти, на который указывает ptr, уже нельзя использовать, так как функция malloc может позднее снова его отдать процессу (целиком или частично). Очень важно, чтобы, функции free передавался указатель, который был получен от функции malloc, или от одной из функций calloc и realloc, принадлежащих тому же семейству. Если указатель не удовлетворяет этому требованию, то почти наверняка возникнут ошибки в механизме распределения динамической памяти, которые приведут к некорректной работе программы или к ее аварийному завершению. Неправильное применение функции free – очень часто встречающаяся и трудноуловимая ошибка.[20]
Еще две функции в семействе malloc непосредственно связаны с выделением памяти. Первой из них является функция calloc.


uses stdio;
function calloc(nelem, nbytes:longint):pointer;
Функция calloc отводит память под массив из nelem элементов, размер каждого из которых равен nbytes. Она обычно используется следующим образом:
(* Выделить память под массив структур *)
var
  aptr:^item;
.
.
.
aptr := calloc(nitem, sizeof(item));
В отличие от функции malloc, память, отводимая функцией calloc, заполняется нулями, что приводит к задержке при ее выполнении, но может быть полезно в тех случаях, когда такая инициализация необходима.
Последней функцией выделения памяти из семейства malloc является функция realloc.


uses stdio;
function realloc(oldptr:pointer; newsize:longint):pointer;
Функция realloc используется для изменения размера блока памяти, на который указывает параметр oldptr и который был получен ранее в результате вызова malloc, calloc или realloc. При этом блок памяти может переместиться, и возвращаемый указатель задает новое положение его начала. После изменения размера блока сохраняется содержимое его части, соответствующей меньшему из старого и нового размеров.

Для инициализации массивов данных можно


uses stdio;
function memset(buf:pointer; character:longint; size:longint):pointer;
function memcpy(buf1:pointer; const buf2:pointer; size:longint):pointer;
function memmove(buf1:pointer; const buf2:pointer; size:longint):pointer;
function memcmp(const buf1, buf2:pointer; size:longint):longint;
function memchr(const buf:pointer; character:longint; size:longint):
         pointer;
Для инициализации массивов данных можно использовать процедуру memset, записывающую значения character в первые size байтов массива памяти buf.
Для прямого копирования одного участка памяти в другой можно использовать любую из процедур memcpy или memmove. Обе эти функции перемещают size байт памяти, начиная с адреса buf1 в участок, начинающийся с адреса buf2. Разница между этими двумя функциями состоит в том, что функция memmove гарантирует, что если области источника и адресата копирования buf1 и buf2 перекрываются, то при перемещении данные не будут искажены. Для этого функция memmove вначале копирует сегмент buf2 во временный массив, а затем копирует данные из временного массива в сегмент buf1 (или использует более разумный алгоритм).
Функция memcmp работает аналогично функции strcmp. Если первые size байтов buf1 и buf2 совпадают, то функция memcmp вернет значение 0.
Функция memchr проверяет первые size байтов buf и возвращает либо адрес первого вхождения символа character, либо значение
nil. Процедуры memset, memcpy и memmove в случае успеха возвращают значение первого параметра.

Файл должен быть заранее открыт


uses linux;
type
  tmmapargs=record
    address : longint;
    size    : longint;
    prot    : longint;
    flags   : longint;
    fd      : longint;
    offset  : longint;
  end;
Function MMap(const m:tmmapargs):longint;
Файл должен быть заранее открыт при помощи системного вызова fdopen. Полученный дескриптор файла используется в качестве поля fd
в структуре tmmapargs.
Поле address позволяет программисту задать начало отображаемого файла в адресном пространстве процесса. Как и для вызова shmat, рассмотренного в разделе 8.3.4, в этом случае программе нужно знать расположение данных и кода процесса в памяти. Из соображений безопасности и переносимости лучше, конечно, позволить выбрать начальный адрес системе, и это можно сделать, присвоив параметру address значение 0. При этом возвращаемое вызовом mmap значение является адресом начала отображения. В случае ошибки вызов mmap вернет значение
-1.
Поле offset определяет смещение в файле, с которого начинается отображение данных. Обычно нужно отображать в память весь файл, поэтому поле offset часто равно 0, что соответствует началу файла. Если поле offset не равно нулю, то оно должно быть кратно размеру страницы памяти.
Число отображаемых в память байтов файла задается полем size. Если это поле не кратно размеру страницы памяти, то size байтов будут взяты из файла, а оставшаяся часть страницы будет заполнена нулями.
Поле prot определяет, можно ли выполнять чтение, запись или выполнение содержимого адресного пространства отображения. Поле prot может быть комбинацией следующих значений, определенных в файле linux:
PROT_READ          Разрешено выполнять чтение данных из памяти
PROT_WRITE        Разрешено выполнять запись данных в память
PROT_EXEC          Разрешено выполнение кода, содержащегося в памяти
PROT_NONE          Доступ к памяти запрещен
Значение поля prot не должно противоречить режиму, в котором открыт файл.
Поле flags влияет на доступность изменений отображенных в память данных файла для просмотра другими процессами. Наиболее полезны следующие значения этого параметра:

MAP_SHARED
Все изменения в области памяти будут видны в других процессах, также отображающих файл в память, изменения также записываются в файл на диске
MAP_PRIVATE
Изменения в области памяти не видны в других процессах и не записываются в файл

После завершения процесса отображение файла автоматически отменяется. Чтобы отменить отображение файла в память до завершения программы, можно использовать системный вызов munmap.


uses linux;
function MUnMap(address:Pointer; length:Longint):Boolean;
Если был задан флаг MAP_SHARED, то в файл вносятся все оставшиеся изменения, при флаге MAP_PRIVATE все изменения отбрасываются.
Следует иметь в виду, что эта функция только отменяет отображение файла в память, но не закрывает файл. Файл требуется закрыть при помощи системного вызова fdсlose.
Следующий пример повторяет программу copyfile, последний вариант которой был рассмотрен в разделе 11.4. Эта программа открывает входной и выходной файлы и копирует один из них в другой. Для простоты опущена часть процедур обработки ошибок.
uses linux;
var
  i, input, output, filesize:longint;
  source, target:pchar;
  args:tmmapargs;
const 
  endchar:char=#0;
type
  oearray=array [0..0] of char;
  poearray=^oearray;
begin
  (* Проверка числа входных параметров *)
  if paramcount <> 2 then
  begin
    writeln(stderr, 'Синтаксис: copyfile источник цель');
    halt (1);
  end;
  (* Открыть входной и выходной файлы *)
  input := fdopen (paramstr(1), Open_RDONLY);
  if input = -1 then
  begin
    writeln(stderr, 'Ошибка при открытии файла ', paramstr(1));
    halt (1);
  end;
  output := fdopen (paramstr(2), Open_RDWR or Open_CREAT or Open_TRUNC, octal(0666));
  if output = -1 then
  begin
    fdclose (input);
    writeln(stderr, 'Ошибка при открытии файла ', paramstr(2));
    halt (2);
  end;
  (* Создать второй файл с тем же размером, что и первый. *)
  filesize := fdseek (input, 0, SEEK_END);
  fdseek (output, filesize - 1, SEEK_SET);
  fdwrite (output, endchar, 1);
  (* Отобразить в память входной и выходной файлы. *)
  args.fd:=input;
  args.flags:=MAP_SHARED;
  args.prot:=PROT_READ;
  args.size:=filesize;
  args.address:=0;
  args.offset:=0;
  source:=pchar(mmap(args));
  if longint(source) = -1 then
  begin
    writeln(stderr, 'Ошибка отображения файла 1 в память');
    halt (1);
  end;
  args.fd:=output;
  args.flags:=MAP_SHARED;
  args.prot:=PROT_WRITE;
  args.size:=filesize;
  args.address:=0;
  args.offset:=0;
  target:=pchar(mmap(args));
  if longint(target) = -1 then
  begin
    writeln(stderr, 'Ошибка отображения файла 2 в память');
    halt (1);
  end;
  (* Копирование *)
  for i:=0 to filesize-1 do
    poearray(target)^[i] := poearray(source)^[i];
  (* Отменить отображение обоих файлов *)
  munmap (source, filesize);
  munmap (target, filesize);
  (* Закрыть оба файла *)
  fdclose (input);
  fdclose (output);
  halt (0);
end.
Конечно, файлы были бы автоматически закрыты при завершении программы. Вызовы munmap включены для полноты изложения.

в виде большого числа секунд,


uses linux;
Function GetTimeOfDay:longint;
Человеку сложно представлять время в виде большого числа секунд, поэтому в ОС UNIX существует набор библиотечных процедур для перевода системного времени в более понятную форму. Наиболее общей является процедура сtime, которая преобразует вывод вызова GetTimeOfDay в строку из 26 символов, например, выполнение следующей программы
uses linux,stdio;
var
  tt:longint;
begin
  tt:=gettimeofday;
  write('Текущее время ', ctime (tt));
  halt(0);
end.
дает примерно такой вывод:
Текущее время Tue Mar 18 00:17:06 1998
С функцией ctime связан набор процедур, использующих структуры типа tm. Тип структуры tm определен в файле stdio и содержит следующие элементы:
tm=record
  tm_sec:longint;         (* Секунды *)
  tm_min:longint;         (* Минуты *)
  tm_hour:longint;        (* Часы от 0 до 24 *)
  tm_mday:longint;        (* Дни месяца от 1 до 31 *)
  tm_mon:longint;         (* Месяц от 0 до 11 *)
  tm_year:longint;        (* Год минус 1900 *)
  tm_wday:longint;        (* День недели Воскресенье = 0 *)
  tm_yday:longint;        (* День года 0-365 *)
  tm_isdst:longint;       (* Флаг летнего времени только для США *)
end;
ptm=^tm;
Назначение всех элементов очевидно. Некоторые из процедур, использующих эту структуру, описаны ниже.


uses stdio;
function localtime(var _timeval:longint):ptm;
function gmtime(var _timeval:longint):ptm;
function asctime(const tptr:ptm):pchar;
function mktime(tptr:ptm):longint;
function difftime(time1, time2:longint):double;cdecl;external 'c';
Процедуры localtime и gmtime конвертируют значение, полученное в результате вызова GetTimeOfDay, в структуру tm и возвращают локальное время и стандартное гринвичское время соответственно. Например, программа
(* Программа tm - демонстрация структуры tm *)
uses linux,stdio;
var
  t:longint;
  tp:ptm;
begin
  (* Получить системное время *)
  t:=gettimeofday;
  (* Получить структуру tm *)
  tp := localtime (t);
  printf ('Время %02d:%02d:%02d'#$a, [tp^.tm_hour, tp^.tm_min,
                                      tp^.tm_sec]);
  halt (0);
end.
выводит сообщение
Время 1:13:23
Процедура asctime преобразует структуру tm в строку в формате вывода процедуры ctime, а процедура
mktime преобразует структуру tm
в соответствующее ей системное время (число секунд). Процедура difftime возвращает разность между двумя значениями времени в секундах.
Другие функции для работы со временем:

EpochToLocal
Преобразует время от начала эпохи в локальные дату и время
GetDate
Возвращает системную дату
GetDateTime
Возвращает системные дату и время
GetEpochTime
То же, что и GetTimeOfDay, но с учетом временной зоны
GetLocalTimezone
Возвращает системную временную зону
GetTime
Возвращает системное время
GetTimezoneFile
Возвращает имя файла временной зоны
ReadTimezoneFile
Возвращает содержимое файла временной зоны

Пример использования GetEpochTime:
Uses linux;
begin
  Write ('Secs past the start of the Epoch (00:00 1/1/1970) : ');
  Writeln (GetEpochTime);
end.
Пример использования EpochToLocal:
Uses linux;
Var Year,month,day,hour,minute,seconds : Word;
begin
  EpochToLocal (GetEpochTime,Year,month,day,hour,minute,seconds);
  Writeln ('Current date : ',Day:2,'/',Month:2,'/',Year:4);
  Writeln ('Current time : ',Hour:2,':',minute:2,':',seconds:2);
end.
Упражнение 12.3. Напишите свою версию процедуры asctime.
Упражнение 12.4. Напишите функцию weekday, возвращающую 1 для рабочих дней и 0 – для выходных. Напишите обратную функцию weekend. Эти функции также должны предоставлять возможность задания выходных дней.
Упражнение 12.5. Напишите процедуры, возвращающие разность между двумя значениями, полученными при вызове gettimeofday в днях, месяцах, годах и секундах. Не забывайте про високосные годы!

Процедура strcat присоединяет строку s2


uses strings;
Function StrCat(s1, s2:PChar):PChar;
Function StrLCat(s1, S2:PChar; length:Longint):PChar;
Function StrComp(S1, S2:PChar):Longint;
Function StrLComp(S1,S2:PChar; length:Longint):Longint;
Function StrIComp(S1,S2:PChar):Longint;
Function StrLIComp(S1,S2:PChar; length:Longint):Longint;
Function StrCopy(s1, s2:PChar):PChar;
Function StrLCopy(s1, S2:PChar; length:Longint):PChar;
Function StrMove(s1, S2:PChar; MaxLen:Longint):PChar;
Function StrNew(s1:PChar):PChar;
Function StrLen(s1:PChar):Longint;
Function StrScan(s1: PChar; C:Char):PChar;
Function StrRScan(s1:PChar; C:Char):PChar;
Function StrPos(S1,S2:PChar):PChar;
uses stdio;
function strpbrk(const s1, s2:pchar):pchar;
function strspn(const s1, s2:pchar):longint;
function strcspn(const s1, s2:pchar):longint;
function strtok(s1:pchar; const s2:pchar):pchar; (* Первый вызов *)
function strtok(nil; const s2:pchar):pchar; (* Последующие вызовы *)
Процедура strcat присоединяет строку s2 к концу строки s1. Процедура strlcat делает то же самое, но добавляет при этом не более length символов. Обе процедуры возвращают указатель на строку s1. Пример использования процедуры strcat:
strcat(fileprefix, '.dat');
Если переменная fileprefix первоначально содержала строку file, то после выполнения процедуры она будет содержать строку file.dat. Следует отметить, что процедура strcat изменяет строку, на которую указывает ее первый аргумент. Таким же свойством обладают и процедуры strlcat, strcopy, strlcopy и strtok. Программист должен убедиться, что размер первого аргумента этих процедур достаточно велик, чтобы вместить результат выполнения соответствующей операции.
Процедура strcomp сравнивает две строки s1 и s2. Если возвращаемое значение положительно, то это означает, что строка s1
лексикографически «больше», чем строка s2, в соответствии с порядком расположения символов в наборе символов ASCII. Если оно отрицательно, то это означает, что строка s1 «меньше», чем строка s2. Если же возвращаемое значение равно нулю, то строки совпадают. Процедура strlcomp аналогична процедуре strcomp, но сравнивает только первые length символов. Процедуры stricomp и strlicomp выполняют те же проверки, но игнорируют регистр символов. Пример использования процедуры strcomp:


if strcmop (token, 'print') = 0 then
begin
  (* Обработать ключевое слово print *)
end;
Процедура strcopy подобна процедуре strcat. Она копирует содержимое строки s2
в строку s1. Процедура strlcopy копирует в точности length символов, отбрасывая ненужные символы (что означает, что строка s1
может не заканчиваться нулевым символом) или записывая нулевые символы вместо недостающих символов строки s2. Процедура strnew возвращает указатель на копию строки s1. Возвращаемый процедурой
strnew указатель может быть передан функции free, так как память выделяется при помощи функции malloc.
Процедура strlen просто возвращает длину строки s1. Другими словами, она возвращает число символов в строке s1
до нулевого символа, обозначающего ее конец.
Процедура strscan возвращает указатель на первое вхождение символа с
(который передается в параметре типа char) в строке s1 или nil, если символ в строке не обнаружен. Процедура strpos возвращает адрес первого вхождения подстроки s2
в строке s1 (или nil, если подстрока не найдена). Процедура strrscan точно так же ищет последнее вхождение символа с. В главе 4 процедура strrscan была использована для удаления пути из полного маршрутного имени файла:
(* Выделяем имя файла из полного маршрутного имени *)
filename := strscan(pathname, '/');
Процедура strpbrk возвращает указатель на первое вхождение в строке s1
любого символа из строки s2 или нулевой указатель, если таких вхождений нет.
Процедура strspn возвращает длину префикса строки s1, который состоит только из символов, содержащихся в строке s2. Процедура strcspn возвращает длину префикса строки s1, который не содержит ни одного символа из строки s2.
И, наконец, процедура strtok позволяет программе разбить строку s1
на лексические единицы (лексемы). В этом случае строка s2
содержит символы, которые могут разделять лексемы (например, пробелы, символы табуляции и перевода строки). Во время первого вызова, для которого первый аргумент равен s1, указатель на строку s1
запоминается, и возвращается указатель на первую лексему. Последующие вызовы, для которых первый аргумент задается равным nil, возвращают следующие лексемы из строки s1. Когда лексем в строке больше не останется, возвращается нулевой указатель.

в целое число. Если строка


uses sysutils;
(* Преобразование строки в целое число *)
Function StrToInt(const s:string):integer;
Function StrToIntDef(const S:string; Default:integer):integer;
function strtol(const str:pchar; endptr:ppchar; base:longint):longint;
function atoi(const str:pchar):longint;
function atol(const str:pchar):longint;
(* Преобразование строки в вещественное число *)
function strtod(const str:pchar; endptr:ppchar):double;
function atof(const str:pchar):double;
Функции StrToInt и StrToIntDef преобразуют строку в целое число. Если строка содержит нецифровые символы или имеет неверный формат, StrToInt генерирует исключение EConvertError, а StrToIntDef возвращает значение, определенное параметром Default.
Функции atoi, atol и atof преобразуют строку числовой константы в число формата longint и double соответственно. В настоящее время эти функции устарели и заменены функциями strtol и strtod.
Функции strtod и strtol намного более надежны. Обе функции удаляют все пробельные символы из начала строки str и все нераспознанные символы в конце строки (включая нулевой символ) и записывают указатель на полученную строку в переменную endptr, если значение аргумента endptr не равно нулю. Последний параметр функции strtol – base может иметь любое значение между 0 и 36, при этом строка конвертируется в целое число с основанием base.