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

         

Программа stest


(* Программа stest - простой сервер для очереди *)

uses ipc,linux,stdio,sysutils;

{$i q.inc}

function proc_obj (msg:pq_entry):integer;

begin

  writeln(#$a'Приоритет: ',msg^.mtype,' имя: ',msg^.mtext);

end;

var

  pid:longint;

begin

  pid := fork;

  case pid of

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

      serve;

    -1:                 (* сервер не существует *)

      warn ('Не удалось запустить сервер');

    else

      writeln('Серверный процесс с идентификатором ', pid);

  end;

  if pid <> -1 then

    halt (0)



  else

    halt (1);

end.

Ниже следует пример использования этих двух простых программ. Перед запуском программы stest в очередь вводятся четыре простых сообщения при помощи программы etest. Обратите внимание на порядок, в котором выводятся сообщения:

$ etest objname1 3

$ etest objname2 4

$ etest objname3 1

$ etest objname4 9

$ stest

Серверный процесс с идентификатором 2545

$

Приоритет 1 имя objname3

Приоритет 3 имя objname1

Приоритет 4 имя objname2

Приоритет 9 имя objname4

Упражнение 8.3. Измените процедуры enter и serve так, чтобы можно было посылать серверу управляющие сообщения. Зарезервируйте для таких сообщений единичный тип сообщения (как это повлияет на расстановку приоритетов?). Реализуйте следующие возможности:

1. Остановка сервера.

2. Стирание всех сообщений из очереди.

3. Стирание сообщений с заданным уровнем приоритета.



Программирование при помощи каналов FIFO


Программирование при помощи каналов FIFO, в основном, идентично программированию с использованием обычных каналов. Единственное существенное различие заключается в их инициализации. Вместо использования вызова assignpipe канал

FIFO создается при помощи вызова mkfifo. В старых версиях UNIX может потребоваться использование более общего вызова mknod.



Программирование в режиме пересылок UDP-дейтаграмм


Перепишем теперь пример, используя модель дейтаграмм. Основное отличие будет заключаться в том, что дейтаграммы (UDP-пакеты), передаваемые между клиентом и сервером, могут достигать точки назначения в произвольном порядке. К тому же, как уже упоминалось, протокол UDP не гарантирует доставку пакетов. При работе с UDP-сокетами процесс клиента должен также сначала создать сокет и связать с ним свой локальный адрес при помощи вызова bind. После этого процесс сможет использовать этот сокет для посылки и приема UDP-пакетов. Чтобы послать сообщение, процесс должен знать адрес назначения, который может быть как конкретным адресом, так и шаблоном, называемым «широковещательным адресом» и обозначающим сразу несколько компьютеров.



Программирование в режиме TCP-соединения


Для того чтобы продемонстрировать основные системные вызовы для работы с сокетами, рассмотрим пример, в котором клиент посылает серверу поток строчных символов через TCP-соединение. Сервер преобразует строчные символы в прописные и посылает их обратно клиенту. В следующих разделах этой главы приведем тот же самый пример, но использующий сокеты UDP-протокола.

Сначала составим план реализации серверного процесса:

(* Серверный процесс *)

(* Включает нужные заголовочные файлы *)

uses sockets,stdio,linux;

var

  sockfd:longint;

begin

  (* Установить абонентскую точку сокета *)

  sockfd := socket (AF_INET, SOCK_STREAM, 0);

  if sockfd = -1 then

  begin

    perror ('Ошибка вызова socket');

    halt (1);

  end;

(* 'Связывание' адреса сервера с сокетом

Ожидание подключения

Цикл

  установка соединения

  создание дочернего процесса для работы с соединением

  если это дочерний процесс,

  то нужно в цикле принимать данные от клиента и посылать ему ответы

*)

end.

План клиентского процесса выглядит следующим образом:

(* Клиентский процесс *)

(* Включает нужные заголовочные файлы *)

var

  sockfd:longint;

begin

  (* Создает сокет *)

  sockfd := socket (AF_INET, SOCK_STREAM, 0);

  if sockfd = -1 then

  begin

    perror ('Ошибка вызова socket');

    halt (1);

  end;

  (* Соединяет сокет с адресом серверного процесса *)

  (* В цикле посылает данные серверу и принимает от него ответы *)

end.

Далее будем постепенно превращать эти шаблоны в настоящие программы, начиная с реализации сервера.



Программные каналы


Упражнение 13.57. Напишите программу, определяющую, возвращает ли fstat количество байт в FIFO в качестве поля size структуры tstat.

uses linux;

var

  s:tstat;

 

begin

  if paramcount<>1 then

  begin

    writeln('Используйте: ',paramstr(0),' имя_файла');

    exit;

  end;

  if not fstat(paramstr(1),s) then

  begin

    writeln('Ошибка вызова stat для файла ',paramstr(1));

    exit;

  end;

  writeln('Размер файла ',paramstr(1),' равен ',s.size);

end.

 

Упражнение 13.58. Напишите программу для определения того, что возвращает функция select при проверке возможности записи в дескриптор канала, у которого закрыт второй конец.

uses linux;

var

  fds:fdset;

  fdin,fdout,ret:longint;

 

begin

  if not assignpipe(fdin,fdout) then

  begin

    writeln('Ошибка создания программного канала');

    exit;

  end;

  fd_zero(fds);

  fd_set(fdin,fds);

  fd_set(fdout,fds);

  fdwrite(fdout,'Некая достаточно длинная строка',31);

  fdclose(fdin);

  writeln('Вызов select');

  ret:=select(2,nil,@fds,nil,1000);

  writeln('select вернул ',ret);

end.

 

Упражнение 13.59. Используя канал между родителем (клиентом) и потомком (сервером), создайте программу, в которой клиент считывает имя файла из стандартного ввода и записывает в канал. Если файл существует, сервер считывает его и записывает в канал, в противном случае возвращает клиенту сообщение об ошибке.

uses linux,sysutils;

const

  BLOCKSIZE=1024;

var

  pid,(*идентификатор процесса*)

  filesize,(*размер файла*)

  fd,(*дескриптор файла*)

  kol,(*количество прочитанных байт*)

  i:longint;

  in1,out1,in2,out2:longint;(*дескрипторы программных каналов*)

  filename:string[80];

  st:tstat;(*для получения информации о размере файла*)

  buf:array[0..BLOCKSIZE-1]of char;(*буфер чтения-записи*)

begin

  (*попытка создания двух программных каналов*)

  if not assignpipe(in1,out1) then

  begin

    writeln('Ошибка создания первого программного канала');




    exit;

  end;

 

  if not assignpipe(in2,out2) then

  begin

    writeln('Ошибка создания второго программного канала');

    fdclose(in1); (*закрываем ранее созданный канал 1*)

    fdclose(out1);

    exit;

  end;

 

  pid:=fork; (*клонирование процесса*)

 

  if pid=-1 then (*ошибка клонирования*)

  begin

    writeln('Ошибка создания потомка');

    exit;

  end;

  if pid=0 (*сервер - потомок*) then (*ветка потомка*)

  begin

    fdclose(out1);(* закрываем для надежности ненужные каналы:

                    первый - для записи, второй - для чтения*)

    fdclose(in2);

    fdread(in1,filename,80); (*читаем из первого канала имя файла*)

    {}writeln('Сервер: получено имя файла - ',filename);

    if access(filename,f_ok or r_ok) then (*если файл существует и доступен для чтения*)

    begin

      fstat(filename,st);(*получем информацию о файле*)

      filesize:=st.size; (*узнаем размер файла*)

      {}writeln('Сервер: определен размер файла - ',filesize);

      fdwrite(out2,filesize,sizeof(filesize)); (*пишем во второй канал размер файла*)

      {}writeln('Сервер: размер файла записан в канал');

      fd:=fdopen(filename,Open_RDONLY);(*открываем файл для чтения*)

      i:=1;

      kol:=fdread(fd,buf,BLOCKSIZE); (*читаем блоками по BLOCKSIZE байт*)

      while kol>0 do

      begin

        fdwrite(out2,buf,kol); (*пишем во второй канал столько, сколько прочли из файла*)

        {}writeln('Сервер: записано в канал ',i*kol/BLOCKSIZE:1:1,' Kb');

        kol:=fdread(fd,buf,BLOCKSIZE);

      end;

      fdclose(fd);(*закрываем файл*)

      {}writeln('Сервер: файл записан в канал');

    end

    else (*если файл не существует или недоступен для чтения*)

    begin

      filesize:=-1; (*записываем в программный канал признак ошибки*)

      fdwrite(out2,filesize,sizeof(filesize));

    end;

    {}writeln('Сервер: работа завершена');

    halt(0); (*завершаем работу потомка*)

  end

  else     (*клиент - родитель*)  (*ветка родителя*)

  begin



    fdclose(in1); (* закрываем ненужные каналы: первый для чтения, второй - для записи*)

    fdclose(out2);

    write('Введите имя файла: '); (*запрос имени файла*)

    readln(filename);

    fdwrite(out1,filename,80); (*пишем имя в программный канал*)

    {}writeln('Клиент: имя файла записано в канал');

    fdread(in2,filesize,sizeof(filesize)); (*получем размер файла из канала*)

    {}writeln('Клиент: из канала получен размер файла - ',filesize);

    if in2=-1 then (*при ошибке*)

      writeln('Файл ',filename,' не существует или недоступен для чтения')

    else

    begin

      (*создаем файл*)

      {}writeln('Клиент: файл создан, идет прием');

      for i:=length(filename) downto 1 do

        if filename[i]='/' then

        begin

          delete(filename,1,i);

          break;

        end;

      fd:=fdopen(filename,Open_WRONLY or Open_CREAT or Open_TRUNC, octal(644));

      if fd=-1 then

      begin

        writeln('Ошибка создания файла ',filename);

        kill(pid,9);

        halt(1);

      end;

     

      for i:=1 to filesize do(*пока есть что читать из канала*)

      begin

        fdread(in2,buf,1);

        if i mod BLOCKSIZE=0 then

        {}writeln('Клиент: записано в файл ',i div BLOCKSIZE,' Kb');

        fdwrite(fd,buf,1);(*записываем в файл*)

      end;

      fdclose(fd);(*закрываем созданный файл*)

      {}writeln('Клиент: файл закрыт');

    end;

    waitpid(pid,nil,0); (*ожидаем завершения потомка*)

    {}writeln('Клиент: сервер завершен, конец работы');

  end;

end.

Упражнение 13.60. Используя popen, создайте канал между who и more.

uses linux;

var

  f1,f2:text;

  s:string;

begin

  popen(f1,'who','r');

  if linuxerror <> 0 then

  begin

    writeln('Ошибка открытия канала с who для чтения');

    exit;

  end;

  popen(f2,'more','w');

  if linuxerror <> 0 then

  begin

    writeln('Ошибка открытия канала с more для записи');

    exit;

  end;

  while not eof(f1) do

  begin

    readln(f1,s);

    writeln(f2,s);

  end;

  pclose(f1);

  pclose(f2);

end.


. Произвольный доступ к файлу: процедуры fseek, rewind и ftell


Стандартная библиотека ввода/вывода содержит процедуры для позиционирования fseek, rewind и ftell, которые позволяют программисту перемещать указатель файла, а также опрашивать его текущее положение. Они могут использоваться только для потоков, допускающих произвольный доступ (в число которых, например, не входят терминалы).



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


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

uses linux, stdio;

.

.

.

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

if linuxerror <> 0 then

begin

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

  begin

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

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

  end

  else

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

end;



Проверка и преобразование символов


В ОС UNIX существуют два полезных набора макросов и функций для работы с символами, которые определены в файле stdio. Первый набор, называемый семейством сtype, предназначен для проверки одиночных символов. Эти макросы-предикаты возвращают true (истинно), если условие выполняется, и false (ложно) – в противном случае. Например, макрос isalpha проверяет, является ли символ буквой, то есть, лежит ли он в диапазонах a-z или A-Z:

uses stdio;

var

  c:integer;

.

.

.

(* Макрос 'isalpha' из набора ctype *)

if isalpha (с) then

begin

  (* Обрабатываем букву *)

end

else

  warn('Символ не является буквой');

Обратите внимание на то, что аргумент с имеет тип integer. Ниже следует полный список макросов ctype:

isalpha(с)

Является ли с буквой?

isupper(с)

Является ли с прописной буквой?

islower(с)

Является ли с строчной буквой?

isdigit(с)

Является ли с цифрой (0–9)?

isxdigit(c)

Является ли с шестнадцатеричной цифрой?

isalnum(c)

Является ли с буквой или цифрой?

isspace(с)

Является ли с пробельным символом; то есть одним из символов: пробел, табуляция, возврат каретки, перевод строки, перевод страницы или вертикальная табуляция?

ispunct(c)

Является ли с знаком препинания?

isprint(с)

Является ли с печатаемым знаком? Для набора символов ASCII это означает любой символ в диапазоне от пробела (040) до тильды (~ или 0176)

isgraph(с)

Является ли с печатаемым знаком, но не пробелом?

iscntrl(с)

Является ли с управляющим символом? В качестве управляющего символа рассматривается символ удаления

ASCII и все символы со значением меньше 040

isascii(с)

Является ли с символом ASCII? Обратите внимание, что для любого целочисленного значения, кроме значения символа EOF, определенного в файле <stdio.h>, которое передается другим процедурам семейства ctype, это условие должно выполняться. (Включение символа EOF позволяет использовать макрокоманды из семейства ctype в процедурах типа getc)

<


Другой набор утилит для работы с символами предназначен для простых преобразований символов. Например, функция tolower переводит прописную букву в строчную.

uses stdio;

var

  newc, с:integer;

.

.

.

(* Перевод прописной буквы в строчную *)

(* Например, перевод буквы 'А' в 'а' *)

newc := tolower(с);

Если с

является прописной буквой, то она преобразуется в строчную, иначе возвращается исходный символ. Другие функции и макросы (которые могут быть объединены под заголовком conv в справочном руководстве системы) включают в себя:


toupper(с)


Функция, преобразующая букву с в прописную, если она является строчной


toascii(c)


Макрос, преобразующий целое число в символ ASCII за счет отбрасывания лишних битов


_toupper(с)


Быстрая версия toupper, выполненная в виде макроса и не выполняющая проверки того, является ли символ строчной буквой


_tolower(с)


Быстрая версия tolower, выполненная в виде макроса, аналогичная макросу _toupper


Псевдотерминалы


Еще одно применение модуля дисциплины линии связи заключается в поддержке работы так называемого псевдотерминала (pseudo terminal), применяемого для организации дистанционного доступа через сеть. Псевдотерминал предназначен для обеспечения соединения терминала одного компьютера с командным интерпретатором другого компьютера. Пример такого соединения приведен на рис. 9.4. На нем пользователь подключен к компьютеру А (клиенту), но использует командный интерпретатор на компьютере В (сервере). Стрелки на схеме показывают направление пересылки вводимых с клавиатуры символов. Здесь схема несколько упрощена за счет исключения стеков сетевых протоколов на клиентской и серверной системе.

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

uses stdio, linux;

var

  attr:termios;

.

.

.

(* Получает текущую дисциплину линии связи *)

tcgetattr(0, attr);

(* Возврат из вызова fdread разрешен только после считывания одного символа *)

attr.c_cc[VMIN] := 1;

attr.c_cc(VTIME] := 0;

attr.c_lflag := attr.c_lflag and not (ISIG or ECHO or ICANON);

(* Устанавливает новую дисциплину линии связи *)

tcsetattr(0, TCSAFLUSH, attr);

Рис. 9.4. Удаленный вход в систему в ОС UNIX

Теперь процесс клиента rlogin может передавать данные по сети в исходном виде.

Когда сервер (В) получает первоначальный запрос на вход в систему, он выполняет вызовы fork и ехес, порождая новый командный интерпретатор. С новым командным интерпретатором не связан управляющий терминал, поэтому создается псевдотерминал, который имитирует обычный драйвер устройства терминала. Псевдотерминал (pseudo tty) действует подобно двунаправленному каналу и просто позволяет двум процессам передавать данные. В рассматриваемом примере псевдотерминал связывает командный интерпретатор с соответствующим сетевым процессом. Псевдотерминал является парой устройств, которые называются ведущим и ведомым устройствами, или портами псевдотерминала. Сетевой процесс открывает ведущий порт устройства псевдотерминала, а затем выполняет запись и чтение. Процесс командного интерпретатора открывает ведомый порт, а затем работает с ним (через дисциплину линии связи). Данные, записываемые в ведущий порт, попадают на вход ведомого порта, и наоборот. В результате пользователь на клиентском компьютере (А) как бы напрямую обращается к командному интерпретатору, который на самом деле выполняется на сервере (В). Аналогично, по мере того как данные выводятся командным интерпретатором на сервере, они обрабатываются дисциплиной линии связи на сервере (которая работает в каноническом режиме) и затем передаются без изменений клиентскому терминалу, не подвергаясь модификации со стороны дисциплины линии связи клиента.




Хотя средства, при помощи которых выполняется инициализация псевдотерминалов, были улучшены в новых версиях ОС

UNIX и спецификации XSI, они все еще остаются довольно громоздкими. Система UNIX обеспечивает конечное число псевдотерминалов, и процесс командного интерпретатора должен открыть следующий доступный псевдотерминал. В системе SVR4 это выполняется и помощи открытия устройства /dev/ptmx, которое определяет и открывает первое неиспользуемое ведущее устройство псевдотерминала. С каждым ведущим устройством связано ведомое устройство. Для того, чтобы предотвратить открытие ведомого устройства другим процессом, открытие устройства /dev/ptmx также блокирует соответствующее ведомое устройство.

uses linux;

var

  mfd:longint;

.

.

.

  (* Открыть псевдотерминал -

   * получить дескриптор файла главного устройства *)

  masterfd := fdopen ('/dev/ptmx', Open_RDWR);

  if masterfd = -1 then

  begin

    perror('Ошибка при открытии главного устройства');

    halt(1);

  end;

Перед тем как открыть и «разблокировать» ведомое устройство, необходимо убедиться, что только один процесс с соответствующими правами доступа сможет выполнять чтение из устройства и запись в него. Функция grantpt изменяет режим доступа и идентификатор владельца ведомого устройства в соответствии с параметрами связанного с ним главного устройства. Функция unlockpt снимает флаг, блокирующий ведомое устройство (то есть делает его доступным). Далее нужно открыть ведомое устройство. Но его имя пока еще не известно. Функция ptsname возвращает имя ведомого устройства, связанного с заданным ведущим устройством, которое обычно имеет вид /dev/pts/pttyXX. Следующий фрагмент демонстрирует последовательность необходимых действий:

uses stdio, linux;

var

  mfd, sfd:longint;

  slavenm:pchar;

.

.

.

(* Открываем ведущее устройство, как и раньше *)

mfd := fdopen ('/dev/ptmx', Open_RDWR);

if mfd = -1 then

begin

  perror('Ошибка при открытии ведущего устройства');

  halt(1);

end;

(* Изменяем права доступа ведомого устройства *)



if grantpt (mfd) = -1 then

begin

  perror('Невозможно разрешить доступ к ведомому устройству');

  halt(1);

end;

(* Разблокируем ведомое устройство, связанное с mfd *)

if unlockpt(mfd) = -1 then

begin

  perror('Невозможно разблокировать ведомое устройство');

  halt(1);

end;

(* Получаем имя ведомого устройства и затем пытаемся открыть его *)

slavenm := ptsname (mfd);

if slavenm = nil then

begin

  perror('Невозможно получить имя ведомого устройства');

  halt(1);

end;

sfd := fdopen (slavenm, Open_RDWR);

if slavefd = -1 then

begin

  perror('Ошибка при открытии ведомого устройства');

  halt(1);

end;

Теперь, когда получен доступ к драйверу устройства псевдотерминала, нужно установить для него дисциплину линии связи. До сих пор дисциплина линии связи рассматривалась как единое целое, тогда как в действительности она состоит из набора внутренних модулей ядра, известных как модули STREAM.

Стандартная дисциплина линии связи псевдотерминала состоит из трех модулей: ldterm (модуль дисциплины линии связи терминала), ptem (модуль эмуляции псевдотерминала) и ведомой части псевдотерминала. Вместе они работают как настоящий терминал. Эта конфигурация показана на рис. 9.5.







ldterm









pterm



Дисциплина

линии связи







Ведомый порт

псевдотерминала







Рис. 9.5. Дисциплина линии связи в виде модулей STREAM для устройства псевдотерминала

Для создания дисциплины линии связи нужно «вставить» дополнительные модули STREAM в ведомое устройство. Это достигается при помощи многоцелевой функции ioctl, например:

/*

 * Заголовочный файл stdio содержит интерфейс STREAMS

 * и определяет макрокоманду I_PUSH, используемую в качестве

 * второго аргумента функции ioctl().

*/

uses stdio;

.

.

.

(* Открываем ведущее и ведомое устройства, как и раньше *)

(* Вставляем два модуля в ведомое устройство *)

ioctl(sfd, I_PUSH, 'ptem');

ioctl(sfd, I_PUSH, 'ldterm');

Обратимся теперь к главному примеру, программе tscript, которая использует псевдотерминал в пределах одного компьютера для перехвата вывода командного интерпретатора в процессе интерактивного сеанса, не влияя на ход этого сеанса. (Эта программа аналогична команде

UNIX script.) Данный пример можно расширить и для дистанционного входа через сеть.


Работа с очередью сообщений: примитивы msgsnd и msgrcv


После создания очереди сообщений для работы с ней могут использоваться два следующих примитива:



Работа с портами ввода вывода


ОС UNIX предоставляет набор низкоуровневых функций для чтения и записи в порты ввода-вывода:

IOperm

Устанавливает права для доступа к порту

ReadPort

Читает данные и порта

ReadPortB

Читает 1 байт из порта

ReadPortL

Читает 4 байта из порта

ReadPortW

Читает 2 байта из порта

WritePort

Пишет данные в порт

WritePortB

Пишет 1 байт в порт

WritePortL

Пишет 4 байта в порт

WritePortW

Пишет 4 байта в порт



Работа со строками и символами


Библиотеки UNIX предоставляют богатые возможности для работы со строчными или символьными данными. Они достаточно полезны и поэтому также заслуживают упоминания.



Расширения режима реального времени


В последнее время в стандарт POSIX были введены определенные расширения для режима реального времени. Как и потоки управления, это специализированная и сложная область, и часто ядро

UNIX обладает достаточными возможностями для решения большинства задач реального времени. Специфические требования для реализации работы в режиме реального времени включают в себя:

–        организацию очереди сигналов и дополнительные средства для работы с сигналами (см. sigwaitinfo, sigtimedwait, sigqueue);

–        управление приоритетом и политикой планирования процессов и потоков (см. процедуры, начинающиеся с sched_);

–        дополнительные средства для работы с таймерами, асинхронным и синхронным вводом/выводом;

–        альтернативы рассмотренным процедурам передачи сообщений, интерфейсам семафоров и разделяемой памяти (попробуйте найти процедуры, начинающиеся с mq_, sem_ и shm_).



Разделяемая память


Операции с разделяемой памятью позволяют двум и более процессам совместно использовать область физической памяти (общеизвестно, что обычно области данных любых двух программ совершенно отделены друг от друга). Чаще всего разделяемая память является наиболее производительным механизмом межпроцессного взаимодействия.

Для того, чтобы сегмент памяти мог использоваться совместно, он должен быть сначала создан при помощи системного вызова shmget. После создания сегмента разделяемой памяти процесс может подключаться к нему при помощи вызова shmat и затем использовать его для своих частных целей. Когда этот сегмент памяти больше не нужен, процесс может отключиться от него при помощи вызова shmdt.



Различия между двумя моделями


Обсудим различия между двумя реализациями рассматриваемого примера с точки зрения техники программирования.

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

С точки зрения клиента в модели TCP-соединений достаточно простого подключения к серверу. В модели UDP-сокетов клиент должен создать сокет и связать свой локальный адрес с этим сокетом.

И, наконец, для передачи данных обычно используются различные системные вызовы. Системные вызовы sendto и recvfrom могут использоваться в обеих моделях, но все же они обычно используются в UDP-модели, чтобы сервер мог получить информацию об отправителе и отправить обратно ответ.



Размер канала


Пока в примерах передавались только небольшие объемы данных. Важно заметить, что на практике размер буфера канала конечен. Другими словами, только определенное число байтов может находиться в канале, прежде чем следующий вызов fdwrite будет заблокирован. Минимальный размер, определенный стандартом POSIX, равен 512 байтам. В большинстве существующих систем это значение намного больше. При программировании важно знать максимальный размер канала для системы, так как он влияет на оба вызова fdwrite и fdread. Если вызов fdwrite выполняется для канала, в котором есть свободное место, то данные посылаются в канал, и немедленно происходит возврат из вызова. Если же в момент вызова fdwrite происходит переполнение канала, то выполнение процесса обычно приостанавливается до тех пор, пока место не освободится в результате выполнения другим процессом чтения из канала.

Приведенная в качестве примера следующая программа записывает в канал символ за символом до тех пор, пока не произойдет блокировка вызова fdwrite. Программа использует вызов alarm для предотвращения слишком долгого ожидания в случае, если вызов fdread никогда не произойдет. Необходимо обратить внимание на использование процедуры fpathconf, служащей для определения максимального числа байтов, которые могут быть записаны в канал за один прием.

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

uses linux,stdio;

var

  count:integer;

(* Вызывается при получении сигнала SIGALRM *)

procedure alrm_action(signo:integer);cdecl;

begin

  writeln ('Запись блокируется после вывода ',count,' символов');

  halt (0);

end;

const

  c:char='x';

var

  fdin,fdout,pipe_size:longint;

  act:sigactionrec;

  temp:sigset_t;

begin

  (* Задать обработчик сигнала *)

  act.handler.sh := @alrm_action;

  sigfillset (@temp);

  act.sa_mask:=temp.__val[0];

  (* Создать канал *)

  if not assignpipe (fdin,fdout) then

  begin

    perror ('Ошибка вызова pipe ');

    halt (1);

  end;

  (* Определить размер канала *)




  pipe_size := fpathconf (fdin, _PC_PIPE_BUF);

  writeln('Максимальный размер канала: ',pipe_size,' байт');

  (* Задать обработчик сигнала *)

  sigaction (SIGALRM, @act, nil);

  while true do

  begin

    (* Установить таймер *)

    alarm (20);

    (* Запись в канал *)

    fdwrite (fdout, c, 1);

    (* Сбросить таймер *)

    alarm (0);

    inc(count);

    if count mod 1024 = 0 then

      writeln (count, ' символов в канале');

  end;

end.

Вот результат работы программы на некоторой системе:

Максимальный размер канала: 32768 байт

1024 символов в канале

2048 символов в канале

3072 символов в канале

4096 символов в канале

5120 символов в канале

.

.

.

31744 символов в канале

32768 символов в канале

Запись блокируется после вывода 32768 символов

Обратите внимание, насколько реальный предел больше, чем заданный стандартом POSIX минимальный размер канала.

Ситуация становится более сложной, если процесс пытается записать за один вызов fdwrite больше данных, чем может вместить даже полностью пустой канал. В этом случае ядро вначале попытается записать в канал максимально возможный объем данных, а затем приостанавливает выполнение процесса до тех пор, пока не освободится место под оставшиеся данные. Это важный момент: обычно вызов fdwrite для канала выполняется неделимыми порциями (atomically), и данные передаются ядром за одну непрерываемую операцию. Если делается попытка записать в канал больше данных, чем он может вместить, то вызов fdwrite выполняется поэтапно. Если при этом несколько процессов выполняют запись в канал, то данные могут оказаться беспорядочно перепутанными.

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


Реализация каталогов


На самом деле каталоги UNIX – не более чем файлы. Во многих аспектах система обращается с ними точно так же, как и с обычными файлами. Они имеют владельца, группу, размер и связанные с ними права доступа. Многие из системных вызовов для работы с файлами, которые были рассмотрены в предыдущих главах, могут использоваться и для работы с каталогами, хотя так делать и не рекомендуется. Например, каталоги можно открывать на чтение при помощи системного вызова open, и возвращенный этим вызовом дескриптор файла может использоваться для последующих вызовов fdread, fdseek, fstat и fdclose.

Тем не менее между каталогами и обычными файлами существуют некоторые важные различия, налагаемые системой. Каталоги не могут быть созданы при помощи системных вызовов fdcreat или fdopen. Системный вызов fdopen также не будет работать с каталогом, если установлен любой из флагов Open_WRONLY или Open_RDWR (только для записи или чтение/запись). При этом вызов вернет ошибку и запишет в переменную linuxerror код ошибки Sys_EISDIR. Эти ограничения делают невозможным изменение каталога при помощи системного вызова fdwrite. Фактически из-за особой природы каталогов для работы с ними гораздо лучше использован выделенное семейство системных вызовов, которое будет далее изучено.

Структура каталогов состоит из набора элементов каталогов, по одному элементу для каждого содержащегося в них файла или подкаталога. Каждый элемент каталога состоит, по крайней мере, из одного положительного числа, номера индексного дескриптора (inode number), и символьного поля, содержащего имя файла. Когда имена файлов были длиной не более 14 символов, элементы каталога имели фиксированную длину и большинство систем UNIX использовали один и тот же метод их реализации (исключение составлял Berkeley UNIX). Тем не менее после введения длинных имен файлов элементы каталога стали иметь различную длину, и реализация каталогов стала зависеть от файловой системы. Поэтому при разработке программ не следует полагаться на формат каталога, и для того, чтобы сделать их действительно переносимыми, необходимо использовать для работы с каталогами системные вызовы из спецификации XSI.




Часть каталога, содержащая три файла, может выглядеть примерно так, как показано на рис. 4.2. (Информация, необходимая для управления свободным пространством в файле каталога, исключена.) Этот каталог содержит имена трех файлов fred, bookmark и abc, которые могут быть и подкаталогами. Номера индексных дескрипторов для этих файлов равны соответственно 120, 207 и 235. На рис. 4.2 представлена логическая структура каталога; в действительности же каталог представляет собой непрерывный поток байтов.


120


f


r


e


d


\0






207


b


o


o


k


m


a


r


k


\0


235


a


b


c


\0






Рис. 4.2. Часть каталога

Номер индексного дескриптора однозначно идентифицирует файл (на самом деле номера индексных дескрипторов уникальны только в пределах одной файловой системы, но подробнее об этом будет рассказано ниже). Номер индексного дескриптора используется операционной системой для поиска в дисковой структуре данных структуры индексного дескриптора (inode structure), содержащей всю информацию, необходимую для обращения файлом: его размер, идентификатор владельца и группы, права доступа, время последнего доступа, последнего изменения и адреса блоков на диске, в которых хранятся данные файла. Большая часть информации, получаемой системным вызовом fstat, описанными в предыдущей главе, фактически получается напрямую из структуры индексного дескриптора. Более подробно структура индексного дескриптора будет рассмотрена в разделе 4.5.

Важно понимать, что представление каталога является только логической картиной. Просмотр содержимого каталога при помощи команды cat может завершиться выводом «мусора» на экран терминала. Более удобно исследовать каталог при помощи команды восьмеричного вывода od

с параметром -с. Например, для вывода содержимого текущей директории следует набрать в командной строке:

$ od -с .

Символ «точка» (.) в этой команде является стандартным способом задания текущего рабочего каталога.


Сеансы и идентификатор сеанса


В свою очередь, каждая группа процессов принадлежит к сеансу. В действительности сеанс относится к связи процесса с управляющим терминалом (controlling terminal). Когда пользователи входят в систему, все процессы и группы процессов, которые они явно или неявно создают, будут принадлежать к сеансу, связанному с их текущим терминалом. Сеанс обычно представляет собой набор из одной группы процессов переднего плана, использующей терминал, и одной или более групп фоновых процессов. Сеанс обозначается при помощи идентификатора сеанса (session-id), который имеет тип longint.

Процесс может получить идентификатор сеанса при помощи вызова getsid:



Семафор как теоретическая конструкция


В информатике понятие семафор (semaphore) был впервые введено голландским теоретиком Э.В. Дейкстрой

(E.W. Dijkstra) для решения задач синхронизации процессов. Семафор sem может рассматриваться как целочисленная переменная, для которой определены следующие операции:

р(sem) или wait (sem)

if sem<>0 then

  уменьшить sem на единицу

else

  ждать, пока sem не станет ненулевым, затем вычесть единицу

v(sem) или signal(sem)

увеличить sem на единицу

if очередь ожидающих процессов не пуста then

  продолжить выполнение первого процесса в очереди ожидания

Обратите внимание, что обозначения р

и v происходят от голландских терминов для понятий ожидания (wait) и сигнализации (signal), причем последнее понятие не следует путать с обычными сигналами UNIX.

Действия проверки и установки в обеих операциях должны составлять одно атомарное действие, чтобы только один процесс мог изменять семафор sem

в каждый момент времени.

Формально удобство семафоров заключается в том, что утверждение

(начальное значение семафора

+ число операций v

- число завершившихся операций р) >= 0

всегда истинно. Это – инвариант семафора (semaphore invariant). Теоретикам нравятся такие инварианты, так как они делают возможным систематическое и строгое доказательство правильности программ.

Семафоры могут использоваться несколькими способами. Наиболее простой из них заключается в обеспечении взаимного исключения (mutual exclusion), когда только один процесс может выполнять определенный участок кода одновременно. Рассмотрим схему следующей программы:

p(sem);

  какие-либо действия

v(sem);

Предположим далее, что начальное значение семафора sem равно единице. Из инварианта семафора можно увидеть, что:

(число завершенных операций р -

  число завершенных операций v) <= начального значения семафора

или:

(число завершенных операций р -

  число завершенных операций v) <= 1

Другими словами, в каждый момент времени только один процесс может выполнять группу операторов, заключенных между определенными операциями р и v. Такая область программы часто называется критическим участком (critical section).

Реализация семафоров в ОС UNIX основана на этой теоретической идее, хотя в действительности предлагаемые средства являются более общими (и, возможно, чрезмерно сложными). Вначале рассмотрим процедуры semget и semctl.



Семейство процедур strings


Некоторые из этих хорошо известных процедур уже были показаны в предыдущих главах книги, например, процедуры strcat и strcopy. Ниже следует подробный список процедур этого семейства.



Семейство вызовов ехес


Если бы вызов fork был единственным доступным для программиста примитивом создания процессов, то система UNIX была бы довольно скучной, так как в ней можно было бы создавать копии только одной программы. К счастью, для смены исполняемой программы можно использовать функции семейства ехес. На рис. 5.2 показано дерево семейства функций ехес. Основное отличие между разными функциями в семействе состоит в способе передачи параметров. Как видно из рисунка, в конечном итоге все эти функции выполняют один системный вызов execve.



Сигнал разрыва соединения


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

Обычно программисту не нужно обрабатывать сигнал SIGHUP, так как он служит нужным целям. Тем не менее может потребоваться перехватывать его для выполнения некоторых операций по «наведению порядка» перед выходом из программы; вот как это можно сделать:

uses linux;

procedure hup_action(sig:integer);cdecl;forward;

var

  act:sigactionrec;

.

.

.

act.handler.sh:=@hup_action;

sigaction(SIGHUP, @act, nil);

Этот подход используется некоторыми редакторами, сохраняющими редактируемый файл и отсылающими пользователю сообщение перед выходом. Если сигнал SIGHUP полностью игнорируется (установкой значения act.handler.sh равного SIG_IGN) и терминал разрывает соединение, то следующие попытки чтения из терминала будут возвращать 0 для обозначения «конца файла».



Сигналы и системные вызовы


В большинстве случаев, если процессу посылается сигнал во время выполнения им системного вызова, то обработка сигнала откладывается до завершения вызова. Но некоторые системные вызовы ведут себя по-другому, и их выполнение можно прервать при помощи сигнала. Это относится к вызовам ввода/вывода (fdread, fdwrite, fdopen, и т.д.), вызовам wait или pause (который мы обсудим в свое время). Во всех случаях, если процесс перехватывает вызов, то прерванный системный вызов возвращает значение –1 и помещает в переменную linuxerror значение Sys_EINTR. Такие ситуации можно обрабатывать при помощи следующего кода:

if fdwrite(tfd, buf, size) < 0 then

begin

  if linuxerror = Sys_EINTR then

  begin

    warn('Вызов fdwrite прерван');

    .

    .

    .

  end;

end;

В этом случае, если программа хочет вернуться к системному вызову fdwrite, то она должна использовать цикл и оператор continue. Но процедура

sigactionrec позволяет автоматически повторять прерванный таким образом системный вызов. Это достигается установкой значения SA_RESTART в поле sa_flags структуры sigactionrec. Если установлен этот флаг, то системный вызов будет выполнен снова, и значение переменной linuxerror не будет установлено.

Важно отметить, что сигналы

UNIX обычно не могут накапливаться. Другими словами, в любой момент времени только один сигнал каждого типа может ожидать обработки данным процессом, хотя несколько сигналов разных типов могут ожидать обработки одновременно. Фактически то, что сигналы не могут накапливаться, означает, что они не могут использоваться в качестве полностью надежного метода межпроцессного взаимодействия, так как процесс не может быть уверен, что посланный им сигнал не будет «потерян».

Упражнение 6.1. Измените программу smallsh из предыдущей главы так, чтобы она обрабатывала клавиши прерывания и завершения как настоящий командный интерпретатор. Выполнение фоновых процессов не должно прерываться сигналами SIGINT и SIGQUIT. Некоторые командные интерпретаторы, (а именно C-shell и Korn shell) помещают фоновые процессы в другую группу процессов. В чем преимущества и недостатки этого подхода? (В последних версиях стандарта POSIX введено накопление сигналов, но в качестве необязательного расширения.)



Символьные ссылки


Существует два важных ограничения на использование вызова link. Обычный пользователь не может создать ссылку на каталог (в некоторых версиях UNIX и суперпользователь не имеет права этого делать), и невозможно создать ссылку между различными файловыми системами (file systems). Файловые системы являются основными составляющими всей файловой структуры UNIX и будут изучаться более подробно в главе 4.

Для преодоления этих ограничений спецификация XSI поддерживает понятие символьных ссылок (symbolic links). Символьная ссылка в действительности представляет собой файл, содержащий вместо данных путь к файлу, на который указывает ссылка. Можно сказать, что символьная ссылка является указателем на другой файл.

Для создания символьной ссылки используется системный вызов symlink:



Системные вызовы и библиотечные подпрограммы


В предисловии уже упомянули, что книга сконцентрирована на интерфейсе системных вызовов (system call interface). Тем не менее понятие системного вызова требует дополнительного определения.

Системные вызовы фактически дают разработчику программного обеспечения доступ к ядру (kernel). Ядро, с которым познакомились в предисловии, – это блок кода, который постоянно находится в памяти и занимается планированием системных процессов UNIX и управлением вводом/выводом. По существу ядро является частью UNIX, которое и делает ее операционной системой. Все управление, выделение ресурсов и контроль над пользовательскими процессами, а также весь доступ к файловой системе осуществляется через ядро.

Системные вызовы осуществляются так же, как и вызовы обычных подпрограмм и функций Паскаля. Например, можно считать данные из файла, используя библиотечную подпрограмму Паскаля read:

read(fileptr, inputbuf);

или при помощи низкоуровневого системного вызова fdread:

nread := fdread(filedes, inputbuf, BUFSIZE);

Основное различие между подпрограммой и системным вызовом состоит в том, что при вызове подпрограммы исполняемый код является частью объектного кода программы, даже если он был скомпонован из библиотеки; при системном вызове основная часть исполняемого кода в действительности является частью ядра, а не вызывающей программы. Другими словами, вызывающая программа напрямую вызывает средства, предоставляемые ядром. Переключение между ядром и пользовательским процессом обычно осуществляется при помощи механизма программных прерываний.

Неудивительно, что большинство системных вызовов выполняет операции либо над файлами, либо над процессами. Фактически системные вызовы составляют основные примитивы, связанные с обоими типами объектов.

При работе с файлами эти операции могут включать передачу данных в файл и из файла, произвольный поиск в файле или изменение связанных с файлом прав доступа.

Код программы

Адресное пространство пользователя (данные и программы пользователя)

Библиотечная процедура read

Пользовательский код для fdread

Адресное пространство ядра (системные ресурсы)

Код ядра для вызова fdread

<


Рис. 1.2. Связь между кодом программы, библиотечной подпрограммой и системным вызовом

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

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

Кроме интерфейса системных вызовов, системы UNIX также предоставляют обширную библиотеку стандартных процедур, одним из важных примеров которых является Стандартная библиотека ввода/вывода

(Standard I/O Library). Подпрограммы этой библиотеки обеспечивают средства преобразования форматов данных и автоматическую буферизацию, которые отсутствуют в системных вызовах доступа к файлам. Хотя процедуры стандартной библиотеки ввода/вывода гарантируют эффективность, они сами, в конечном счете, используют интерфейс системных вызовов. Их можно представить, как дополнительный уровень средств доступа к файлам, основанный на системных примитивах доступа к файлам, а не отдельную подсистему. Таким образом, любой процесс, взаимодействующий со своим окружением, каким бы незначительным не было это взаимодействие, должен использовать системные вызовы.

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

Упражнение 1.1. Объясните значение следующих терминов: ядро, системный вызов, подпрограмма Pascal, процесс, каталог, путь.


Системные вызовы и переменная linuxerror


Из вышеизложенного материала видно, что все описанные до сих пор системные вызовы файлового ввода/вывода могут завершиться неудачей. В этом случае возвращаемое значение всегда равно -1. Чтобы помочь программисту получить информацию о причине ошибки, система UNIX предоставляет глобальную целочисленную переменную, содержащую код ошибки. Значение кода ошибки связано с сообщением об ошибке, таким как no permission (нет доступа) или invalid argument

(недопустимый аргумент). Полный список кодов и описаний ошибок приведен в Приложении 1. Текущее значение кода ошибки соответствует типу последней ошибки, произошедшей во время системного вызова.

Переменная, содержащая код ошибки, имеет имя linuxerror (сокращение от linux error number – номер ошибки в Linux). Программист может использовать переменную linuxerror в программе на языке Паскаль, подключив модуль linux.

Следующая программа использует вызов fdopen, и в случае его неудачного завершения использует функцию writeln для вывода значения переменной linuxerror:

(* Программа err1.pas - открывает файл с обработкой ошибок *)

uses linux;

var

  fd:integer;

begin

  fd := fdopen ('nonesuch', Open_RDONLY);

  if fd=-1 then

    writeln(stderr, 'Ошибка ', linuxerror);

end.

Если, например, файл nonesuch не существует, то код соответствующей ошибки в стандартной реализации UNIX будет равен 2; Так же, как и остальные возможные значения переменной linuxerror, этот код является значением определенной в модуле linux константы, в данном случае – константы Sys_ENOENT, имя которой является сокращением от no such entry (нет такого файла или каталога). Эти константы можно непосредственно использовать в программе.

При использовании переменной linuxerror следует проявлять осторожность, так как при следующем системном вызове ее значение не сбрасывается. Поэтому наиболее безопасно использовать linuxerror сразу же после неудачного системного вызова.



Системные вызовы sigpending и sigsuspend


ОС UNIX также содержит дополняющие вызов SigProcMask системные вызовы SigPending и SigPending, которые определены следующим образом:



Системные вызовы ттар и munmap


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



Системный вызов fcntl


Системный вызов fcntl был введен для управления уже открытыми файлами. Это довольно странная конструкция, которая может выполнять разные функции.



Системный вызов fdclose


Системный вызов fdclose противоположен вызову fdopen. Он сообщает системе, что вызывающий его процесс завершил работу с файлом. Необходимость этого вызова определяется тем, что число файлов, которые могут быть одновременно открыты программой, ограничено.



Системный вызов fdcreat


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



Системный вызов fdopen


Для выполнения операций записи или чтения данных в существующем файле его следует открыть при помощи системного вызова fdopen. Ниже приведено описание этого вызова. Для ясности и согласованности с документацией системы все описания системных вызовов будут использовать структуру прототипов функций ANSI. В них также будут приводиться заголовочные модули, в которых декларируются прототипы и определяются все важные постоянные:



Системный вызов fdopen


Вызов open может использоваться для открытия дисциплины линии связи терминала так же, как и для открытия обычного дискового файла, например:

fd := fdopen('/dev/tty0a', Open_RDWR);

Однако при попытке открыть терминал возврата из вызова не произойдет до тех пор, пока не будет установлено соединение. Для терминалов с модемным управлением это означает, что возврат из вызова не произойдет до тех пор, пока не будут установлены сигналы управления модемом и не получен сигнал «детектирования несущей», что может потребовать значительного времени либо вообще не произойти.

Следующая процедура использует вызов alarm (представленный в главе 6) для задания интервала ожидания, если возврат из вызова fdopen не произойдет за заданное время:

(* Процедура ttyopen - вызов fdopen с интервалом ожидания *)

uses stdio,linux;

const

  TIMEOUT=10;

  timeout_flag:boolean=FALSE;

  termname:pchar='';

procedure settimeout(value:longint);cdecl;

begin

  writeln(stderr, 'Превышено время ожидания ', termname);

  timeout_flag := TRUE;

end;

function ttyopen(filename:pchar; flags:longint):longint;

var

  fd:longint;

  act, oact:sigactionrec;

  mask:sigset_t;

begin

  fd := -1;

  termname := filename;

  (* Установить флаг таймаута *)

  timeout_flag := FALSE;

  (* Установить обработчик сигнала SIGALRM *)

  act.handler.sh := @settimeout;

  sigfillset(@mask);

  act.sa_mask:=mask.__val[0];

  sigaction(SIGALRM, @act, @oact);

  alarm(TIMEOUT);

  fd := fdopen(filename, flags);

  (* Сброс установок *)

  alarm(0);

  sigaction(SIGALRM, @oact, @act);

  if timeout_flag then

    ttyopen:=-1

  else

    ttyopen:=0;

end;



Системный вызов fdread


Системный вызов fdread используется для копирования произвольного числа символов или байтов из файла в буфер. Буфер формально устанавливается как ссылка на бестиповую переменную; это означает, что он может содержать элементы любого типа. Хотя обычно буфер является массивом данных типа char, он также может быть массивом структур, определенных пользователем.

Заметим, что программисты на языке Паскаль часто любят использовать термины «символ» и «байт» как взаимозаменяемые. Байт является единицей памяти, необходимой для хранения символа, и на большинстве машин имеет длину восемь бит. Термин «символ» обычно описывает элемент из набора символов ASCII, который является комбинацией всего из семи бит. Поэтому обычно байт может содержать больше значений, чем число символов ASCII; такая ситуация возникает при работе с двоичными данными. Тип char языка С представляет более общее понятие байта, поэтому название данного типа является не совсем правильным.



Системный вызов fdread


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

fdread вернет значение 1, так как сам символ новой строки тоже доступен программе. Поэтому нулевое значение, как и обычно, может использоваться для определения конца файла (то есть ввода символа еof).

Использование вызова fdread для чтения из терминала в программе io уже рассматривалось в главе 2. Тем не менее эта тема нуждается в более подробном объяснении, поэтому рассмотрим следующий вызов:

nread := fdread(0, buffer, 256);

При извлечении стандартного ввода процесса из обычного файла вызов интерпретируется просто: если в файле более 256 символов, то вызов fdread вернет в точности 256 символов в массиве buffer. Чтение из терминала происходит в несколько этапов – чтобы продемонстрировать это, обратимся к рис. 9.3, который показывает взаимодействие между программой и пользователем в процессе вызова fdread.

Эта схема иллюстрирует возможную последовательность действий, инициированных чтением с терминала с помощью вызова read. Для каждого шага изображены два прямоугольника. Верхний из них показывает текущее состояние строки ввода на уровне драйвера терминала; нижний, обозначенный как буфер чтения, показывает данные, доступные для чтения процессом в настоящий момент. Необходимо подчеркнуть, что схема показывает только логику работы с точки зрения пользовательского процесса. Кроме того, обычная реализация драйвера терминала использует не один, а два буфера (очереди), что, впрочем, не намного усложняет представленную схему.

1

Строка ввода >

echo

(Вызов read)

Буфер чтения >

2

echo q <erase>

3

echo hello

4

echo hello <nl>

v (Данные перемещаются в буфер чтения)

5

(Возврат из вызова read)

echo hello <nl>

6

(Теперь буфер пуст)

<


Рис. 9.3. Этапы чтения из терминала в каноническом режиме

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

На шаге 2 пользователь напечатал q, затем передумал и набрал символ erase для удаления символа из строки ввода. Эта часть схемы подчеркивает, что редактирование строки ввода может выполняться, не затрагивая программу, выполняющую вызов read.

На шаге 3 строка ввода завершена, только еще не содержит символ перехода на новую строку. Часть схемы, обозначенная как шаг 4, показывает состояние в момент ввода символа перехода на новую строку, при этом драйвер терминала передает строку ввода, включая символ перевода строки, в буфер чтения. Это приводит к шагу 5, на котором вся строка ввода становится доступной для чтения. В процессе, выполнившем вызов read, происходит возврат из этого вызова, при этом число введенных символов nread равно 11. Шаг 6 показывает ситуацию сразу же после такого завершения вызова, при этом и строка ввода, и буфер чтения снова временно пусты.

Следующий пример подкрепляет эти рассуждения. Он основан на простой программе read_demo, которая имеет лишь одну особенность: она использует небольшой размер буфера для приема байтов из стандартного ввода.

(* Программа read_demo - вызов fdread для терминала *)

uses linux;

const

  SMALLSZ=10;

var

  nread:longint;

  smallbuf:array [0..SMALLSZ] of char;

begin

  nread := fdread (0, smallbuf, SMALLSZ);

  while nread > 0 do

  begin

    smallbuf[nread] := #0;

    writeln('nread: ',nread,' ', smallbuf);

    nread := fdread (0, smallbuf, SMALLSZ);

  end;

end.

Если подать на вход программы следующий терминальный ввод

1

1234

Это более длинная строка

<EOF>

то получится такой диалог:

1

nread: 2 1

1234

nread: 5 1234

Это более длинная строка

nread: 10 Это более

nread: 10 длинная с

nread: 6 трока

Обратите внимание, что для чтения самой длинной строки требуется несколько последовательных операций чтения. Заметим также, что значения счетчика nread включают также символ перехода на новую строку. Это не показано для простоты изложения.

Что происходит, если терминал не находится в каноническом режиме? В таком случае для полного управления процессом ввода программа должна устанавливать дополнительные параметры состояния терминала. Это осуществляется при помощи семейства системных вызовов, которые будут описаны позже.

Упражнение 9.1. Попробуйте запустить программу read_demo, перенаправив ее ввод на чтение файла.


Системный вызов fdwrite


Системный вызов fdwrite противоположен вызову fdread. Он копирует данные из буфера программы, рассматриваемого как массив, во внешний файл.



Системный вызов fdwrite


Этот вызов намного проще в отношении взаимодействия с терминалом. Единственный важный момент заключается в том, что вызов fdwrite будет блокироваться при переполнении очереди вывода терминала. Программа продолжит работу, только когда число символов в очереди станет меньше некоторого заданного порогового уровня.



Системный вызов fork


Основным примитивом для создания процессов является системный вызов fork. Он является механизмом, который превращает UNIX в многозадачную систему.



Системный вызов frename


Фактически задачу предыдущего примера можно выполнить гораздо легче, используя системный вызов frename, который был добавлен в систему UNIX сравнительно недавно. Системный вызов frename может использоваться для переименования как обычных файлов, так и каталогов.



Системный вызов msgctl


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



Системный вызов pause


ОС UNIX также содержит дополняющий вызов alarm системный вызов

pause, который определен следующим образом:



Системный вызов shmget


Сегменты разделяемой памяти создаются при помощи вызова shmget.



Системный вызов unlink


В разделе 2.1.13 мы представили системный вызов unlink в качестве простого способа удаления файла из системы. Например:

unlink('/tmp/scratch');

удалит файл /tmp/scratch.

Фактически системный вызов unlink просто удаляет указанную ссылку и уменьшает счетчик ссылок (link count) файла на единицу. Данные в файле будут безвозвратно потеряны только после того, как счетчик ссылок на него станет равным нулю, и он не будет открыт ни в одной программе. В этом случае занятые файлом блоки на диске добавляются к поддерживаемому системой списку свободных блоков. Хотя данные могут еще существовать физически в течение какого-то времени, восстановить их будет невозможно. Так как многие файлы иметь лишь одну ссылку – принятое имя файла, удаление файла является обычным результатом вызова unlink. И наоборот, если счетчик ссылок не уменьшится до нуля, то данные в файле останутся нетронутыми, и к ним можно будет обратиться при помощи других ссылок на файл.

Следующая короткая программа переименовывает файл, вначале создавая на него ссылку с новым именем и удаляя в случае успеха старую ссылку на файл. Это упрощенная версия стандартной команды UNIX mv:

(* Программа move - переименование файла *)

uses linux,stdio;

const

  usage = 'Применение: move файл1 файл2';

(*

 * Программа использует аргументы командной строки,

 * передаваемые обычным способом.

 *)

begin

  if (paramcount <> 2) then

  begin

    writeln(stderr, usage);

    halt(1);

  end;

  if not link(paramstr(1), paramstr(2)) then

  begin

    perror('Ошибка в вызове link');

    halt(1);

  end;

  if not unlink (argv[1]) then

  begin

    perror('Ошибка в вызове unlink');

    unlink(paramstr(2));

    halt(1);

  end;

  writeln('Успешное завершение');

  halt(0);

end.

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



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


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

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

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

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

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

my_lock.l_type := F_UNLCK;

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

if linuxerror <> 0 then

begin

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

  halt(1);

end;



Снова о системных вызовах link и unlink


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

Если в каталоге с рис. 4.2 создать ссылку на файл abc с именем xyz при помощи следующего вызова

link('abc', 'xyz');

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

120

f

r

e

d

\0

207

b

o

o

k

m

a

r

k

\0

235

a

b

c

\0

235

x

y

z

\0

Рис. 4.3. Пример каталога с новым файлом



Соглашения


В книге приняты следующие выделения:

–        моноширинным шрифтом набраны листинги, параметры командной строки, пути к файлам и значения переменных;

–        также моноширинным шрифтом набран вывод на терминал, при этом курсивом выделены символы, вводимые пользователем;

–        полужирным шрифтом отмечены названия элементов интерфейса, а также клавиши и их комбинации;

–        курсивом выделены слова и утверждения, на которые следует обратить особое внимание, а также точки входа указателя.



Совместное использование вызовов ехес и fork


Системные вызовы fork и ехес, объединенные вместе, представляют мощный инструмент для программиста. Благодаря ветвлению при использовании вызова ехес во вновь созданном дочернем процессе программа может выполнять другую программу в дочернем процессе, не стирая себя из памяти. Следующий пример показывает, как это можно сделать. В нем мы также представим простую процедуру обработки ошибок fatal и системный вызов wait. Системный вызов wait, описанный ниже, заставляет процесс ожидать завершения работы дочернего процесса.

 (* Программа runls3 - выполнить ls как субпроцесс *)

uses linux,stdio;

var

  pid:longint;

begin

  pid := fork;

  case pid of

    -1:

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

    0:

    begin

      (* Потомок вызывает exec *)

      execl('/bin/ls -l');

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

    end;

    else

    begin

      (* Родительский процесс вызывает wait для приостановки

       * работы до завершения дочернего процесса.

       *)

      wait(nil);

      writeln('Программа ls завершилась');

      halt(0);

    end;

  end;

end.

Процедура fatal использует функцию perror для вывода сообщения, а затем завершает работу процесса. Процедура fatal реализована следующим образом:

procedure fatal(s:pchar);

begin

  perror(s);

  halt(1);

end;

Снова графическое представление, в данном случае рис. 5.4, используется для наглядного объяснения работы программы. Рисунок разбит на три части: До вызова fork, После вызова fork и После вызова ехес.

В начальном состоянии, До вызова fork, существует единственный процесс А

и программный счетчик

PC направлен на оператор fork, показывая, что это следующий оператор, который должен быть выполнен.

После вызова fork существует два процесса, А

и В. Родительский процесс A выполняет системный вызов wait. Это приведет к приостановке выполнения процесса А до тех пор, пока процесс В не завершится. В это время процесс В использует вызов execl для запуска на выполнение команды ls.

pid:=fork;

< PC

A

До вызова fork

wait(nil);

< PC

execl('/bin/ls -l');

< PC

A

B

После вызова fork

После вызова exec

(* 1-ая строка ls*)

< PC

wait(nil);

< PC

A

B (теперь

выполняет команду ls)

Рис. 5.4. Совместное использование вызовов fork и ехес

Что происходит дальше, показано в части После вызова ехес на рис. 5.4. Процесс В

изменился и теперь выполняет программу ls. Программный счетчик процесса В установлен на первый оператор команды ls. Так как процесс А

ожидает завершения процесса В, то положение его программного счетчика PC не изменилось.

Теперь можно увидеть в общих чертах механизмы, используемые командным интерпретатором. Например, при обычном выполнении команды оболочка использует вызовы fork, ехес и wait приведенным выше образом. При фоновом исполнении команды вызов wait пропускается, и оба процесса – команда и оболочка – продолжают выполняться одновременно.



Создание файла при помощи вызова fdopen


Вызов fdopen может использоваться для создания файла, например:

filedes := fdopen('/tmp/newfile', Open_WRONLY or Open_CREAT, octal(0644));

Здесь объединены флаги Open_CREAT и Open_WRONLY, задающие создание файла /tmp/newfile при помощи вызова fdopen. Если /tmp/newfile не существует, то будет создан файл нулевой длины с таким именем и открыт только для записи.

В этом примере вводится третий параметр mode вызова fdopen, который нужен только при создании файла. Не углубляясь в детали, заметим, что параметр mode содержит число, определяющее права доступа (access permissions) к файлу, указывающие, кто из пользователей системы может осуществлять чтение, запись или выполнение файла. В вышеприведенном примере используется восьмеричное значение 0644. При этом пользователь, создавший файл, может выполнять чтение из файла и запись в него. Остальные пользователи будут иметь доступ только для чтения файла. В следующей главе показано, как вычисляется это значение. Для простоты оно будет использовано во всех примерах этой главы.

Следующая программа создает файл newfile в текущем каталоге:

uses linux;

const

  PERMS=0644;          (* права доступа при открытии с Open_CREAT *)

  filename='newfile';

var

  filedes:integer;

 

begin

  filedes := fdopen (filename, Open_RDWR or Open_CREAT, octal(PERMS));

  if filedes = -1 then

  begin

    writeln('Не могу создать ', filename);

    halt(1);                (* выход по ошибке *)

  end;

  writeln('Файл ', filename, ' успешно создан (открыт для записи), дескриптор равен ',filedes);

 

  (* Остальная программа *)

  halt(0);

end.

Что произойдет, если файл newfile уже существует? Если позволяют права доступа к нему, то он будет открыт на запись, как если бы флаг Open_CREAT не был задан. В этом случае параметр mode не будет иметь силы. С другой стороны, объединение флагов Open_CREAT и Open_EXCL (exclusive – исключительный) приведет к ошибке во время вызова fdcreat, если файл уже существует. Например, следующий вызов




fd := fdopen('lock', Open_WRONLY or Open_CREAT or Open_EXCL, octal(0644));

означает, что если файл lock не существует, его следует создать с правами доступа 0644. Если же он существует, то в переменную fd будет записано значение -1, свидетельствующее об ошибке. Имя файла lock (защелка) показывает, что он создается для обозначения исключительного доступа к некоторому ресурсу.

Еще один полезный флаг – флаг Open_TRUNC. При его использовании вместе с флагом Open_CREAT файл будет усечен до нулевого размера, если он существует, и права доступа к файлу позволяют это. Например:

fd := fdopen ('file', Open_WRONLY or Open_CREAT or Open_TRUNC, octal(0644));

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

Упражнение 2.2. Интересно, что флаг Open_TRUNC может использоваться и без флага Open_CREAT. Попытайтесь предугадать, что при этом получится, а затем проверьте это при помощи программы в случаях, когда файл существует и не существует.

Существует возможность установить размер файла не только в 0, но и в любое заданное количество байт. Это позволяет сделать функция fdTruncate.


Создание и удаление каталогов


Как уже упоминалось ранее, каталоги нельзя создать при помощи системных вызовов fdcreat или fdopen. Для выполнения этой задачи существует специальный системный вызов mkdir.



Создание сокета


При любых моделях связи клиент и сервер должны создать абонентские точки (transport end points,), или сокеты, которые являются дескрипторами, используемыми для установки связи между процессами в сети. Они создаются при помощи системного вызова

socket.



Специальные символы


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

и X, это приводит к выводу префикса 0, 0х и 0Х соответственно. Поэтому такой фрагмент программы

var

  arg:integer = $FF;

printf('В восьмеричной форме, %#o'#$a, [arg]);

приведет к выводу строки:

В восьмеричной форме, 0377

Для вывода вещественных чисел задание знака # приведет к выводу десятичной точки, даже если задано нулевое число знаков после запятой.

В спецификации может также содержаться знак плюса (+) для принудительного вывода символа +

даже для положительных чисел. (Этот символ имеет смысл только для вывода целых чисел со знаком или вещественных чисел.) Знак +

располагается в спецификации на особом месте, следуя сразу же после знака минус, который обозначает выравнивание влево, или после знака процента, если знак минуса отсутствует. Следующие строки кода

var

  farg:single=57.88;

printf('Значение farg равно <%-+10.2f>'#$a, [farg]);

приведут к выводу:

Значение farg равно <+57.88>

Обратите внимание на комбинацию символов минус и плюс. Можно также заменить символ +

пробелом. В этом случае процедура printf выведет на месте знака плюс пробел. Это позволяет правильно выравнивать таблицы, содержащие положительные и отрицательные числа.



Спецификация Х/Open


ОС UNIX имеет долгую историю, и существовало множество ее официальных и фактических стандартов, а также коммерческих и учебных вариантов. Тем не менее ядро системы UNIX, находящееся в центре внимания в данной книге, остается необычайно стабильным.

Мы положили в основу текста и примеров документы (все датированные 1994 годом) System Interface Definitions (Описания системного интерфейса) и System Interfaces and Headers (Системные интерфейсы и заголовки) из 4-й версии второго выпуска Х/Open, а также часть связанного с ними документа Networking Services (Сетевые сервисы). Для удобства мы будем применять сокращение XSI – от Х/Open System Interfaces (Системные интерфейсы Х/Open). При необходимости особенности различных реализации системы будут обсуждаться отдельно.

Здесь нужно сделать небольшое отступление. Консорциум Х/Open первоначально объединил производителей аппаратного обеспечения, серьезно заинтересованных в открытых операционных системах и платформе UNIX, но со временем число его членов возросло. Одна из главных задач консорциума состояла 3 в выработке практического стандарта UNIX, и руководство по обеспечению мобильности программ, называемое XPG, послужило базовым документом для проекта нескольких основных производителей (включая Sun, IBM, Hewlett Packard, Novell и Open Software Foundation), обычно называемого Spec 1170 Initiative (1170 – это число охватываемых этим документом вызовов, заголовков, команд и утилит). Целью этого проекта было создание единой унифицированной спецификации системных сервисов UNIX, включая системные вызовы, которые являются основой этого документа. В результате получился удобный набор спецификации, объединивший многие конфликтующие направления в стандартизации UNIX, главную часть которых составляют вышеупомянутые документы. Другие представленные разработки охватывали основные команды UNIX и обработку вывода на экран.

С точки зрения системного программирования, документы XSI формируют практическую базу, и множество примеров из книги будет выполняться на большинстве существующих платформ UNIX. Стандарт Х/Open объединяет ряд соответствующих и дополняющих друг друга стандартов с их практической реализацией. Он объединил в себе ANSI/ISO стандарт языка С, важный базовый стандарт POSIX (IEEE 1003.1-1990) и стандарт SVID, а также позаимствовал элементы спецификаций Open Software Foundation (Организации открытого программного обеспечения) и некоторые известные функции из системы Berkeley UNIX, оказавшей большое влияние на развитие UNIX систем в целом.




Конечно, стандартизация продолжилась и в дальнейшем. В 1996 г. в результате слияния Х/Open и OSF ( Open Software Foundation) образовалась Группа открытых стандартов (The Open Group). Последние разработки (на момент написания книги) из стандарта POSIX, с учетом опыта практической реализации, Группа открытых стандартов называет второй версией Single UNIX Specification (Единой спецификации UNIX, далее по тексту – SUSV2), которая, в свою очередь, содержит пятый выпуск System Interface Definitions, System Interfaces and Headers и Networking Services. Эти важные, хотя и специализированные, дополнения охватывают такие области, как потоки, расширения реального времени и динамическая компоновка.

В заключение заметим, что:



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

–        при необходимости создания программы, строго следующей стандартам, необходимо предусмотреть в ней установку (и проверку) соответствующих флагов, таких как _XOPEN_SOURCE или _POSIX_SOURCE.


Список кодов и сообщений об ошибках


Ниже приведен список сообщений об ошибках системных вызовов. Он основан на информации выпуска 4.2 стандарта X/Open System Interfaces Standard (стандарта системных интерфейсов X/Open). Каждый пункт списка озаглавлен мнемоническим сокращением имени ошибки, определенным соответствующим кодом ошибки в файле linuxerror, и содержит системное сообщение об ошибке из таблицы sys_errlist, а также краткое ее описание. Обратите внимание, что текст сообщения об ошибке может меняться в зависимости от установки параметра LC_MESSAGES текущей локализации

(locale).

Sys_E2BIG

Слишком длинный список аргумента (Argument list too long). Чаще всего означает, что вызову ехес передается слишком длинный (согласно полному числу байтов) список аргументов

Sys_EACCES

Нет доступа (Permission denied). Произошла ошибка, связанная с отсутствием прав доступа. Может происходить при выполнении системных вызовов fdopen, link, fdcreat и аналогичных им. Может также генерироваться вызовом ехес, если отсутствует доступ на выполнение

Sys_EADDRINUSE

Адрес уже используется

(Address in use). Означает, что запрошенный программой адрес уже используется

Sys_EADDRNOTAVAIL

Адрес недоступен (Address not available). Эта ошибка может возникать, если программа запрашивает адрес, который уже используется процессом

Sys_Sys_EAFNOSUPPORT

Семейство адресов не поддерживается (Address family not supported). При использовании интерфейса вызова сокетов было задано семейство адресов, которое не поддерживается системой

Sys_EAGAIN

Ресурс временно недоступен, следует повторить попытку позже (Resource temporarily unavailable, try again later). Обычно означает переполнение некоторой системной таблицы. Эта ошибка может генерироваться вызовом fork (если слишком много процессов) и вызовами межпроцессного взаимодействия (если слишком много объектов одного из типов межпроцессного взаимодействия)

Sys_EALREADY

Соединение устанавливается (Connection already in progress). Означает отказ при попытке установления соединения из-за того, что этот сокет уже находится в состоянии установления соединения

Sys_EBADF

Недопустимый дескриптор файла

(Bad file descriptor). Файловый дескриптор не соответствует открытому файлу или же установленный режим доступа (только для чтения или только для записи) не позволяет выполнить нужную операцию. Генерируется многими вызовами, например, fdread и fdwrite

Sys_EBADMSG

Недопустимое сообщение (Bad message). (Данная ошибка связана с архитектурой модулей STREAM.) Генерируется, если системный вызов получает сообщение потока, которое не может прочитать. Может, например, генерироваться, если вызов fdread был выполнен для чтения в обход модуля STREAM, и в результате было получено управляющее сообщение STREAM, а не сообщение с данными

Sys_EBUSY

Занято устройство или ресурс (Device or resource busy). Может генерироваться, например, если вызов rmdir пытается удалить каталог, который используется другим процессом

Sys_ECHILD

Нет дочерних процессов (No child processes). Был выполнен вызов wait или waitpid, но соответствующий дочерний процесс не существует

Sys_ECONNABORTED

Соединение разорвано (Connection aborted). Сетевое соединение было разорвано по неопределенной причине

Sys_ECONNREFUSED

Отказ в установке соединения (Connection refused). Очередь запросов переполнена или ни один процесс не принимает запросы на установку соединения

Sys_ECONNRESET

Сброс соединения (Connection reset). Соединение было закрыто другим процессом

Sys_EDEADLK

Предотвращен клинч (Resource deadlock would occur). В случае успешного выполнения вызова произошел бы клинч (то есть ситуация, когда два процесса ожидали бы действия друг от друга). Эта ошибка может возникать в результате вызовов fcntl и lockf

Sys_EDESTADDRREQ

Требуется адрес назначения (Destination request required). При выполнении операции с сокетом был опущен адрес назначения

Sys_EDOM

Ошибка диапазона (Domain error). Ошибка пакета математических процедур. Означает, что аргумент функции выходит за границы области определения этой функции. Может возникать во время выполнения функций семейств trig, exp, gamma и др.

Sys_EDQUOT

Зарезервирован (Reserved)

Sys_EEXIST

Файл уже существует

(File exists). Файл уже существует, и это препятствует успешному завершению вызова. Может возникать во время выполнения вызовов link, mkdir, mkfifo, shmget и fdopen

Sys_EFAULT

Недопустимый адрес (Bad address). Генерируется системой после ошибки защиты памяти. Обычно означает, что был задан некорректный адрес памяти. Не все системы могут отслеживать ошибки памяти и сообщать о них процессам

Sys_EFBIG

Слишком большой файл

(File too large). При попытке увеличить размер файла было превышено максимальное значение размера файла для процесса (установленное вызовом ulimit) или общесистемное максимальное значение размера файла

Sys_EHOSTUNREACH

Компьютер недоступен (Host is unreachable). Генерируется сетью, если компьютер выключен или недоступен для маршрутизатора

Sys_EIDRM

Идентификатор удален

(Identifier removed). Означает, что идентификатор межпроцессного взаимодействия, например, идентификатор разделяемой памяти, был удален из системы при помощи команды ipcrm

Sys_EILSEQ

Недопустимая последовательность байтов (Illegal byte sequence). Недопустимый символ (не все возможные «широкие» символы являются допустимыми). Эта ошибка может возникать во время вызова fprintf или fscanf

Sys_EINPROGRESS

Соединение в процессе установки (Connection in progress). Означает, что вызов запроса соединения принят и будете выполнен. Данный код выставляется при вызове connect с флагом Open_NONBLOCK

Sys_EINTR

Прерванный вызов функции (Interrupted function call). Возвращается при поступлении сигнала во время выполнения системного вызова. (Возникает только во время выполнения некоторых вызовов – обратитесь к документации системы)

Sys_EINVAL

Недопустимый аргумент (Invalid argument). Означает, что системному вызову был передан недопустимый параметр или список параметров. Может генерироваться вызовами fcntl, sigaction, некоторыми процедурами межпроцессного взаимодействия, а также математическими функциями

Sys_ЕIO

Ошибка ввода/вывода

(I/O error). Во время ввода/вывода произошла физическая ошибка

Sys_EISCONN

Сокет подключен (Socket is connected). Этот сокет уже соединен

Sys_EISDIR

Это каталог (Is a directory). Была выполнена попытка открыть каталог для записи. Эта ошибка генерируется вызовами fdopen, fdread или frename

Sys_ELOOP

Слишком много уровней символьных ссылок (Too many levels of symbolic links). Возвращается, если системе приходится обойти слишком, много символьных ссылок при попытке найти файл в каталоге. Эта ошибка может генерироваться любым системным вызовом, принимающим в качестве параметра имя файла

Sys_EMFILE

Слишком много открытых процессом файлов (Too many open files in a process). Происходит в момент открытия файла и означает, что процессом открыто максимально возможное число файлов, заданное постоянной OPEN_MAX в файле <limits.h>

Sys_EMLINK

Слишком много ссылок

(Too many links). Генерируется вызовом link, если с файлом связано максимально возможное число жестких ссылок, заданное постоянной LINK_MAX в файле <limits.h>

Sys_EMSGSIZE

Слишком большое сообщение

(Message too large). Генерируется в сети, если посланное сообщение слишком велико, чтобы поместиться во внутреннем буфере приемника

Sys_EMULTIHOP

Зарезервирован (Reserved)

Sys_ENAMETOOLONG

Слишком длинное имя файла (Filename too long). Может означать, что имя файла длиннее NAME_MAX или полное маршрутное имя файла превышает значение РАТН_МАХ. Выставляется любым системным вызовом, принимающим в качестве параметра имя файла или полное маршрутное имя

Sys_ENETDOWN

Сеть не работает (Network is down)

Sys_ENETUNREACH

Сеть недоступна (Network unreachable). Путь к указанной сети недоступен

Sys_ENFILE

Переполнение таблицы файлов

(File table overflow). Генерируется вызовами, которые возвращают дескриптор открытого файла (такими, как fdcreat, fdopen и pipe). Это означает, что внутренняя таблица ядра переполнена, и нельзя открыть новые дескрипторы файлов

Sys_ENOBUFS

Нет места в буфере (No buffer space is available). Относится к сокетам. Это сообщение об ошибке выводится, если при выполнении любого из вызовов, работающих с сокетами, система не способна нарастить буферы данных

Sys_ENODATA

Сообщение отсутствует (No message available). (Данная ошибка связана с архитектурой модулей STREAM.) Возвращается вызовом fdread, если в модуле STREAM нет сообщений

Sys_ENODEV

Устройство не существует

(No such device). Была сделана попытка выполнить недопустимый системный вызов для устройства (например, чтение для устройства, открытого только для записи)

Sys_ENOENT

Файл или каталог не существует (No such file or directory). Эта ошибка происходит, если файл, заданный полным маршрутным именем (например, при выполнении вызова fdopen), не существует или не существует один из каталогов в пути

Sys_ENOEXEC

Ошибка формата Exec

(Ехес format error). Формат запускаемой программы не является допустимым форматом исполняемой программы. Эта ошибка возникает во время вызова ехес

Sys_ENOLCK

Нет свободных блокировок (No locks available). Больше нет свободных блокировок, которые можно было бы установить при помощи вызова fcntl

Sys_ENOLINK

Зарезервирован (Reserved)

Sys_ENOMEM

Нет места в памяти (Not enough space). Ошибка нехватки памяти происходит, если процессу требуется больше памяти, чем может обеспечить система. Может генерироваться вызовами ехес, fork и процедурами brk и sbrk, которые используются библиотекой управления динамической памятью

Sys_ENOMSG

Нет сообщений нужного типа (No message of the desired type). Возвращается, если вызов msgrcv не может найти сообщение нужного типа в очереди сообщений

Sys_ENOPROTOOPT

Протокол недоступен (Protocol not available). Запрошенный протокол не поддерживается системным вызовом socket

Sys_ENOSPC

Исчерпано свободное место на устройстве (No space left on device). Устройство заполнено, и увеличение размера файла или создание элемента каталога невозможно. Может генерироваться вызовами write, fdcreat, fdopen, mknod или link

Sys_ENOSR

Нет ресурсов потоков (No streams resources). (Данная ошибка связана с архитектурой модулей STREAM.) Bpeменное состояние, о котором сообщается, если ресурсы памяти модуля STREAM не позволяют в данный момент передать сообщение

Sys_ENOSTR

Это не STREAM (not a STREAM). (Данная ошибка связана с архитектурой модулей STREAM) Возвращается, если функция работы с модулем STREAM, такая как операция «push» функции ioctl, вызывается для устройства, которое не является устройством, представленным модулями STREAM

Sys_ENOSYS

Функция не реализована (Function not implemented). Означает, что запрошен системный вызов, не реализованный в данной версии системы

Sys_ENOTCONN

Сокет не подключен (Socket not connected). Эта ошибка генерируется, если для неподключенного сокета выполняется вызов sendmsg или rcvmsg

Sys_ENOTDIR

Это не каталог (Not a directory). Возникает, если путь не представляет имя каталога. Может устанавливаться вызовами chdir, mkdir, link и многими другими

Sys_ENOTEMPTY

Каталог не пуст (Directory not empty). Возвращается, например, вызовом rmdir, если делается попытка удалить непустой каталог

Sys_ENOTSOCK

Это не сокет (Not a socket). Дескриптор файла, используемый в вызове для работы с сетью, например, вызове connect, не является дескриптором сокета

Sys_ENOTTY

Не символьное устройство (Not a character device). Был выполнен вызов ioctl для открытого файла, который не является символьным устройством

Sys_ENXIO

Устройство или адрес не существует (No such device or address). Происходит, если выполняется попытка получить доступ к несуществующему устройству или адресу устройства. Эта ошибка может возникать при доступе к отключенному устройству

Sys_EOPNOTSUPP

Операция не поддерживается сокетом (Operation not supported on a socket). Связанное с сокетом семейство адресов не поддерживает данной функции

Sys_EOVERFLOW

Значение не может уместиться в типе данных (Value too large to be stored in the data type)

Sys_EPERM

Запрещенная операция (Operation not permitted). Означает, что процесс пытался выполнить действие, разрешенное только владельцу файла или суперпользователю (root)

Sys_EPIPE

Разрыв связи в канале

(Broken pipe). Устанавливается вызовом fdwrite и означает, что была выполнена попытка осуществить запись в канал, который не открыт на чтение ни одним процессом. Обычно при этом процесс, выполняющий запись в канал, прерывается при помощи сигнала SIGPIPE. Код ошибки EPIPE устанавливается, только если сигнал SIGPIPE перехватывается, игнорируется или блокируется

Sys_EPROTO

Ошибка протокола

(Protocol error). Эта ошибка зависит от устройства и означает, что была получена ошибка протокола

Sys_EPROTONOSUPPORT

Протокол не поддерживается

(Protocol not supported). Возвращается системным вызовом socket, если семейство адресов не поддерживается системой

Sys_EPROTOTYPE

Тип сокета не поддерживается (Socket type not supported). Возвращается вызовом socket, если заданный тип протокола, такой как SOCK_DGRAM, не поддерживается системой

Sys_ERANGE

Результат слишком велик или слишком мал (Result too large or too small). Эта ошибка возникает при вызове математических функций и означает, что возвращаемое функцией значение не может быть представлено на процессоре компьютера

Sys_EROFS

Файловая система доступна только для чтения (Readonly file system). Была выполнена попытка осуществить вызов fdwrite или изменить элемент каталога для файловой системы, которая была смонтирована в режиме только для чтения

Sys_ESPIPE

Некорректное позиционирование

(Illegal seek). Для канала была предпринята попытка вызова fdseek

Sys_ESRCH

Процесс не существует

(No such process). Задан несуществующий процесс. Генерируется вызовом kill

Sys_ESTALE

Зарезервирован (Reserved)

Sys_ETIME

Таймаут вызова ioctl для модуля STREAM (ioctl timeout on a STREAM). (Данная ошибка связана с архитектурой модулей STREAM.) Показывает, что истекло время ожидания вызова ioctl для модуля ядра STREAM. Это может означать, что период ожидания нужно увеличить

Sys_ETIMEDOUT

Истекло время ожидания соединения (Connection timed out). Когда процесс пытается установить соединение с другой системой, то заданное время ожидания может истечь, если эта система не включена или перегружена запросами на установку соединения

Sys_ETXTBSY

Файл программного кода занят (Text file busy). Если ошибка генерируется вызовом ехес, то это означает, что была выполнена попытка запустить на выполнение исполняемый файл, открытый для записи. Если же она генерируется вызовом, возвращающим дескриптор файла, то была сделана попытка открыть на запись файл программы, которая в данный момент выполняется

Sys_EWOULDBLOCK

Операция привела бы к блокировке (Operation would block). Эта ошибка возвращается, если дескриптор ввода/вывода был открыт как не блокируемый и был выполнен запрос записи или чтения, который в обычных условиях был бы заблокирован. В соответствии со спецификацией XSI код ошибки EWOULDBLOCK должен иметь то же значение, что и SYS_EAGAIN

Sys_EXDEV

Попытка создания ссылки между устройствами (Cross-device link). Возникает, если выполняется попытка связать при помощи вызова link файлы в разных файловых системах



Стандартная библиотека ввода/вывода: взгляд в будущее


Системные вызовы доступа к файлам лежат в основе всего ввода и вывода программ UNIX. Тем не менее эти вызовы действительно примитивны и работают с данными только как с простыми последовательностями байтов, оставляя все остальное на усмотрение программиста. Соображения эффективности также ложатся на плечи разработчика.

Чтобы несколько упростить ситуацию, система UNIX предоставляет стандартную библиотеку ввода/вывода, которая содержит намного больше средств, чем уже описанные системные вызовы. Поскольку книга в основном посвящена интерфейсу системных вызовов с ядром, подробное рассмотрение стандартной библиотеки ввода/вывода отложено до главы 11. Тем не менее для сравнения стоит и в этой главе кратко описать возможности стандартного ввода/вывода.

Возможно, наиболее очевидное отличие между стандартным вводом/выводом и примитивами системных вызовов состоит в способе описания файлов. Вместо целочисленных дескрипторов файлов, процедуры стандартного ввода/вывода явно или неявно работают со структурой FILE. Следующий пример показывает, как открывается файл при помощи процедуры fopen:

uses stdio;

var

  stream:PFILE;

begin

  stream := fopen ('junk', 'r');

  if stream = nil then

  begin

    printf('Невозможно открыть файл junk'#10,[]);

    halt(1);

  end;

end.

Первая строка примера

uses stdio;

подключает модуль библиотеки ввода/вывода stdio, описанной в приложении. Этот файл, кроме всего прочего, содержит определение TFILE, PFILE и объявления для таких функций, как fopen.

Настоящее содержание этого примера заключается в операторе:

stream := fopen ('junk', 'r');

if stream = nil then

begin

.

.

end;

Здесь junk – это имя файла, а строка 'r' означает, что файл открывается только для чтения. Строка 'w' может использоваться для усечения файла до нулевой длины или создания файла и открытия его на запись. В случае успеха функция fopen проинициализирует структуру TFILE и вернет ее адрес в переменной stream. Указатель stream может быть передан другим процедурам из библиотеки. Важно понимать, что где-то внутри тела функции fopen осуществляется вызов нашего старого знакомого fdopen. И, естественно, где-то внутри структуры TFILE находится дескриптор файла, привязывающий структуру к файлу. Существенно то, что процедуры стандартного ввода/вывода написаны на основе примитивов системных вызовов. Основная функция библиотеки состоит в создании более удобного интерфейса и автоматической буферизации.

После открытия файла существует множество стандартных процедур ввода/ вывода для доступа к нему. Одна из них – процедура getc, считывающая одиночный символ, другая, putc, выводит один символ. Они используются следующим образом:



Стандартные процедуры опроса состояния


Для опроса состояния структуры TFILE существует ряд простых процедур. Они, например, позволяют программе определять, вернула ли процедура ввода, такая как getc, символ EOF из-за того, что достигнут конец файла или в результате возникновения ошибки. Эти процедуры описаны ниже: