Кэширование: вызовы sync и fsync
Из соображений эффективности в традиционной файловой системе копии суперблоков смонтированных систем находятся в оперативной памяти. Их обновление может выполняться очень быстро, без необходимости обращаться к диску. Аналогично все операции между памятью и диском обычно кэшируются в области данных оперативной системы вместо немедленной записи на диск. Операции чтения также буферизуются в кэше. Следовательно, в любой заданный момент времени данные на диске могут оказаться устаревшими по сравнению с данными кэша в оперативной памяти. В UNIX существуют две функции, которые позволяют процессу убедиться, что содержимое кэша совпадает с данными на диске. Системный вызов sync используется для сброса на диск всего буфера памяти, содержащего информацию о файловой системе, а вызов fsync используется для сброса на диск всех данных и атрибутов, связанных с определенным файлом.
Клинч
Предположим, что два процесса, РА и РВ, работают с одним файлом. Допустим, что процесс РА блокирует участок файла
SX, а процесс РВ – не пересекающийся с ним участок SY. Пусть далее процесс РА попытается заблокировать участок SY при помощи команды F_SETLKW, а процесс РВ попытается заблокировать участок SX, также используя команду
F_SETLKW. Ни одна из этих попыток не будет успешной, так как процесс РА приостановит работу, ожидая, когда процесс РВ освободит участок SY, а процесс РВ также будет приостановлен в ожидании освобождения участка SX процессом РА. Если не произойдет вмешательства извне, то будет казаться, что два процесса обречены вечно находиться в этом «смертельном объятии».
Такая ситуация называется клинчем (deadlock) по очевидным причинам. Однако UNIX иногда предотвращает возникновение клинча. Если выполнение запроса F_SETLK приведет к очевидному возникновению клинча, то вызов завершается неудачей, и возвращается значение -1, а переменная linuxerror принимает значение Sys_EDEADLK. К сожалению, вызов fcntl может определять только клинч между двумя процессами, в то время как можно создать трехсторонний клинч.[14]
Во избежание такой ситуации сложные приложения, использующие блокировки, должны всегда задавать предельное время ожидания.
Следующий пример поможет пояснить изложенное. В точке /*А*/ программа блокирует с 0 по 9 байты файла locktest. Затем программа порождает дочерний процесс, который в точках, помеченных как /*В*/ и /*С*/, блокирует байты с 10 по 14 и пытается выполнить блокировку байтов с 0 по 9. Из-за того, что родительский процесс уже выполнил последнюю блокировку, работа дочернего будет
приостановлена. В это время родительский процесс выполняет вызов sleep в течение 10 секунд. Предполагается, что этого времени достаточно, чтобы дочерний процесс выполнил два вызова, устанавливающие блокировку. После того, как родительский процесс продолжит работу, он пытается заблокировать байты с 10 по 14 в точке /*D*/, которые уже были заблокированы дочерним процессом. В этой точке возникнет опасность клинча, и вызов fcntl завершится неудачей.
(* Программа deadlock - демонстрация клинча *)
uses linux, stdio;
var
fd:longint;
first_lock, second_lock:flockrec;
begin
first_lock.l_type := F_WRLCK;
first_lock.l_whence := SEEK_SET;
first_lock.l_start := 0;
first_lock.l_len := 10;
second_lock.l_type := F_WRLCK;
second_lock.l_whence := SEEK_SET;
second_lock.l_start := 10;
second_lock.l_len := 5;
writeln(sizeof(flockrec));
fd := fdopen ('locktest', Open_RDWR);
fcntl (fd, F_SETLKW, longint(@first_lock));
if linuxerror>0 then (*A *)
fatal ('A');
writeln ('A: успешная блокировка (процесс ',getpid,')');
case fork of
-1:
(* ошибка *)
fatal ('Ошибка вызова fork');
0:
begin
(* дочерний процесс *)
fcntl (fd, F_SETLKW, longint(@second_lock));
if linuxerror>0 then (*B *)
fatal ('B');
writeln ('B: успешная блокировка (процесс ',getpid,')');
fcntl (fd, F_SETLKW, longint(@first_lock));
if linuxerror>0 then (*C *)
fatal ('C');
writeln ('C: успешная блокировка (процесс ',getpid,')');
halt (0);
end;
else
begin
(* родительский процесс *)
writeln ('Приостановка родительского процесса');
sleep (10);
fcntl (fd, F_SETLKW, longint(@second_lock));
if linuxerror>0 then (*D *)
fatal ('D');
writeln ('D: успешная блокировка (процесс ',getpid,')');
end;
end;
end.
Вот пример работы этой программы:
А: успешная блокировка (процесс 1410)
Приостановка родительского процесса
В: успешная блокировка (процесс 1411)
D: Deadlock situation detected/avoided
С: успешная блокировка (процесс 1411)
В данном случае попытка блокировки завершается неудачей в точке /*D*/, и процедура perror выводит соответствующее системное сообщение об ошибке. Обратите внимание, что после того, как родительский процесс завершит работу и его блокировки будут сняты, дочерний процесс сможет выполнить вторую блокировку.
Это пример использует процедуру fatal, которая была применена в предыдущих главах.
Упражнение 8.1. Напишите процедуры, выполняющие те же действия, что и вызовы fdread и fdwrite, но которые завершатся неудачей, если уже установлена блокировка нужного участка файла. Измените аналог вызова fdread так, чтобы он блокировал читаемый участок. Блокировка должна сниматься после завершения вызова fdread.
Упражнение 8.2. Придумайте и реализуйте условную схему блокировок нумерованных логических записей файла. (Совет: можно блокировать участки файла вблизи максимально возможного смещения файла, даже если там нет данных. Блокировки в этом участке файла могут иметь особое значение, например, каждый байт может соответствовать определенной логической записи. Блокировка в этой области может также использоваться для установки различных флагов.)
Ключи средств межпроцессного взаимодействия
Программный интерфейс всех трех средств IPC System V однороден, что отражает схожесть их реализации в ядре. Наиболее важным из общих свойств является ключ (key). Ключи – это числа, обозначающие объект межпроцессного взаимодействия в системе UNIX примерно так же, как имя файла обозначает файл. Другими словами, ключ позволяет ресурсу межпроцессного взаимодействия совместно использоваться несколькими процессами. Обозначаемый ключом объект может быть очередью сообщения, набором семафоров или сегментом разделяемой памяти. Ключ имеет тип TKey, состав которого зависит от реализации и определяется в файле ipc.
Ключи не являются именами файлов и несут меньший смысл. Они должны выбираться осторожно во избежание конфликта между различными программами, в этом помогает применение дополнительной опции – «версии проекта». (Одна известная система управления базами данных использовала для ключа шестнадцатеричное значение типа 0xDB; плохое решение, так как такое же значение мог выбрать и другой разработчик.) В ОС UNIX существует простая библиотечная функция ftok, которая образует ключ по указанному файлу.
Командный интерпретатор smallsh
В этом разделе создается простой командный интерпретатор smallsh. Этот пример имеет два достоинства. Первое состоит в том, что он развивает понятия, введенные в этой главе. Второе – в том, что подтверждается отсутствие в стандартных командах и утилитах UNIX чего-то особенного. В частности, пример показывает, что оболочка является обычной программой, которая запускается при входе в систему.
Наши требования к программе smallsh просты: она должна транслировать и выполнять команды – на переднем плане и в фоновом режиме – а также обрабатывать строки, состоящие из нескольких команд, разделенных точкой с запятой. Другие средства, такие как перенаправление ввода/вывода и раскрытие имен файлов, могут быть добавлены позднее.
Основная логика понятна:
while не встретится EOF do
begin
получить строку команд от пользователя
оттранслировать аргументы и выполнить
ожидать возврата из дочернего процесса
end;
Дадим имя userin функции, выполняющей «получение командной строки от пользователя». Эта функция должна выводить приглашение, а затем ожидать ввода строки с клавиатуры и помещать введенные символы в буфер программы. Функция userin реализована следующим образом:
uses stdio,linux;
(* Заголовочный файл для примера *)
{$i smallsh.inc}
(* Буферы программы и рабочие указатели *)
var
inpbuf:array [0..MAXBUF-1] of char;
tokbuf:array [0..2*MAXBUF-1] of char;
const
ptr:pchar=@inpbuf[0];
tok:pchar=@tokbuf[0];
(* Вывести приглашение и считать строку *)
function userin(p:pchar):integer;
var
c, count:integer;
begin
(* Инициализация для следующих процедур *)
ptr := inpbuf;
tok := tokbuf;
(* Вывести приглашение *)
write(p);
count := 0;
while true do
begin
c := getchar;
if c = EOF then
begin
userin:=EOF;
exit;
end;
if count < MAXBUF then
begin
inpbuf[count] := char(c);
inc(count);
end;
if (c = $a) and (count < MAXBUF) then
begin
inpbuf[count] := #0;
userin:=count;
exit;
end;
(* Если строка слишком длинная, начать снова *)
if c = $a then
begin
writeln ('smallsh: слишком длинная входная строка');
count := 0;
write (p);
end;
end;
end;
Некоторые детали инициализации можно пока не рассматривать. Главное, что функция userin вначале выводит приглашение ввода команды (передаваемое в качестве параметра), а затем считывает ввод пользователя по одному символу до тех пор, пока не встретится символ перевода строки или конец файла (последний случай обозначается символом
EOF).
Функция getchar содержится в стандартной библиотеке ввода/вывода. Она считывает один символ из стандартного ввода программы, который обычно соответствует клавиатуре. Функция userin помещает каждый новый символ (если это возможно) в массив символов inpbuf. После своего завершения функция userin возвращает либо число считанных символов, либо EOF, обозначающий конец файла. Обратите внимание, что символы перевода строки не отбрасываются, а добавляются в массив inpbuf.
Заголовочный файл smallsh.inc, упоминаемый в функции userin, содержит определения для некоторых полезных постоянных (например,
MAXBUF). В действительности файл содержит следующее:
(* smallsh.inc - определения для интерпретатора smallsh *)
{ifndef SMALL_H}
{define SMALL_H}
const
EOL=1; (* конец строки *)
ARG=2; (* обычные аргументы *)
AMPERSAND=3; (* символ '&' *)
SEMICOLON=4; (* точка с запятой *)
MAXARG=512; (* макс. число аргументов *)
MAXBUF=512; (* макс. длина строки ввода *)
FOREGROUND=0; (* выполнение на переднем плане *)
BACKGROUND=1; (* фоновое выполнение *)
{endif} (* SMALL_H *)
Другие постоянные, не упомянутые в функции userin, встретятся в следующих процедурах.
Рассмотрим следующую процедуру, gettok. Она выделяет лексемы (tokens) из командной строки, созданной функцией userin. (Лексема является минимальной единицей языка, например, имя или аргумент команды.) Процедура gettok вызывается следующим образом:
toktype := gettok(@tptr);
Целочисленная переменная toktype будет содержать значение, обозначающее тип лексемы. Диапазон возможных значений берется из файла smallsh.inc и включает символы EOL (конец строки),
SEMICOLON и так далее. Переменная tptr является символьным указателем, который будет указывать на саму лексему после вызова gettok. Так как процедура gettok сама выделяет пространство под строки лексем, нужно передать адрес переменной tptr, а не ее значение.
Исходный код процедуры gettok приведен ниже. Обратите внимание, что поскольку она ссылается на символьные указатели tok и ptr, то должна быть включена в тот же исходный файл, что и userin. (Теперь должно быть понятно, зачем была нужна инициализация переменных tok и ptr в начале функции userin.)
(* Получить лексему и поместить ее в буфер tokbuf *)
function gettok (outptr:ppchar):integer;
var
_type:integer;
begin
(* Присвоить указателю на строку outptr значение tok *)
outptr^ := tok;
(* Удалить пробелы из буфера, содержащего лексемы *)
while (ptr^ = ' ') or (ptr^ = #9) do
inc(ptr);
(* Установить указатель на первую лексему в буфере *)
tok^ := ptr^;
inc(tok);
(* Установить значение переменной type в соответствии
* с типом лексемы в буфере *)
case ptr^ of
#$a:
begin
_type := EOL;
inc(ptr);
end;
'&':
begin
_type := AMPERSAND;
inc(ptr);
end;
';':
begin
_type := SEMICOLON;
inc(ptr);
end;
else
begin
_type := ARG;
inc(ptr);
(* Продолжить чтение обычных символов *)
while inarg (ptr^) do
begin
tok^ := ptr^;
inc(tok);
inc(ptr);
end;
end;
end;
tok^ := #0;
inc(tok);
gettok:=_type;
end;
Функция inarg используется для определения того, может ли символ быть частью «обычного» аргумента. Пока можно просто проверять, является ли символ особым для командного интерпретатора команд smallsh или нет:
const
special:array [0..5] of char = (' ', #9, '&', ';', #$a, #0);
function inarg(c:char):boolean;
var
wrk:pchar;
begin
wrk := special;
while wrk^<>#0 do
begin
if c = wrk^ then
begin
inarg:=false;
exit;
end;
inc(wrk);
end;
inarg:=true;
end;
Теперь можно составить функцию, которая будет выполнять главную работу нашего интерпретатора. Функция procline будет разбирать командную строку, используя процедуру gettok, создавая тем самым список аргументов процесса. Если встретится символ перевода строки или точка с запятой, то она вызывает для выполнения команды процедуру runcommand. При этом она предполагает, что командная строка уже была считана при помощи функции userin.
{$i smallsh.inc}
function procline:integer; (* обработка строки ввода *)
var
arg:array [0..MAXARG] of pchar; (* массив указателей для runcommand *)
toktype:integer; (* тип лексемы в команде *)
narg:integer; (* число аргументов *)
_type:integer; (* на переднем плане или в фоне *)
begin
narg := 0;
while true do (* бесконечный цикл *)
begin
(* Выполнить действия в зависимости от типа лексемы *)
toktype := gettok (@arg[narg]);
case toktype of
2://ARG
if narg < MAXARG then
inc(narg);
1,3,4://EOL,SEMICOLON, AMPERSAND:
begin
if toktype = AMPERSAND then
_type := BACKGROUND
else
_type := FOREGROUND;
if narg <> 0 then
begin
arg[narg] := nil;
runcommand (arg, _type);
end;
if toktype = EOL then
exit;
narg := 0;
end;
end;
end;
end;
Следующий этап состоит в определении процедуры
runcommand, которая в действительности запускает командные процессы. Процедура runcommand в сущности, является переделанной процедурой docommand, с которой встречались раньше. Она имеет еще один целочисленный параметр
where. Если параметр where принимает значение BACKGROUND, определенное в файле smallsh.inc, то вызов waitpid пропускается, и процедура runcommand просто выводит идентификатор процесса и завершает работу.
{$i smallsh.inc}
(* Выполнить команду, возможно ожидая ее завершения *)
function runcommand(cline:ppchar;where:integer):integer;
var
pid:longint;
status:integer;
begin
pid := fork;
case pid of
-1:
begin
perror ('smallsh');
runcommand:=-1;
exit;
end;
0:
begin
execvp (cline^, cline, envp);
perror (cline^);
halt(1);
end;
end;
(* Код родительского процесса *)
(* Если это фоновый процесс, вывести pid и выйти *)
if where = BACKGROUND then
begin
writeln ('[Идентификатор процесса ',pid,']');
runcommand:=0;
exit;
end;
(* Ожидание завершения процесса с идентификатором pid *)
if waitpid (pid, @status, 0) = -1 then
runcommand:=-1
else
runcommand:=status;
end;
Обратите внимание, что простой вызов wait из функции docommand был заменен вызовом waitpid. Это гарантирует, что выход из процедуры docommand произойдет только после завершения процесса, запущенного в этом вызове docommand, и помогает избавиться от проблем с фоновыми процессами, которые завершаются в это время. (Если это кажется не совсем ясным, следует вспомнить, что вызов wait возвращает идентификатор первого завершающегося дочернего процесса, а не идентификатор последнего запущенного.)
Процедура runcommand также использует системный вызов execvp. Это гарантирует, что при запуске программы, заданной командой, выполняется ее поиск во всех каталогах, указанных в переменной окружения PATH, хотя, в отличие от настоящего командного интерпретатора, в программе smallsh нет никаких средств для работы с переменной PATH.
Последний шаг состоит в написании программы, которая связывает вместе остальные функции. Это простое упражнение:
(* Программа smallsh - простой командный интерпретатор *)
{$i smallsh.inc}
const
prompt = 'Command> '; (* приглашение ввода командной строки *)
begin
while userin (prompt) <> EOF do
procline;
end.
Эта процедура завершает первую версию программы
smallsh. И снова следует отметить, что это только набросок законченного решения. Так же, как в случае процедуры docommand, поведение программы smallsh далеко от идеала, когда пользователь вводит символ прерывания, поскольку это приводит к завершению работы программы smallsh. В следующей главе будет показано, как можно сделать программу smallsh более устойчивой.
Упражнение 5.9. Включите в программу smallsh механизм для выключения с помощью символа \ (escaping) специального значения символов, таких как точка с запятой и символ &, так чтобы они могли входить в список аргументов программы. Программа должна также корректно интерпретировать комментарии, обозначаемые символом # в начале. Что должно произойти с приглашением командной строки, если пользователь выключил таким способом специальное значение символа возврата строки?
Упражнение 5.10. Системный вызов dup2 можно использовать для получения копии дескриптора открытого файла. В этом случае он вызывается следующим образом:
dup2(filedes, reqvalue);
где filedes – это исходный дескриптор открытого файла. Значение переменной reqvalue должно быть небольшим целым числом. Если уже был открыт файл с дескриптором, равным reqvalue, он закрывается. После успешного вызова переменная reqvalue будет содержать дескриптор файла, который ссылается на тот же самый файл, что и дескриптор filedes. Следующий фрагмент программы показывает, как перенаправить стандартный ввод, то есть дескриптор файла со значением 0:
fd := fdopen('somefile', Open_RDONLY);
fdclose (0);
dup2(fd, 0);
Используя этот вызов вместе с системными вызовами fdopen и fdclose, переделайте программу smallsh так, чтобы она поддерживала перенаправление стандартного ввода и стандартного вывода, используя ту же систему обозначений, что и стандартный командный интерпретатор UNIX. Помните, что стандартный ввод и вывод соответствует дескрипторам 0 и 1 соответственно. Обратите внимание, что существует также близкий по смыслу вызов dup.
Команды ipcs и ipcrm
Существуют две команды оболочки для работы со средствами межпроцессного взаимодействия. Первая из них – команда ipcs, которая выводит информацию о текущем статусе средства межпроцессного взаимодействия. Вот простой пример ее применения:
$ ipcs
IPC status from /dev/kmem as of Wed Feb 26 18:31:31 1998
Т ID KEY MODE OWNER GROUP
Message Queues:
Shared Memory:
Semaphores
s 10 0х00000200 --ra------- keith users
Другая команда ipcrm используется для удаления средства межпроцессного взаимодействия из системы (если пользователь является его владельцем или суперпользователем), например, команда
$ ipcrm -s 0
удаляет семафор, связанный с идентификатором 0, а команда
$ ipcrm -S 200
удаляет семафор со значением ключа равным 200.
За дополнительной информацией о возможных параметрах этих команд следует обратиться к справочному руководству системы.
Комплексный пример
Число возможных комбинаций различных форматов огромно, поэтому для экономии места в одну программу были включены сразу несколько примеров. Функция arctan является стандартной функцией арктангенса из математической библиотеки math.
(* Программа cram - демонстрация процедуры printf *)
uses stdio,math;
const
weekday:pchar = 'Воскресенье';
month:pchar = 'Сентября';
str:pchar = 'Hello, world';
i:longint = 11058;
day:longint = 15;
hour:longint = 16;
minute:longint = 25;
begin
(* Вывести дату *)
printf ('Дата %s, %d %s, %d:%.2d'#$a,
[weekday, day, month, hour, minute]);
(* Перевод строки *)
putchar ($a);
(* Демонстрация различных комбинаций ширины поля и точности *)
printf ('>>%s<<'#$a, [str]);
printf ('>>%30s<<'#$a, [str]);
printf ('>>%-30s<<'#$a, [str]);
printf ('>>%30.5s<<'#$a, [str]);
printf ('>>%-30.5s<<'#$a, [str]);
putchar ($a);
(* Вывести число i в разных форматах *)
printf ('%d, %u, %o, %x, %X'#$a, [i, i, i, i, i]);
(* Вывести число пи с точностью 5 знаков после запятой *)
printf ('пи равно %.5f '#$a, [4 * arctan (1.0)]);
end.
Программа генерирует следующий вывод:
Дата Воскресенье, 15 Сентября, 16:25
>>Hello, world<<
>> Hello, world<<
>>Hello, world <<
>> Hello<<
>>Не11о <<
11058, 11058, 25462, 2b32, 2B32
пи равно 3.14159
Литература
Бах Морис. Архитектура операционной системы UNIX. (http://lib.ru/BACH/)
Богатырев Андрей. Хрестоматия по программированию на Си в Unix. (http://lib.ru/CTOTOR/book.txt)
Голдт С., ван дер Меер С., Буркетт С., Уэлш М. Руководство программиста для Linux. (http:// www.opennet.ru/docs/RUS/Lpg)
Джонсон Майкл. Ядро ОС Linux. Руководство системного программиста. (www.linuxrsp.ru/docs/books/khg.html)
Керниган Б.В., Пайк Р. UNIX – универсальная среда программирования. – М.: Финансы и статистика, 1992. – 304 с.
Полищук А.П., Семериков С.А. Программирование в X Window средствами Free Pascal. (http://freepascal.ru/article//book/xwin/, http://pascal.sources.ru/graph/xwinfpc/)
Робачевский А.М. Операционная система UNIX. – Спб.: БХВ-Петербург, 2000. – 528 с.
Уолтон Шон. Создание сетевых приложений в среде Linux. Руководство разработчика. – М.: Вильямс, 2001. – 464 с.
Хантер Робин. Основные концепции компиляторов. – М.: Издательский дом «Вильямс», 2002. – 256 с.
Хэвиленд К., Грэй Д., Салама Б. Системное программирование в UNIX. Руководство программиста по разработке ПО. – M.: ДМК Пресс, 2000. – 368 с.
Эндрюс Грегори Р. Основы многопоточного, параллельного и распределенного программирования. – М.: Издательский дом «Вильямс», 2003. – 512 с.
Draft Standard for Information Technology – Portable Operating System Interface (POSIX) Part 2: Shell and Utilities (IEEE P1003.2 Draft 11.2 – September 1991)
A.D. Marshall. Programming in C. UNIX System Calls and Subroutines using C.
(http://www.cm.cf.ac.uk/Dave/C/CE.html)
Mark Mitchell, Jeffrey Oldham, and Alex Samuel. Advanced Linux Programming. – New Riders Publishing, 2001. – 340 p.
Steve D. Pate. UNIX Internals: A Practical Approach. – Addison-Wesley, 1996. – 667 p.
Uresh Vahalia. UNIX Internals: The New Frontiers. – Prentice Hall, 1996. – 601 p.
Kurt Wall, Mark Watson, and Mark Whitis. Linux Programming Unleashed. – Sams, 1999. – 847 p.
[1]
Согласно спецификации SUSV2 заголовочный файл limits.h
должен определять константу OPEN_MAX, задающую это значение.
[2]
Использование suid-программ для нарушения защиты – известнейший способ взлома систем. Существует набор строгих правил составления защищенных suid-программ. Самое простое из этих правил (но явно недостаточное) – не давать никому права читать содержимое таких программ. Благодаря этому иногда можно скрыть слабое место программы от посторонних глаз.
[3]
Фоновый процесс при попытке вывода на консоль остановится, получив сигнал SIGTTOU (см. главу 6). Следует придумать иной способ оповещения об изменениях файлов.
[4]
Не следует путать это понятие с понятием потока выполнения, когда несколько копий кода могут работать с одним набором данных. Потоки выполнения сейчас доступны в некоторых реализациях UNIX, и они охватываются последними расширениями стандарта POSIX и спецификации XSI. Тем не менее мы не будем более подробно описывать сложности многопоточной модели. За дальнейшей информацией обратитесь к справочному руководству системы.
[5]
Здесь следует отметить, что значение argv[0] – не пустая трата памяти, а весьма важный параметр. Во-первых, он напоминает программе ее имя: признаком хорошего стиля программирования считается вывод диагностики от имени программы argv[0], ведь заранее не известно, как впоследствии переименует программу пользователь. Во-вторых, у исполняемого файла может быть несколько имен (вспомните про ссылки), и это можно выгодно использовать. Часто множество утилит на самом деле является одной программой, которая ведет себя по-разному в зависимости от использованного имени. Это кажется странным, но прекрасно работает. Так, программа удаленного выполнения команд rsh, будучи названной именем neibor, ведет себя так, как будто ей сообщили дополнительный первый аргумент neibor. Можно заготовить набор псевдонимов этой программы для запуска команд на соседних системах сети.
[6]
Современные системы, согласно спецификации SUSV2 и стандарту POSIX, должны также позволять возвращаться от истинных идентификаторов к сохраненным (saved-set-uid, saved-set-gid) действующим идентификаторам пользователя и группы.
[7]
Некоторые из упомянутых сигналов могут отсутствовать в используемой системе, тогда компилятор сообщит о неизвестном мнемоническом имени. Иногда имя сигнала определено, а данный сигнал отсутствует в системе. В ряде случаев требуется определить наличие определенного сигнала на стадии выполнения программы. В этих ситуациях можно воспользоваться советом, приведенным в информативной части стандарта POSIX 1003.1: наличие поддержки сигнала сообщает вызов функции sigaction() с аргументами act
и оасt, имеющими значения NULL.
[8]
Спецификация SUSV2 приводит для этих сигналов нормальное завершение в качестве действия по умолчанию; лучшими примерами являются сигналы SIGCHLD и SIGURG.
[9]
Тем не менее при написании сложных систем следует знать некоторые дополнительные детали механизма доставки и обработки сигналов (см. стандарт POSIX 1003.1, спецификацию SUSV2, и руководство используемой программистом системы).
[10]
На самом деле программа имеет возможность с помощью операций управления ввода/вывода выяснить тип конечного устройства, используемого в качестве стандартного ввода/вывода (файл, терминал или канал). Некоторые стандартные утилиты UNIX ведут себя по-разному в разных ситуаций; сравните вывод ls на терминал и в канал.
[11]
В некоторых реализациях вызов select изменяет также содержимое структуры timeout: оно заполняется оставшимся временем до истечения первоначально заданного интервала. Данную возможность следует учитывать при вызове select с нулевыми масками в качестве высокоточного аналога вызова sleep – тогда использование select в цикле может привести к неправильным результатам.
[12]
В некоторых версиях UNIX есть также возможность применения обязательных блокировок (mandatory lock).
[13]
Три упомянутые средства часто называют System V IPC, поскольку впервые это семейство межпроцессных взаимодействий было введено в диалекте System V Unix. Стандарт POSIX 1003.1 их не описывает; более того. аналогичные POSIX-средства имеют другой интерфейс и семантику. Тем не менее System V IPC внесены в спецификацию SUSV2 как заимствованные из второй версии спецификации SVID.
[14]
Клинч иногда является следствием гораздо более запутанной ситуации, если установлению блокировки препятствуют одновременно несколько других блокировок.
[15]
В базовом документе POSIX 1003.1 средства IPC System V не вводятся.
[16]
Более точно порядок разрешения доступа к объекту IPC описан в спецификации SUSV2.
[17]
Протокол UDP не гарантирует доставку дейтаграмм; кроме того, может быть нарушен исходный порядок сообщений, допускается также случайное дублирование дейтаграмм. Приложения, использующие протокол UDP, должны обеспечивать контроль данных на прикладном уровне, для этого может потребоваться организация подтверждения доставки и повторной пересылки данных.
[18]
Потоки stdin, stdout
и stderr не следует закрывать: это может привести к аварийному завершению процесса, так как соответствующие структуры TFILE
часто размещены в статической, а не динамической памяти.
[19]
Опытные программисты не советуют использовать функцию fscanf
(scanf) для ввода данных, кроме случаев простого интерактивного ввода. Вместо функции fscanf
(scanf) предлагается использовать комбинацию вызовов fgets (gets) и sscanf. Недостаток функции fscanf
(scanf) в данном случае состоит в том, что при случайном нарушении формата строки вводимых данных эта функция может перейти к чтению следующей строки, поскольку функция fscanf (scanf) не отличает символ окончания строки от других разделителей полей. В такой ситуации сложно обработать ошибку входных данных корректно, а кроме того, ввод следующей строки тоже будет нарушен.
[20]
Для отладки программ, интенсивно использующих динамическую память, существуют специальные библиотеки, подменяющие стандартный механизм процедур семейства malloc. Эти библиотеки менее производительны, зато выполняют функцию «раннего предупреждения» подобных ошибок, то есть незамедлительно фиксируют нарушения правил работы с динамической памятью.
[21]
Полезно также знать о процедуре setsockopt, управляющей параметрами соединения.
Маска создания файла и системный вызов umask
Как уже было отмечено в главе 2, первоначально права доступа к файлу задаются в момент его создания при помощи вызова fdcreat или fdopen в расширенной форме, например:
filedes := fdopen('datafile', Open_CREAT, ocatl(0644));
С каждым процессом связано значение, называемое маской создания файла (file creation mask). Эта маска используется для автоматического выключения битов прав доступа при создании файлов, независимо от режима, заданного соответствующим вызовом fdcreat или fdopen. Это полезно для защиты всех создаваемых за время существования процесса файлов, так как предотвращается случайное включение лишних прав доступа.
Основная идея просматривается четко: если в маске создания файлов задан какой-либо бит доступа, то при создании файлов он всегда остается выключенным. Биты в маске могут быть установлены при помощи тех же восьмеричных постоянных, которые были описаны ранее для кода доступа к файлам, хотя при этом могут использоваться только основные права доступа на чтение, запись и выполнение. Экзотические права доступа, такие как STAT_ISUID, не имеют смысла для маски создания файла.
Таким образом, с точки зрения программиста, оператор
filedes := fdopen(pathname, Open_CREAT, mode);
эквивалентен оператору
filedes := fdopen(pathname, Open_CREAT, (not mask) and mode);
где переменная mask содержит текущее значение маски создания файла, not – это оператор побитового отрицания, а and – оператор побитового И.
Например, если значение маски равно 04+02+01=07, то права доступа, обычно задаваемые этими значениями, при создании файла выключаются. Поэтому файл, создаваемый при помощи оператора
filedes := fdopen(pathname, Open_CREAT, octal(0644));
в действительности будет иметь код доступа 0640. Это означает, что владелец файла и пользователи из связанной с файлом группы смогут использовать файл, а пользователи всех остальных категорий не будут иметь доступа к нему.
Маску создания файла можно изменить при помощи системного вызова umask.
Массив с_сс
Символы редактирования строки, которые рассматривались в разделе 9.2.4, находятся в массиве с_сс. Их позиции в этом массиве задаются константами, определенными в файле stdio. Все определенные в спецификации ХSI значения приведены в табл. 9.1. Размер массива определяется константой
NCCS, определенной в файле linux.
Следующий фрагмент программы показывает, как можно изменить значение символа quit
для терминала, связанного со стандартным вводом (дескриптор файла со значением 0):
var
tdes:termios;
(* Получить исходные настройки терминала *)
tcgetattr(0, tdes);
tdes.c_cc[VQUIT] := char(octal(031)); (* CTRL-Y *)
(* Изменить установки терминала *)
tcsetattr(0, TCSAFLUSH, tdes);
Таблица 9.1. Коды управляющих символов
Константа | Значение | ||
VINTR | Клавиша прерывания (Interrupt key) | ||
VQUIT | Клавиша завершения (Quit key) | ||
VERASE | Символ стирания (Erase character) | ||
VKILL | Символ удаления строки (Kill character) | ||
VEOF | Символ конца файла (EOF character) | ||
VEOL | Символ конца строки (End of line marker – необязательный) | ||
VSTART | Символ продолжения передачи данных (Start character) | ||
VSTOP | Символ остановки передачи данных (Stop character) | ||
VSUSP | Символ временной приостановки выполнения (Suspend character) |
Этот пример иллюстрирует наиболее безопасный способ изменения состояния терминала. Сначала нужно получить текущее состояние терминала. Далее следует изменить только нужные параметры, не трогая остальные. И, наконец, изменить состояние терминала при помощи модифицированной структуры termios. Как уже было упомянуто, сохранять исходные значения полезно также для восстановления состояния терминала перед завершением программы, иначе нестандартное состояние терминала может оказаться сюрпризом для остальных программ.
Математические функции
ОС UNIX предоставляет обширную библиотеку математических функций для программирования научных и технических приложений. Для применения некоторых из этих функций необходимо подключать файл math, который включает определения функций, некоторых важных констант (таких как е или ?) и структур, относящихся к обработке ошибок. Для использования большинства функций, которых коснемся ниже, потребуется также подключить математическую библиотеку:
uses math;
Межпроцессное взаимодействие
Система UNIX позволяет процессам, выполняемым одновременно, взаимодействовать друг с другом, используя ряд методов межпроцессного взаимодействия.
Одним из таких методов является использование программных каналов
(pipes). Они обычно связывают выход одной программы с входом другой без необходимости сохранения данных в промежуточном файле. Пользователи опять же могут применять эти средства при помощи командного интерпретатора. Командная строка
$ ls | wc -l
организует конвейер из двух процессов, в одном из которых будет выполняться программа ls, а в другом – одновременно программа подсчета числа слов wc. При этом выход ls будет связан с входом wc. В результате будет выведено число файлов в текущем каталоге.
Другими средствами межпроцессного взаимодействия UNIX являются сигналы
(signals), которые обеспечивают модель взаимодействия по принципу программных прерываний. Дополнительные средства предоставляют семафоры (semaphores) и разделяемая память (shared memory). Для обмена между процессами одной системы могут также использоваться сокеты
(sockets), используемые обычно для взаимодействия между процессами в сети.
Мотивация
На первом этапе стоит рассмотреть простой пример демонстрации того, почему блокировка записей необходима в некоторых ситуациях.
Примером будет служить известная корпорация ACME Airlines, использующая ОС UNIX в своей системе заказа билетов. Она имеет два офиса, А и В, в каждом из которых установлен терминал, подключенный к компьютеру авиакомпании. Служащие компании используют для доступа к базе данных, реализованной в виде обычного файла UNIX, программу acmebook. Эта программа позволяет пользователю выполнять чтение и обновление базы данных. В частности, служащий может уменьшить на единицу число свободных мест при заказе билета на определенный авиарейс.
Предположим теперь, что осталось всего одно свободное место на рейс АСМ501 в Лондон, и миссис Джонс входит в офис А; в то же самое время мистер Смит входит в офис В, и они оба заказывают место на рейс АСМ501. При этом возможна такая последовательность событий:
1.
Служащий офиса А запускает программу acmebook. Назовем стартовавший процесс РА.
2. Сразу же после этого служащий в офисе В также запускает программу acmebook. Назовем этот процесс РВ.
3. Процесс РА считывает соответствующую часть базы данных при помощи системного вызова fdread и определяет, что осталось всего одно свободное место.
4. Процесс РВ выполняет чтение из базы данных сразу же после процесса РА и также определяет, что осталось одно свободное место на рейс АСМ501.
5. Процесс РА обнуляет счетчик свободных мест для рейса при помощи системного вызова fdwrite, изменяя соответствующую часть базы данных. Служащий в офисе А вручает билет миссис Джонс.
6. Сразу же вслед за этим процесс РВ также выполняет запись в базу данных, также записывая нулевое значение в счетчик свободных мест. Но на этот раз значение счетчика ошибочно – на самом деле оно должно было бы быть равно -1, то есть хотя процесс РА уже обновил базу данных, процесс РВ не знает об этом и спешит выполнить заказ, как если бы место было свободно. Вследствие этого мистер Смит также получит билет, и на самолет будет продано больше билетов, чем число свободных мест в нем.
Эта проблема возникает из- за того, что несколько процессов могут одновременно обращаться к файлу UNIX. Комплексная операция с данными файла, состоящая из нескольких вызовов fdseek, fdread и fdwrite, может быть выполнена двумя или более процессами одновременно, и это, как показывает наш простой пример, будет иметь непредвиденные последствия.
Одно из решений состоит в том, чтобы разрешить процессу выполнить блокировку (lock) части файла, с которой он работает. Блокировка, которая нисколько не изменяет содержимое файла, показывает другим процессам, что данные, о которых идет речь, уже используются. Это предотвращает вмешательство другого процесса во время последовательности дискретных физических операций, образующих одну комплексную операцию, или транзакцию. Этот механизм часто называют блокировкой записи (record locking), где запись означает просто произвольную часть файла. Для обеспечения корректности сама операция блокировки должна быть атомарной, чтобы она не могла пересечься с параллельной попыткой блокировки в другом процессе.
Для обеспечения нормальной работы блокировка должна выполняться централизованно. Возможно, лучше всего это возложить на ядро, хотя пользовательский процесс, выступающий в качестве агента базы данных, также может служить для этой цели. Блокировка записей на уровне ядра может выполняться при помощи уже известного нам вызова fcntl.
Обратите внимание, что возможен также альтернативный способ блокировки записей – при помощи процедуры lockf. Этот подход все еще встречается во многих системах – за дополнительными сведениями следует обратиться к справочному руководству системы.
Наборы сигналов
Наборы сигналов являются одним из основных параметров, передаваемых работающим с сигналами системным вызовам. Они просто задают список сигналов, которые необходимо передать системному вызову.
Наборы сигналов определяются при помощи типа sigset_t, который определен в файле linux. Размер типа задан так, чтобы в нем мог поместиться весь набор определенных в системе сигналов. Выбрать определенные сигналы можно, начав либо с полного набора сигналов и удалив ненужные сигналы, либо с пустого набора, включив в него нужные. Инициализация пустого и полного набора сигналов выполняется при помощи процедур sigemptyset и sigfillset соответственно. После инициализации с наборами сигналов можно оперировать при помощи процедур sigaddset и sigdelset, соответственно добавляющих и удаляющих указанные вами сигналы.
Назначение этой книги
Со времени своего появления в Bell Laboratories в 1969 г. операционная система UNIX становилась все более популярной, вначале получив признание в академическом мире, а затем уже в качестве стандартной операционной системы для нового поколения многопользовательских микро- и миникомпьютеров в 80-х годах. И этот рост, по-видимому, продолжается в момент написания данной книги.
Операционная система UNIX оправдала возлагавшиеся на нее надежды и теперь является ключевой деталью технологического пейзажа на рубеже XXI века. Не говоря уже о том, что UNIX всегда занимала сильные позиции в научном и техническом сообществах, в настоящее время существует множество крупномасштабных систем управления данными и обработки транзакций на платформе UNIX. Но, самое главное, ОС UNIX, безусловно, является ядром серверов магистральной сети Internet.
Основное внимание в книге уделяется программному интерфейсу между ядром UNIX (частью UNIX, которая делает ее операционной системой) и прикладным программным обеспечением, которое выполняется в среде UNIX. Этот интерфейс часто называется интерфейсом системных вызовов UNIX (хотя разница между такими вызовами и обычными библиотечными процедурами теперь уже не столь очевидна, как это было раньше). В дальнейшем мы увидим, что системные вызовы – это примитивы, на которых в конечном счете построены все программы UNIX – и поставляемые вместе с операционной системой, и разрабатываемые независимо. Целевая аудитория книги состоит из программистов, уже знакомых с UNIX, которые собираются разрабатывать программное обеспечение для ОС UNIX на языке Pascal. Эта книга в равной мере подойдет разработчикам системного программного обеспечения и прикладных или деловых приложений – фактически всем, кто серьезно интересуется разработкой программ для ОС UNIX.
Кроме системных вызовов мы также рассмотрим некоторые из наиболее важных библиотек подпрограмм, которые поставляются с системой UNIX. Эти библиотеки, конечно же, написаны с использованием системных вызовов и во многих случаях делают то же самое, но на более высоком уровне, или более удобны для использования программистами. Надеемся, что при исследовании как системных вызовов, так и библиотеки подпрограмм вы получите представление о том, когда можно пользоваться существующими достижениями, не «изобретая велосипед», а также лучше поймете внутреннее устройство этой все еще прекрасной операционной системы.
Нормальное и аварийное завершение
Получение большинства сигналов приводит к нормальному завершению
(normal termination)
процесса. Действие сигнала при этом похоже на неожиданный вызов процессом функции _exit. Статус завершения, возвращаемый при этом родительскому процессу, сообщит о причине завершения дочернего процесса. В файле stdio определены макросы, которые позволяют родительскому процессу определить причину завершения дочернего процесса (получение сигнала и, собственно, значение сигнала. Следующий фрагмент программы демонстрирует родительский процесс, проверяющий причину завершения дочернего процесса и выводящий соответствующее сообщение:
uses stdio;
.
.
pid:=wait(@status);
if pid=-1 then
begin
perror('ошибка вызова wait');
halt(1);
end;
(* Проверка нормального завершения дочернего процесса *)
if WIFEXITED(status) then
begin
exit_status := WEXITSTATUS(status);
writeln('Статус завершения ', pid, ' был ', exit_status);
end;
(* Проверка, получил ли дочерний процесс сигнал *)
if WIFSIGNALED(status) then
begin
sig_no := WTERMSIG(status);
writeln('Сигнал номер ', sig_no, ' завершил процесс ', pid);
end;
(* Проверка остановки дочернего процесса *)
if WIFSTOPPED(status) then
begin
sig_no := WSTOPSIG(status);
writeln('Сигнал номер ', sig_no, ' остановил процесс ', pid);
end;
Как уже упоминалось, сигналы SIGABRT, SIGBUS, SIGSEGV, SIGQUIT, SIGILL, SIGTRAP, SIGSYS, SIGXCPU, SIGXFSZ и SIGFPE приводят к аварийному завершению и обычно сопровождаются сбросом образа памяти на диск. Образ памяти процесса записывается в файл с именем core в текущем рабочем каталоге процесса (термин core, или сердечник, напоминает о временах, когда оперативная память состояла из матриц ферритовых сердечников). Файл core будет содержать значения всех переменных программы, регистров процессора и необходимую управляющую информацию ядра на момент завершения программы. Статус завершения процесса после аварийного завершения будет тем же, каким он был бы в случае нормального завершения из-за этого сигнала, только в нем будет дополнительно выставлен седьмой бит младшего байта. В большинстве современных систем UNIX
определен макрос WCOREDUMP, который возвращает истинное или ложное значение в зависимости от установки этого бита в своем аргументе. Тем не менее следует учесть, что макрос WCOREDUMP не определен спецификацией XSI. С применением этого макроса предыдущий пример можно переписать так:
(* Проверка, получил ли дочерний процесс сигнал *)
if WIFSIGNALED(status) then
begin
sig_no := WTERMSIG(status);
writeln('Сигнал номер ', sig_no, ' завершил процесс ', pid);
if WCOREDUMP(status) then
writeln('... создан файл дампа памяти');
end;
Формат файла core известен отладчикам UNIX, и этот файл можно использовать для изучения состояния процесса в момент сброса образа памяти. Этим можно воспользоваться для определения точки, в которой возникает проблема.
Стоит также упомянуть функцию abort, которая не имеет аргументов:
abort;
Эта функция посылает вызвавшему ее процессу сигнал SIGABRT, вызывая его аварийное завершение, то есть сброс образа памяти. Процедура
abort полезна в качестве средства отладки, так как позволяет процессу записать свое текущее состояние, если что-то происходит не так. Она также иллюстрирует тот факт, что процесс может послать сигнал самому себе.
О книге
В основу данной книги положено второе издание руководства программиста UNIX System Programming: A programmer’s guide to software development by Keith Haviland, Dina Gray, Ben Salama. Очень удачное по структуре и подбору примеров, это руководство является одним из лучших учебников по системному программированию в UNIX, поэтому с самого начала мы посчитали уместным сохранить их, исправив и дополнив в соответствии с новыми возможностями Linux/BSD и компилятора Free Pascal.
На первом этапе нашей работы был создан модуль stdio, необходимый для совместимости со стандартной библиотекой языка Си. В модуль вошли множество структур данных, процедур и функций, не входящих в библиотечные модули Free Pascal, но существенно облегчающие жизнь программиста.
На втором этапе примеры из книги Кейт Хэвиленд, Даны Грей и Бена Саламы были переведены с Си на Паскаль. Это потребовало модификации значительной части текста книги, посвященной описанию используемых библиотечных функций и системных вызовов.
Наконец, книга была дополнена описанием структур данных, процедур и функций библиотечных модулей linux, ipc и sockets, специфичных для ОС Linux/BSD.
В результате проделанной работы была получена данная книга, в которой сохранилось часть исходного текста из книги Кейт Хэвиленд, Даны Грей и Бена Саламы. Разумеется, при необходимости эти фрагменты могут быть заменены на другие, но результатом этого будет всего лишь изложение известной справочной информации иными словами.
Обход дерева каталогов
Иногда необходимо выполнить операцию над иерархией каталогов, начав от стартового каталога, и обойти все лежащие ниже файлы и подкаталоги. Для этого определим процедуру ftw, выполняющую обход дерева каталогов, начиная с заданного, и вызывающая процедуру, определенную пользователем для каждой встретившейся записи в каталоге.
Обобщение концепции файла
В UNIX концепция файлов расширена и охватывает не только обычные файлы (regular files), но и периферийные устройства, а также каналы межпроцессного взаимодействия. Это означает, что одни и те же примитивы могут использоваться для записи и чтения из текстовых и двоичных файлов, терминалов, накопителей на магнитной ленте и даже оперативной памяти. Данная схема позволяет рассматривать программы как обобщенные инструменты, способные использовать любые типы устройств. Например,
$ cat file > /dev/rmt0
представляет грубый способ записи файла на магнитную ленту (путь /dev/rmt0 обычно обозначает стример).
Обработка сигналов
При получении сигнала процесс может выполнить одно из трех действий:
–
выполнить действие по умолчанию. Обычно действие по умолчанию заключается в прекращении выполнения процесса. Для некоторых сигналов, например, для сигналов SIGUSR1 и SIGUSR2, действие по умолчанию заключается в игнорировании сигнала.[8]
Для других сигналов, например, для сигнала SIGSTOP, действие по умолчанию заключается в остановке процесса;
– игнорировать сигнал и продолжать выполнение. В больших программах неожиданно возникающие сигналы могут привести к проблемам. Например, нет смысла позволять программе останавливаться в результате случайного нажатия на клавишу прерывания, в то время как она производит обновление важной базы данных;
– выполнить определенное пользователем действие. Программист может захотеть выполнить при выходе из программы операции по «наведению порядка» (такие как удаление рабочих файлов), что бы ни являлось причиной этого выхода.
В старых версиях UNIX обработка сигналов была относительно простой. Здесь будут изучены современные процедуры управления механизмом сигналов, и хотя эти процедуры несколько сложнее, их использование дает вполне надежный результат, в отличие от устаревших методов обработки сигналов. Прежде чем перейти к примерам, следует сделать несколько пояснений. Начнем с определена набора сигналов (signal set).
Обработка текста
Упражнение 13.1. Напишите программу, отсекающую n пробелов в начале каждой строки (или n первых любых символов). Учтите, что в файле могут быть строки короче n (например, пустые строки).
Program Tabs;
uses sysutils,linux;
var
f1,f2:Text;
TmpS:string;
n,Code:Integer;
begin
Assign(f1,Paramstr(1));
Assign(f2,Paramstr(2));
Reset(f1);
Rewrite(f2);
if ParamCount=3 then val(ParamStr(3),n,Code)
else n:=0;
While Not(eof(f1)) do
begin
Readln(f1,TmpS);
Writeln(f2,Copy(TmpS,n,Ord(TmpS[0])-n));
end;
Close(f1);
Close(f2);
end.
Упражнение 13.2. Составьте программу, центрирующую строки файла относительно середины экрана, т.е. добавляющую в начало строки такое количество пробелов, чтобы середина строки печаталась в 40-ой позиции (считаем, что обычный экран имеет ширину 80 символов).
Program Center;
uses crt;
var
s:string;
f:text;
procedure writecenter(s:string);
begin
if ord(s[0])<80 then
begin
GotoXY(wherex+(80-ord(s[0]))div 2,wherey);
writeln(s);
end
else writeln(s);
end;
begin
if paramcount<1 then
begin
Writeln('error');
Halt(1);
end;
Assign(f,paramstr(1));
Reset(f);
While not(eof(f)) do
begin
Readln(f,s);
writecenter(s);
end;
Close(f);
end.
Упражнение 13.3. Напишите программу, переносящую слишком длинные строки. Слова разбивать нельзя (неумещающееся слово следует перенести целиком). Ширину строки считать равной 60.
Program Tabs;
var
f1,f2:Text;
TmpS,StrBuf:string;
n,Code:Integer;
const
step=60;
begin
Assign(f1,Paramstr(1));
Assign(f2,Paramstr(2));
Reset(f1);
Rewrite(f2);
StrBuf:='';
While Not(eof(f1)) do
begin
Readln(f1,TmpS);
TmpS:=StrBuf+TmpS;
n:=step;
if ord(Tmps[0])>step then
begin
While (not(TmpS[n]in [' ',',','(',')',';','.']))do if n>=1 then Dec(n) else Exit;
While (TmpS[n]in [' ',',','(',')',';','.'])do if n>=1 then Dec(n) else Exit;;
Очереди сообщений
Начнем подробное рассмотрение средств межпроцессного взаимодействия с примитивов очередей сообщений.
В сущности, сообщение является просто последовательностью символов или байтов (необязательно заканчивающейся нулевым символом). Сообщения передаются между процессами при помощи очередей сообщений (message queues), которые можно создавать или получать к ним доступ при помощи вызова msgget. После создания очереди процесс может помещать в нее сообщения при помощи вызова msgsnd, если он имеет соответствующие права доступа. Затем другой процесс может считать это сообщение при помощи примитива msgrcv, который извлекает сообщение из очереди. Таким образом, обработка сообщений аналогична обмену данными при помощи вызовов чтения и записи для каналов (рассмотренном в разделе 7.1.2.).
Функция msgget определяется следующим образом:
Ограничения файловой системы: процедуры pathconf и fpathconf
Комитет разработки стандарта POSIX и другие группы разработки стандартов несколько формализовали способ определения системных ограничений. Так как система может поддерживать различные типы файловых систем, определенные ограничения могут различаться для разных файлов и каталогов. Для запроса этих ограничений, связанных с определенным каталогом, могут использоваться две процедуры, pathconf и fpathconf.
Ограничения на размер файла: вызов ulimit
Для каждого процесса существует ограничение на размер файла, который может быть создан при помощи системного вызова fdwrite. Это ограничение распространяется также на ситуацию, когда увеличивается размер существующего файла, имевшего до этого длину, меньшую максимально возможной.
Предельный размер файла можно изменять при помощи системного вызова ulimit.
Операции над семафорами: вызов semop
Вызов semop выполняет основные операции над семафорами.
Операции с разделяемой памятью: вызовы shmat и shmdt
Сегмент памяти, созданный вызовом shmget, является участком физической
памяти и не находится в логическом
пространстве данных процесса. Для использования разделяемой памяти текущий процесс, а также все другие процессы, взаимодействующие с этим сегментом, должны явно подключать этот участок памяти к логическому адресному пространству при помощи вызова shmat:
Операция get
Программа применяет ключ для создания объекта межпроцессного взаимодействия или получения доступа к существующему объекту. Обе операции вызываются при помощи операции get. Результатом операции get является его целочисленный идентификатор (facility identifier), который может использоваться при вызовах других процедур межпроцессного взаимодействия. Если продолжить аналогию с именами файлов, то операция get похожа на вызов fdcreat или fdopen, а идентификатор средства межпроцессного взаимодействия ведет себя подобно дескриптору файла. В действительности, в отличие от дескрипторов файла, идентификатор средства межпроцессного взаимодействия является уникальным. Различные процессы будут использовать одно и то же значение идентификатора для объекта межпроцессного взаимодействия.
В качестве примера рассмотрим вызов msgget для создания новой очереди сообщений (что представляет собой очередь сообщений, обсудим позже):
mqid := msgget(octal(0100), octal(0644) or IPC_CREAT or IPC_EXCL);
Первый аргумент вызова, msgget, является ключом очереди сообщений. В случае успеха процедура вернет неотрицательное значение в переменной mqid, которая служит идентификатором очереди сообщений. Соответствующие вызовы для семафоров и объектов разделяемой памяти называются соответственно semget и shmget.
Первый аргумент, pathname, является указателем
uses linux;
Function fdOpen(PathName:String;flags:longint):longint;
Function fdOpen(PathName:Pchar;flags:longint):longint;
Function fdOpen(PathName:String;flags,mode:longint):longint;
Function fdOpen(PathName:Pchar;flags,mode:longint):longint;
Первый аргумент, pathname, является указателем на строку маршрутного имени открываемого файла. Значение pathname может быть абсолютным путем, например:
/usr/keith/junk
Данный путь задает положение файла по отношению к корневому каталогу. Аргумент pathname может также быть относительным путем, задающим маршрут от текущего каталога к файлу, например:
keith/junk
или просто:
junk
В последнем случае программа откроет файл junk в текущем каталоге. В общем случае, если один из аргументов системного вызова или библиотечной процедуры – имя файла, то в качестве него можно задать любое допустимое маршрутное имя файла UNIX.
Второй аргумент системного вызова fdopen, который в нашем описании называется flags, имеет целочисленный тип и определяет метод доступа. Параметр flags принимает одно из значений, заданных постоянными в модуле linux при помощи директивы const (fcntl является сокращением от file control, «управление файлом»). Так же, как и большинство стандартных модулей, файл linux.ppu может быть включен в программу при помощи директивы:
uses linux;
В модуле linux определены три постоянных, которые сейчас представляют для нас интерес:
Open_RDONLY Открыть файл только для чтения
Open_WRONLY Открыть файл только для записи
Open_RDWR Открыть файл для чтения и записи
В случае успешного завершения вызова fdopen и открытия файла возвращаемое вызовом fdopen значение будет содержать неотрицательное целое число – дескриптор файла. Значение дескриптора файла будет наименьшим целым числом, которое еще не было использовано в качестве дескриптора файла выполнившим вызов процессом – знание этого факта иногда может понадобиться. Как отмечено во введении, в случае ошибки вызов fdopen возвращает вместо дескриптора файла значение -1. Это может произойти, например, если файл не существует. Для создания нового файла можно использовать вызов fdopen с параметром flags, равным Open_CREAT, – эта операция описана в следующем разделе.
Необязательный (optional) третий параметр mode, используемый только вместе с флагом Open_CREAT, также будет обсуждаться в следующем разделе – он связан с правами доступа к файлу. Следует обратить внимание на квадратные скобки в описании, которые обозначают, что параметр mode является необязательным.
Следующий фрагмент программы открывает файл junk для чтения и записи и проверяет, не возникает ли при этом ошибка. Этот последний момент особенно важен: имеет смысл устанавливать проверку ошибок во все программы, которые используют системные вызовы, поскольку каким бы простым не было приложение, иногда может произойти сбой. В этом примере используются библиотечные процедуры writeln для вывода сообщения и halt – для завершения процесса. Обе эти процедуры являются стандартными в любой системе.
uses linux;
const
workfile = 'junk'; (* задать имя рабочего файла *)
var
filedes:integer;
begin
(* Открыть, используя постоянную Open_RDWR из модуля linux *)
(* Файл открывается для чтения/записи *)
filedes := fdopen (workfile, Open_RDWR);
if filedes = -1 then
begin
writeln('Невозможно открыть файл ', workfile);
halt(1); (* выход по ошибке *)
end;
writeln('Файл ', workfile, ' успешно открыт, дескриптор равен ', filedes);
(*
* Остальная программа
*)
halt(0); (* нормальный выход *)
end.
Обратите внимание, что используется halt с параметром 1 в случае ошибки, и 0 – в случае удачного завершения. Это соответствует соглашениям UNIX и является правильной практикой программирования. Как будет показано в следующих главах, после завершения программы можно получить передаваемый вызову halt аргумент (program’s exit status
– код завершения программы).
fdTruncate устанавливает длину файла, заданного
uses linux;
Function fdTruncate (fd,size:longint): boolean;
fdTruncate устанавливает длину файла, заданного дескриптором fd, в size байт, где size должен быть меньше либо равен текущей длине файла fd. Функция возвращает True, если вызов был успешен, и false в случае ошибки.
Первый параметр PathName указывает на
uses stdio;
function Fdcreat(PathName:Pchar;mode:longint):longint;cdecl;external 'c';
Первый параметр PathName указывает на маршрутное имя файла UNIX, определяющее имя создаваемого файла и путь к нему. Так же, как в случае вызова fdopen, параметр mode задает права доступа. При этом, если файл существует, то второй параметр также игнорируется. Тем не менее, в отличие от вызова fdopen, в результате вызова fdcreat файл всегда будет усечен до нулевой длины. Пример использования вызова fdcreat:
filedes := fdcreat('/tmp/newfile', octal(0644));
что эквивалентно вызову
filedes := fdopen('/tmp/newfile', Open_WRONLY or Open_CREAT or Open_TRUNC, octal(0644));
Ключевое слово cdecl определяет для данной функции стиль вызова, характерный для языка С. Это необходимо для доступа к функциям в объектных файлах, сгенерированных компилятором языка С, таким, как функции стандартной библиотеки языка С (это указывается с помощью ключевого слова external, параметром которого является имя библиотеки – 'c').
Следует отметить, что вызов fdcreat всегда открывает файл только для записи. Например, программа не может создать файл при помощи fdcreat, записать в него данные, затем вернуться назад и попытаться прочитать данные из файла, если предварительно не закроет его и не откроет снова при помощи вызова fdopen.
Упражнение 2.3. Напишите небольшую программу, которая вначале создает файл при помощи вызова fdcreat, затем, не вызывая fdclose, сразу же, открывает его при помощи системного вызова fdopen для чтения в одном случае и записи в другом. В обоих случаях выведите сообщение об успешном или неуспешном завершении, используя writeln.
Системный вызов fdclose имеет всего
uses linux;
Function fdClose(fd:longint):boolean;
Системный вызов fdclose имеет всего один аргумент – дескриптор закрываемого файла, обычно получаемый в результате предыдущего вызова fdopen или fdcreat. Следующий фрагмент программы поясняет простую связь между вызовами fdopen и fdclose:
filedes := fdopen('file', Open_RDONLY);
.
.
.
fdclose(filedes);
Системный вызов fdclose возвращает true в случае успешного завершения и false – в случае ошибки (которая может возникнуть, если целочисленный аргумент не является допустимым дескриптором файла).
При завершении работы программы все открытые файлы закрываются автоматически.
Первый параметр, filedes, является дескриптором
uses linux;
Function fdRead(filedes:longint;var buffer;size:longint):longint;
Первый параметр, filedes, является дескриптором файла, полученным во время предыдущего вызова fdopen или fdcreat. Второй параметр, buffer, – это ссылка на массив или структуру, в которую должны копироваться данные. Во многих случаях в качестве этого параметра будет выступать просто имя массива, например:
var
fd:integer;
nread:longint;
buffer:array [0..SOMEVALUE-1] of char;
(* Дескриптор файла fd получен в результате вызова fdopen *)
.
.
.
nread := fdread(fd, buffer, SOMEVALUE);
Как видно из примера, третьим параметром вызова fdread является положительное число (имеющее тип longint), задающее число байтов, которое требуется считать из файла.
Возвращаемое вызовом fdread число (присваиваемое в примере переменной nread) содержит число байтов, которое было считано в действительности. Обычно это число запрошенных программой байтов, но, как будет показано в дальнейшем, – не всегда, и значение переменной nread может быть меньше. Кроме того, в случае ошибки вызов fdread возвращает значение -1. Это происходит, например, если передать fdread недопустимый дескриптор файла.
и вызов fdread, вызов fdwrite
uses linux;
Function fdWrite (fd:longint; var buf; size:longint):longint;
Так же, как и вызов fdread, вызов fdwrite имеет три аргумента: дескриптор файла filedes, указатель на записываемые данные buffer и n – положительное число записываемых байтов. Возвращаемое вызовом значение является либо числом записанных символов, либо кодом ошибки -1. Фактически, если возвращаемое значение не равно -1, то оно почти всегда будет равно n. Если оно меньше n, значит, возникли какие-то серьезные проблемы. Например, это может произойти, если в процессе вызова fdwrite было исчерпано свободное пространство на выходном носителе. (Если носитель уже был заполнен до вызова fdwrite, то вызов вернет значение -1.)
Вызов fdwrite часто использует дескриптор файла, полученный при создании нового файла. Легко увидеть, что происходит в этом случае. Изначально файл имеет нулевую длину (он только что создан или получен усечением существующего файла до нулевой длины), и каждый вызов fdwrite просто дописывает данные в конец файла, перемещая указатель чтения-записи на позицию, следующую за последним записанным байтом. Например, в случае удачного завершения фрагмент кода
var
fd:integer;
w1, w2 : longint;
header1: array [0..511] of char;
header2: array [0..1023] of char;
.
.
.
fd := fdopen('newfile', Open_WRONLY or Open_CREAT or Open_EXCL, octal(0644));
if fd = -1 then
exit;
w1 := fdwrite(fd, header1, 512);
w2 := fdwrite(fd, header2, 1024);
дает в результате файл длиной 1536 байт, с содержимым массивов header1 и header2.
Что произойдет, если программа откроет существующий файл на запись и сразу же запишет в него что-нибудь? Ответ очень прост: старые данные в файле будут заменены новыми, символ за символом. Например, предположим, что файл oldhat имеет длину 500 символов. Если программа откроет файл oldhat для записи и выведет в него 10 символов, то первые 10 символов в файле будут заменены содержимым буфера записи программы. Следующий вызов fdwrite заменит очередные 10 символов и так далее. После достижения конца исходного файла в процессе дальнейших вызовов fdwrite его длина будет увеличиваться. Если нужно избежать переписывания файла, можно открыть файл с флагом Open_APPEND. Например:
filedes := fdopen(filename, Open_WRONLY or Open_APPEND);
Теперь в случае успешного вызова fdopen указатель чтения-записи будет помещен, сразу же за последним байтом в файле, и вызов fdwrite будет добавлять данные в конец файла. Этот прием более подробно будет объяснен в разделе 2.1.12.
Программа, демонстрирующая fdOpen, fdwrite и fdCLose.
Uses linux;
Const Line : String[80] = 'This is easy writing !';
Var FD : Longint;
begin
FD:=fdOpen ('Test.dat',Open_WrOnly or Open_Creat);
if FD>0 then
begin
if length(Line)<>fdwrite (FD,Line[1],Length(Line)) then
Writeln ('Error when writing to file !');
fdClose(FD);
end;
end.
uses
uses linux;
Function fdFlush(fd:Longint):boolean;
это дескриптор открытого файла. Второй
uses linux;
Function fdSeek(filedes, offset, SeekType:longint):longint;
Первый параметр, filedes, – это дескриптор открытого файла. Второй параметр, offset, обычно определяет новое положение указателя чтения-записи и задает число байтов, которое нужно добавить к начальному положению указателя. Третий целочисленный параметр, SeekType, определяет, что принимается в качестве начального положения, то есть откуда вычисляется смещение offset. Флаг SeekType может принимать одно из символьных значений (определенных в модуле linux), как показано ниже:
SEEK_SET |
Смещение offset вычисляется от начала файла, обычно имеет значение = 0 |
SEEK_CUR |
Смещение offset вычисляется от текущего положения в файле, обычное значение = 1 |
SEEK_END |
Смещение offset вычисляется от конца файла, обычное значение = 2 |
Эти значения показаны в графическом виде на рис. 2.1, на котором представлен файл из 7 байт.
< |
SEEK_SET |
|||
a |
||||
Текущее |
b |
|||
положение |
> |
c |
< |
SEEK_CUR |
указателя |
d |
|||
файла |
e |
|||
f |
||||
g |
< |
SEEK_END |
||
Рис. 2.1. Символьные значения флага SeekType
Пример использования вызова fdseek:
var
newpos:longint;
.
.
.
newpos := fdseek(fd, -16, SEEK_END);
который задает положение указателя в 16 байтах от конца файла.
Во всех случаях возвращаемое значение (содержащееся в переменной newpos в примере) дает новое положение в файле. В случае ошибки оно будет содержать стандартный код ошибки -1.
Существует ряд моментов, которые следует отметить. Во-первых, обе переменные newpos и offset имеют тип longint, и должны вмещать смещение для любого файла в системе. Во-вторых, как показано в примере, смещение offset может быть отрицательным. Другими словами, возможно перемещение в обратную сторону от начального положения, заданного флагом SeekType. Ошибка возникнет только при попытке переместиться при этом на позицию, находящуюся до начала файла. В-третьих, можно задать позицию за концом файла. В этом случае, очевидно, не существует данных, которые можно было бы прочитать – невозможно предугадать будущие записи в этот участок (UNIX не имеет машины времени) – но последующий вызов fdwrite имеет смысл и приведет к увеличению размера файла. Пустое пространство между старым концом файла и начальным положением новых данных не обязательно выделяется физически, но для последующих вызовов fdread оно будет выглядеть как заполненное символами null ASCII.
В качестве простого примера мы можем создать фрагмент программы, который будет дописывать данные в конец существующего файла, открывая файл, перемещаясь на его конец при помощи вызова fdseek и начиная запись:
filedes := fdopen(filename, Open_RDWR);
fdseek(filedes, 0, SEEK_END);
fdwrite(filedes, outbuf, OBSIZE);
Здесь параметр направления поиска для вызова fdseek установлен равным SEEK_END для перемещения в конец файла. Так как перемещаться дальше нам не нужно, то смещение задано равным нулю.
Вызов fdseek также может использоваться для получения размера файла, так как он возвращает новое положение в файле.
var
filesize:longint;
filedes:integer;
.
.
.
filesize := fdseek(filedes, 0, SEEK_END);
Упражнение 2.8. Напишите функцию, которая использует вызов fdseek для получения размера открытого файла, не изменяя при этом значения указателя чтения-записи.
Вызов имеет единственный аргумент
uses linux;
Function UnLink(Var Path): Boolean;
Вызов имеет единственный аргумент – строку с именем удаляемого файла, например:
unlink('/tmp/usedfile');
Вызов возвращает true в случае успешного завершения и false – в случае ошибки.
тип последнего параметра может меняться
uses linux;
/* Примечание: тип последнего параметра может меняться */
Function Fcntl(filedes:longint;Cmd:Integer):integer;
Function Fcntl(var filedes:Text;Cmd:Integer):integer;
Procedure Fcntl(Fd:text;Cmd:Integer;Arg:longint);
Procedure Fcntl(Fd:longint;Cmd:longint;Arg:Longint);
Системный вызов fcntl работает с открытым файлом, заданным дескриптором файла filedes. Конкретная выполняемая функция задается выбором одного из значений параметра cmd из модуля linux. Тип третьего параметра зависит от значения параметра cmd. Например, если вызов fcntl используется для установки флагов статуса файла, тогда третий параметр будет целым числом. Если же, как можно будет увидеть позже, вызов fcntl будет использоваться для блокировки файла, то третий параметр будет указателем на структуру lock. Иногда третий параметр вообще не используется.
Некоторые из этих функций относятся к взаимодействию файлов и процессов, и мы не будем рассматривать их здесь; тем не менее две из этих функций, заданные значениями F_GETFL и F_SETFL параметра cmd, представляют для нас сейчас интерес.
При задании параметра F_GETFL вызов fcntl возвращает текущие флаги статуса файла, установленные вызовом fdopen. Следующая функция filestatus использует fcntl для вывода текущего статуса открытого файла.
(*
* Функция filestatus описывает текущий статус файла
*)
uses linux;
function filestatus(filedes:integer):integer;
var
arg1:integer;
begin
arg1 := fcntl (filedes, F_GETFL);
if arg1 = -1 then
begin
writeln('Ошибка чтения статуса файла');
filestatus := -1;
exit;
end;
write('Дескриптор файла ', filedes, ': ');
(*
* Сравнить аргумент с флагами открытия файла.
*)
case (arg1 and Open_ACCMODE) of
Open_WRONLY:
write('Только для записи');
Open_RDWR:
write('Для чтения-записи');
Open_RDONLY:
write('Только для чтения');
else
write('Режим не существует');
end;
if (arg1 and Open_APPEND)<>0 then
write (' - установлен флаг append');
writeln;
filestatus := 0;
end;
Следует обратить внимание на проверку установки определенного бита во флаги статуса файла в переменной arg1 при помощи побитового оператора И, обозначаемого AND. Поле интересующих нас битов вырезается с помощью специальной маски Open_ACCMODE, определенной в модуле linux. Дальнейшие действия осуществляются с учетом того, что в данном поле не может быть выставлено более одного бита, поскольку эти три режима доступа к файлу не совместимы.
Значение F_SETFL используется для переустановки связанных с файлом флагов статуса. Новые флаги задаются в третьем аргументе вызова fcntl. При этом могут быть установлены только некоторые флаги, например, нельзя вдруг превратить файл, открытый только для чтения, в файл, открытый для чтения и записи. Тем не менее с помощью F_SETFL можно задать режим, при котором все следующие операции записи будут только дописывать информацию в конец файла:
if (fcntl(filedes, F_SETFL, Open_APPEND) = -1) then
writeln('Ошибка вызова fcntl');
в цикл для копирования одного
uses stdio;
(* Считать символ из __stream *)
function getc(__stream:pfile):integer;
(* Поместить символ в __stream *)
function putc(__c:integer;__stream:pfile):integer;
Можно поместить обе процедуры в цикл для копирования одного файла в другой:
var
с:integer;
istream, ostream:PFILE;
(* Открыть файл istream для чтения и файл ostream для записи *)
.
.
.
с := getc (istream);
while c<>-1 do
begin
putc (с, ostream);
с := getc (istream);
end;
Значение -1
возвращается функцией getc при достижении конца файла, поэтому тип возвращаемого функцией getc значения определен как integer.
На первый взгляд, функции getc и putc могут вызывать определенное беспокойство, поскольку они работают с одиночными символами, а это, как уже было продемонстрировано на примере системных вызовов, чрезвычайно неэффективно. Процедуры стандартного ввода/вывода избегают этой неэффективности при помощи изящного механизма буферизации, который работает следующим образом: первый вызов функции getc приводит к чтению из файла BUFSIZ символов при помощи системного вызова fdread. Данные находятся в буфере, созданном библиотекой и находящемся в пользовательском адресном пространстве. Функция getc возвращает только первый символ. Все остальные внутренние действия скрыты от вызывающей программы. Последующие вызовы функции getc поочередно возвращают символы из буфера. После того как при помощи функции getc программе будут переданы BUFSIZ символов и будет выполнен очередной вызов getc, из файла снова будет считан буфер целиком. Аналогичный механизм реализован в функции putc.
Такой подход весьма удобен, так как он освобождает программиста от беспокойства по поводу эффективности работы программы. Это также означает, что данные записываются большими блоками, и запись в файл будет производиться с задержкой (для терминалов сделаны специальные оговорки). Поэтому весьма неблагоразумно использовать для одного и того же файла и стандартные процедуры ввода/вывода, и системные вызовы, такие как fdread, fdwrite или fdseek. Это может привести к хаосу, если не представлять четко, что при этом происходит. С другой стороны, вполне допустимо смешивать системные вызовы и процедуры стандартного ввода/вывода для разных файлов.
Кроме механизма буферизации стандартный ввод/вывод предоставляет утилиты для форматирования и преобразования, например, функция printf обеспечивает форматированный вывод:
printf('Целое число %d'#10, [ival]);
Кстати, функция printf неявно осуществляет запись в стандартный вывод.
Восьмеричные значения для прав доступа
uses linux;
Function Octal(l:longint):longint;
Таблица 3.1. Восьмеричные значения для прав доступа к файлам
Восьмеричное значение |
Символьное обозначение |
Значение |
0400 |
STAT_IRUSR |
Владелец имеет доступ для чтения |
0200 |
STAT_IWUSR |
Владелец имеет доступ для записи |
0100 |
STAT_IXUSR |
Владелец может выполнять файл |
0040 |
STAT_IRGRP |
Группа имеет доступ для чтения |
0020 |
STAT_IWGRP |
Группа имеет доступ для записи |
0010 |
STAT_IXGRP |
Группа может выполнять файл |
0004 |
STAT_IROTH |
Другие пользователи имеют доступ для чтения |
0002 |
STAT_IWOTH |
Другие пользователи имеют доступ для записи |
0001 |
STAT_IXOTH |
Другие пользователи могут выполнять файл |
Из таблицы легко увидеть, что можно сделать файл доступным для чтения всем типам пользователей, сложив 0400 (доступ на чтение для владельца), 040 (доступ на чтение для членов группы файла) и 04
(доступ на чтение для всех остальных пользователей). В итоге это дает код доступа к файлу 0444. Такой код может быть получен и при помощи побитовой операции ИЛИ (or) для соответствующих символьных представлений; например, 0444 эквивалентен выражению:
STAT_IRUSR or STAT_IRGRP or STAT_IROTH
Поскольку все остальные значения из таблицы не включены, код доступа 0444 также означает, что никто из пользователей, включая владельца файла, не может получить доступ к файлу на запись или выполнение.
Чтобы устранить это неудобство, можно использовать более одного восьмеричного значения, относящегося к одной категории пользователей. Например, сложив 0400, 0200 и 0100, получим в сумме значение 0700, которое показывает, что владелец файла может читать его, производить в него запись и запускать файл на выполнение.
Поэтому чаще встречается значение кода доступа:
0700 + 050 + 05 = 0755
Это означает, что владелец файла может читать и писать в файл или запускать файл на выполнение, в то время как права членов группы, связанной с файлом, и все остальных пользователей ограничены только чтением или выполнением файла.
Легко понять, почему программисты UNIX предпочитают использовать восьмеричные постоянные, а не имена констант из модуля linux, когда просто значение 0755 представляется выражением:
STAT_IRUSR or STAT_IWUSR or STAT_IXUSR or STAT_IRGRP or STAT_IXGRP or STAT_IROTH or STAT_IXOTH
Рассказ о правах доступа еще не закончен. В следующем подразделе будет продемонстрировано, как три других типа прав доступа влияют на файлы, содержащие исполняемые программы. В отношении доступа к файлам важно то, что каждый каталог UNIX, почти как обычный файл, имеет набор прав доступа, которые влияют на доступность файлов в каталоге. Этот вопрос будет подробно рассмотрен в главе 4.
Упражнение 3.1. Что означают следующие значения прав доступа: 0761, 0777, 0555, 0007 и 0707?
Упражнение 3.2. Замените восьмеричные значения из упражнения 3.1 эквивалентными символьными выражениями.
Упражнение 3.3. Напишите процедуру lsoct, которая переводит набор прав доступа из формы, получаемой на выходе команды ls
(например, rwxr-xr-x) в эквивалентные восьмеричные значения. Затем напишите обратную процедуру octls.
запрещает присваивание файлу прав доступа
uses linux;
Function Umask(Mask:Integer):Integer;
Например:
var
oldmask:integer;
.
.
.
oldmask := umask(octal(022));
Значение octal(022) запрещает присваивание файлу прав доступа на запись всем, кроме владельца файла. После вызова в переменную oldmask будет помещено предыдущее значение маски.
Поэтому, если вы хотите быть абсолютно уверены, что файлы создаются именно с кодами доступа, заданными в вызовах fdcreat или fdopen, вам следует вначале вызвать umask с нулевым аргументом. Так как все биты в маске создания файла будут равны нулю, ни один из битов в коде доступа, передаваемом вызовам fdopen или fdcreat, не будет сброшен. В следующем примере этот подход используется для создания файла с заданным кодом доступа, а затем восстанавливается старая маска создания файла. Программа возвращает дескриптор файла, полученный в результате вызова fdopen.
uses linux,stdio;
function specialcreat(pathname:string;mode:longint):integer;
var
oldu,filedes:integer;
begin
(* Установить маску создания файла равной нулю *)
oldu:=umask(0);
if oldu = -1 then
begin
perror('Ошибка сохранения старой маски');
specialcreat:=-1;
exit;
end;
(* Создать файл *)
filedes:=fdopen(pathname, Open_WRONLY or Open_CREAT or Open_EXCL, mode);
if (filedes = -1) then
perror ('Ошибка открытия файла');
(* Восстановить прежний режим доступа к файлу *)
if (umask (oldu) = -1) then
perror ('Ошибка восстановления старой маски');
(* Вернуть дескриптор файла *)
specialcreat:=filedes;
end;
Как мы уже видели, существует
uses linux;
Function Access(PathName:Pathstr; AMode:integer):Boolean;
Как мы уже видели, существует несколько режимов доступа к файлу, поэтому параметр amode содержит значение, указывающее на интересующий нас метод доступа. Параметр amode может принимать следующие значения, определенные в модуле linux:
R_OK – имеет ли вызывающий процесс доступ на чтение;
W_ОК – имеет ли вызывающий процесс доступ на запись;
Х_ОК – может ли вызывающий процесс выполнить файл.
Аргумент amode не конкретизирует, к какой категории пользователей относится вопрос, так как вызов access сообщает права доступа к файлу конкретного пользователя, имеющего ruid и rgid текущего процесса. Переменная amode также может принимать значение F_OK, в этом случае проверяется лишь существование файла. Как обычно, параметр pathname задает имя файла.
Значение, возвращаемое вызовом access, либо равно нулю (доступ разрешен) или -1 (доступ не разрешен). В последнем случае переменная linuxerror будет содержать значение кода ошибки. Значение Sys_EACCES, например, означает, что запрошенный режим доступа к файлу не разрешен, а значение Sys_ENOENT показывает, что указанного файла просто не существует.
Следующий пример программы использует вызов access для проверки, разрешено ли пользователю чтение файла при любом значении бита STAT_ISUID исполняемого файла этой программы:
(* Пример использования вызова access *)
uses linux,stdio;
const
filename = 'afile';
begin
if not access (filename, R_OK) then
begin
writeln(stderr, 'Пользователь не имеет доступа на чтение к файлу ', filename);
halt(1);
end;
writeln(filename, ' доступен для чтения, продолжаем');
(* Остальная программа *)
end.
Упражнение 3.6. Напишите программу whatable, которая будет сообщать, можете ли вы выполнять чтение, запись или выполнение заданного файла. Если доступ невозможен, программа whatable должна сообщать почему (используйте коды, ошибок, возвращаемых в переменной linuxerror).
uses linux;
Function Chmod(PathName:Pathstr; NewMode:Longint):Boolean;
Для изменения прав доступа к существующему файлу применяется системный вызов chmod. Вызов разрешен владельцу файла или суперпользователю.
Параметр pathname указывает имя файла. Параметр newmode содержит новый код доступа файла, образованный описанным в первой части главы способом.
Пример использования вызова chmod:
if not chmod(pathname, octal(0644)) then
perror('Ошибка вызова chmod');
Упражнение 3.7. Напишите программу setperm, которая имеет два аргумента командной строки. Первый – имя файла, второй – набор прав доступа в восьмеричной форме или в форме, выводимой команда ls. Если файл существует, то программа setperm должна попытаться поменять права доступа к файлу на заданные. Используйте процедуру lsoct, которую вы разработали в упражнении 3.3.
uses linux;
Function Chown(PathName:Pathstr; Owner_id,Group_id:Longint):Boolean;
Например:
Uses linux;
Var UID,GID : Longint;
F : Text;
begin
Writeln (' This will only work if you are root.');
Write ('Enter a UID : ');readln(UID);
Write ('Enter a GID : ');readln(GID);
Assign (f,'test.txt');
Rewrite (f);
Writeln (f,'The owner of this file should become : ');
Writeln (f,'UID : ',UID);
Writeln (f,'GID : ',GID);
Close (F);
if not Chown ('test.txt',UID,GID) then
if LinuxError=Sys_EPERM then
Writeln ('You are not root !')
else
Writeln ('Chmod failed with exit code : ',LinuxError)
else
Writeln ('Changed owner successfully !');
end.
Вызов имеет три аргумента: pathname, указывающий имя файла, owner_id, задающий нового владельца, и group_id, задающий новую группу. Возвращаемое значение retval равно true в случае успеха и false – в случае ошибки.
В системе, удовлетворяющей спецификации XSI, вызывающий процесс должен быть процессом суперпользователя или владельца файла (точнее, действующий идентификатор вызывающего процесса должен либо совпадать с идентификатором владельца файла, либо быть равным 0). При несанкционированной попытке изменить владельца файла выставляется код ошибки Sys_EPERM.
Поскольку вызов chown разрешен текущему владельцу файла, обычный пользователь может передать свой файл другому пользователю. При этом пользователь не сможет впоследствии отменить это действие, так как идентификатор пользователя уже не будет совпадать с идентификатором пользователя файла. Следует также обратить внимание на то, что при смене владельца файла в целях предотвращения неправомерного использования вызова chown для получения системных полномочий, сбрасываются права доступа set-user-id и set-group-id. (Что могло бы произойти, если бы это было не так?)
Первый параметр, original_path, является указателем
uses linux;
Function Link(original_path, New_Path:pathstr):Boolean;
Первый параметр, original_path, является указателем на массив символов, содержащий полное имя файла в системе UNIX. Он должен задавать существующую ссылку на файл, то есть фактическое имя файла. Второй параметр, new_path, задает новое имя файла или ссылку на файл, но файл, заданный параметром new_path, еще не должен существовать.
Системный вызов link возвращает значение true в случае успешного завершения и false – в случае ошибки. В последнем случае новая ссылка на файл не будет создана.
Например, оператор
link('/usr/keith/chap.2', '/usr/ben/2.chap');
создаст новую ссылку /usr/ben/2.chap на существующий файл /usr/keith/chap.2. Теперь к файлу можно будет обратиться, используя любое из имен. Пример показывает, что ссылка не обязательно должна находиться в одном каталоге с файлом, на который она указывает.
заданный аргументом oldpathname, получает новое
uses linux;
Function Frename(oldpathname, newpathname:Pchar):Boolean;
Function Frename(oldpathname, newpathname:String):Boolean;
Файл, заданный аргументом oldpathname, получает новое имя, заданное вторым параметром newpathname. Если файл с именем newpathname уже существует, то перед переименованием файла oldpathname он удаляется.
Упражнение 3.8. Напишите свою версию команды rm, используя вызов unlink. Ваша программа должна проверять, имеет ли пользователь право записи в файл при помощи вызова access и в случае его отсутствия запрашивать подтверждение перед попыткой удаления ссылки на файл. (Почему?) Будьте осторожны при тестировании программы!
После завершения вызова symlink создается
uses linux;
Function SymLink(realname, symname:pathstr):Boolean;
После завершения вызова symlink создается файл symname, указывающий на файл realname. Если возникает ошибка, например, если файл с именем symname уже существует, то вызов symlink возвращает значение false. В случае успеха вызов возвращает истинное значение.
Пример использования SymLink:
Uses linux;
Var F : Text;
S : String;
begin
Assign (F,'test.txt');
Rewrite (F);
Writeln (F,'This is written to test.txt');
Close(f);
{ new.txt and test.txt are now the same file }
if not SymLink ('test.txt','new.txt') then
writeln ('Error when symlinking !');
{ Removing test.txt still leaves new.txt
Pointing now to a non-existent file ! }
If not Unlink ('test.txt') then
Writeln ('Error when unlinking !');
Assign (f,'new.txt');
{ This should fail, since the symbolic link
points to a non-existent file! }
{$i-}
Reset (F);
{$i+}
If IOResult=0 then
Writeln ('This shouldn''t happen');
{ Now remove new.txt also }
If not Unlink ('new.txt') then
Writeln ('Error when unlinking !');
end.
Если файл символьной ссылки открывается при помощи fdopen, то системный вызов fdopen корректно прослеживает путь к файлу realname. Если необходимо считать данные из самого файла symname, то нужно использовать системный вызов readlink.
uses linux;
Function ReadLink(sympath, buffer:pchar; bufsize:longint):longint;
Function ReadLink(name:pathstr):pathstr;
Системный вызов readlink вначале открывает файл sympath, затем читает его содержимое в переменную buffer, и, наконец, закрывает файл sympath. К сожалению, спецификация XSI не гарантирует, что строка в переменной buffer будет заканчиваться нулевым символом. Возвращаемое вызовом readlink значение равно числу символов в буфере или -1 – в случае ошибки.
Следует сделать предупреждение, касающееся использования и прослеживания символьных ссылок. Если файл, на который указывает символьная ссылка, удаляется, то при попытке доступа к файлу при помощи символьной ссылки выдается ошибка, которая может ввести вас в заблуждение. Программа все еще сможет «видеть» символьную ссылку, но, к сожалению, вызов fdopen не сможет проследовать по указанному в ссылке пути и вернет ошибку, установив значение переменной linuxerror равным Sys_EEXIST.
BaseName выделяет из полного пути
uses linux;
Function BaseName(Const Path;Const Suf:Pathstr):Pathstr;
Function DirName(Const Path:Pathstr):Pathstr;
Procedure FSplit(const Path:PathStr; var Dir:DirStr; Var Name:NameStr;
Var Ext:ExtStr);
BaseName выделяет из полного пути Path имя файла, обрезая окончание Suf, если оно существует. Для каталогов завершающий слэш всегда убирается (за исключением корневого каталога).
DirName возвращает часть пути, соответствующую имени каталога. Это будет часть параметра Path до завершающего слэша, или ничего в его отсутствие.
FSplit разбивает полное имя файла на 3 части: путь Path, имя Name и расширение ext. Расширением считаются все символы, следующие за последней точкой.
Например:
uses Linux;
var
Path,Name,Ext : string;
begin
FSplit(ParamStr(1),Path,Name,Ext);
WriteLn('Split ',ParamStr(1),' in:');
WriteLn('Path : ',Path);
WriteLn('Name : ',Name);
WriteLn('Extension: ',Ext);
end.
Системный вызов fstat имеет два
uses linux;
Function FStat(Path:Pathstr;Var Info:tstat):Boolean;
Function FStat(Fd:longint;Var Info:tstat):Boolean;
Function FStat(var F:Text;Var Info:tstat):Boolean;
Function FStat(var F:File;Var Info:tstat):Boolean;
Function LStat(Path:Pathstr; Var Info:tstat):Boolean;
Системный вызов fstat имеет два аргумента: первый из них – path, как обычно, указывает на полное имя файла. Второй аргумент info является ссылкой на структуру tstat (stat). Эта структура после успешного вызова будет содержать связанную с файлом информацию. Вместо имени файла может также использоваться его дескриптор или файловая переменная.
.
.
.
var
s:tstat;
filedes:integer;
retval:boolean;
filedes := fdopen('/tmp/dina', Open_RDWR);
(* Структура s может быть заполнена при помощи вызова ... *)
retval := fstat('/tmp/dina', s);
/* ... или */
retval := fstat(filedes, @s);
Определение структуры tstat находится в модуле linux и включает следующие элементы:
stat=record
dev : word;
pad1 : word;
ino : longint;
mode : word;
nlink : word;
uid : word;
gid : word;
rdev : word;
pad2 : word;
size : longint;
blksze : Longint;
blocks : Longint;
atime : Longint;
unused1 : longint;
mtime : Longint;
unused2 : longint;
ctime : Longint;
unused3 : longint;
unused4 : longint;
unused5 : longint;
end;
Системный вызов lstat получает информацию о символьной ссылке. Например:
uses linux;
var f : text;
i : byte;
info : stat;
begin
{ Make a file }
assign (f,'test.fil');
rewrite (f);
for i:=1 to 10 do writeln (f,'Testline # ',i);
close (f);
{ Do the call on made file. }
if not fstat ('test.fil',info) then
begin
writeln('Fstat failed. Errno : ',linuxerror);
halt (1);
end;
writeln;
writeln ('Result of fstat on file ''test.fil''.');
writeln ('Inode : ',info.ino);
– uid, gid
Идентификаторы пользователя uid и группы gid файла. Первоначально устанавливаются вызовом fdcreat и изменяются системным вызовом chown
– rdev
Этот элемент имеет смысл только в случае использования файла для описания устройства. На него пока можно не обращать внимания.
– size
Текущий логический размер файла в байтах. Нужно понимать, что способ хранения файла определяется реальными параметрами устройства, и поэтому физический размер занимаемого пространства может быть больше, чем логический размер файла. Элемент size изменяется при каждом вызове fdwrite в конце файла.
– atime
Содержит время последнего чтения из файла (хотя первоначальные вызов fdcreat и fdopen устанавливают это значение).
– mtime
Указывает время последней модификации файла – изменяется при каждом вызове fdwrite для файла.
– ctime
Содержит время последнего изменения информации, возвращаемой в структуре stat. Это время изменяется системными вызовами link (меняется элемент nlink), chmod (меняется mode) и fdwrite (меняется mtime и, возможно, size).
– blksize
Содержит размер блока ввода/вывода, зависящий от настроек системы. Для некоторых систем этот параметр может различаться для разных файлов.
– blocks
Содержит число физических блоков, занимаемых определенным файлом.
Системный вызов utime позволяет установить время доступа и модификации файла. Структура utimbuf содержит два поля, actime и modtime, оба типа Longint. Они должны быть заполнены значениями времени в секундах с 1.1.1970 г. относительно последнего времени доступа и последнего времени модификации.
uses linux;
Function Utime(path:pathstr; utim:utimbuf):Boolean;
Например:
Uses linux;
Var utim : utimbuf;
year,month,day,hour,minute,second : Word;
begin
{ Set access and modification time of executable source }
GetTime (hour,minute,second);
GetDate (year,month,day);
utim.actime:=LocalToEpoch(year,month,day,hour,minute,second);
utim.modtime:=utim.actime;
if not Utime('ex25.pp',utim) then
writeln ('Call to UTime failed !')
else
begin
Write ('Set access and modification times to : ');
Write (Hour:2,':',minute:2,':',second,', ');
Writeln (Day:2,'/',month:2,'/',year:4);
end;
end.
Следующий пример – процедура filedata выводит данные, связанные с файлом, определяемым переменной pathname. Пример сообщает размер файла, идентификатор пользователя, группу файла, а также права доступа к файлу.
Чтобы преобразовать права доступа к файлу в удобочитаемую форму, похожую на результат, выводимый командой ls, был использован массив octarray чисел типа integer, содержащий значения для основных прав доступа, и массив символов perms, содержащий символьные эквиваленты прав доступа.
(* Процедура filedata выводит данные о файле *)
uses linux;
(*
* Массив octarray используется для определения
* установки битов прав доступа.
*)
const
octarray:array[0..8] of integer= (
0400, 0200, 0100,
0040, 0020, 0010,
0004, 0002, 0001);
(*
* Мнемонические коды для прав доступа к файлу,
* длиной 10 символов, включая нулевой символ в конце строки.
*)
const
perms:pchar = 'rwxrwxrwx';
function filedata(pathname:string):integer;
var
statbuf:tstat;
descrip:array [0..9] of char;
j:integer;
begin
if not fstat (pathname, statbuf) then
begin
writeln('Ошибка вызова stat для ', pathname);
filedata:=-1;
exit;
end;
(* Преобразовать права доступа в удобочитаемую форму *)
for j:=0 to 8 do
begin
(*
* Проверить, установлены ли права доступа
* при помощи побитового И
*)
if (statbuf.mode and octal(octarray[j]))<>0 then
descrip[j] := perms[j]
else
descrip[j] := '-';
end;
descrip[9] := #0; (* задать строку *)
(* Вывести информацию о файле *)
writeln(#10'Файл ', pathname, ':');
writeln('Размер ',statbuf.size,' байт');
writeln('User-id ',statbuf.uid,', Group-id ',statbuf.gid,#10);
writeln('Права доступа: ', descrip);
filedata:=0;
end;
Более полезным инструментом является следующая программа lookout. Она раз в минуту проверяет, изменился ли какой-либо из файлов из заданного списка, опрашивая время модификации каждого из файлов (mtime). Это утилита, которая предназначена для запуска в качестве фонового процесса.[3]
(* Программа lookout сообщает об изменении файла *)
uses linux, stdio;
const
MFILE=10;
var
sb:tstat;
j:integer;
last_time:array [1..MFILE] of longint;
procedure sleep(t:longint);cdecl;external 'c';
procedure cmp(name:string;last:longint);
begin
(*
* Проверять время изменения файла,
* если можно считать данные о файле.
*)
if not fstat(name,sb) or (sb.mtime <> last) then
begin
writeln('lookout: файл ',name,' изменился');
halt(0);
end;
end;
begin
if (paramcount < 1) then
begin
writeln('Применение: lookout имя_файла ...');
halt(1);
end;
if (paramcount > MFILE) then
begin
writeln('lookout: слишком много имен файлов');
halt (1);
end;
(* Инициализация *)
for j:=1 to paramcount do
begin
if not fstat(paramstr(j), sb) then
begin
writeln ('lookout: ошибка вызова stat для ', paramstr(j));
halt(1);
end;
last_time[j]:=sb.mtime;
end;
(* Повторять до тех пор, пока файл не изменится *)
while true do
begin
for j:=1 to paramcount do
cmp(paramstr(j), last_time[j]);
(*
* Остановиться на 60 секунд.
* Функция 'sleep' стандартная
* библиотечная процедура UNIX.
*)
sleep (60);
end;
end.
Упражнение 3.9. Напишите программу, которая проверяет и записывает изменения размера файла в течение часа. В конце работы она должна строить простую гистограмму, демонстрирующую изменения размера во времени.
Упражнение 3.10. Напишите программу slowwatch, которая периодически проверяет время изменения заданного файла (она не должна завершаться ошибкой, если файл изначально не существует). При изменении файла программа slowwatch должна копировать его на свой стандартный вывод. Как можно убедиться (или предположить), что обновление файла закончено до того, как он будет скопирован?
Первый параметр, pathname, указывает на
uses stdio;
function mkdir(pathname:pchar;mode:integer):integer;
Первый параметр, pathname, указывает на строку символов, содержащую имя создаваемого каталога. Второй параметр, mode, является набором прав доступа к каталогу. Права доступа будут изменяться с учетом значения umask процесса, например:
var
retval:integer;
retval := mkdir('/tmp/dir1', octal(0777));
Как обычно, системный вызов mkdir возвращает нулевое значение в случае успеха, и -1 – в случае неудачи. Обратите внимание, что mkdir также помещает две ссылки (.
и ..) в создаваемый новый каталог. Если бы этих элементов не было, работать с полученным каталогом было бы невозможно.
Если каталог больше не нужен, то его можно удалить при помощи системного вызова rmdir.
uses stdio;
function rmdir(pathname:pchar):integer;
Параметр pathname определяет путь к удаляемому каталогу. Этот вызов завершается успехом, только если удаляемый каталог пуст, то есть содержит только записи «точка» (.) и «двойная точка (..).
Передаваемый вызову opendir параметр является
uses linux;
Function OpenDir(dirname:pchar):pdir;
Function OpenDir(dirname:string):pdir;
Передаваемый вызову opendir параметр является именем открываемого каталога. При успешном открытии каталога dirname вызов opendir возвращает указатель на переменную типа TDIR. Определение типа TDIR, представляющего дескриптор открытого каталога, находится в модуле linux. Это определение аналогично определению типа TFILE, используемого в стандартной библиотеке ввода/вывода, описанной в главах 2 и 11. Указатель позиции ввода/вывода в полученном от функции opendir дескрипторе установлен на первую запись каталога. Если вызов завершился неудачно, то функция возвращает nil. Всегда следует проверять возвращаемое значение, прежде чем это значение может быть использовано.
После того, как программа закончит работу с каталогом, она должна закрыть его. Это можно сделать при помощи функции closedir.
uses linux;
Function CloseDir(dirptr:pdir):integer;
Функция closedir закрывает дескриптор открытого каталога, на который указывает аргумент dirptr. Обычно его значение является результатом предшествующего вызова opendir, что демонстрирует следующий пример:
uses linux;
var
dp:pdir;
begin
dp := opendir ('/tmp/dir1');
if dp = nil then
begin
writeln('Ошибка открытия каталога /tmp/dir1');
halt(1);
end;
(*
Код, работающий с каталогом
.
.
.
*)
closedir (dp);
end.
Функции readdir должен передаваться допустимый
uses linux;
Function ReadDir(dirptr:pdir):pdirent;
Функции readdir должен передаваться допустимый указатель на дескриптор открытого каталога, обычно возвращаемый предшествующим вызовом opendir. При первом вызове readdir в структуру dirent будет считана первая запись в каталоге. В результате успешного вызова указатель каталога переместится на следующую запись.
Когда в результате последующих вызовов readdir достигнет конца каталога, то вызов вернет нулевой указатель. Если в какой-то момент потребуется начать чтение каталога с начала, то можно использовать системный вызов rewinddir, определенный следующим образом:
uses stdio;
procedure rewinddir(dirptr:pdir);
Следующий после вызова rewinddir вызов readdir вернет первую запись в каталоге, на который указывает переменная dirptr.
В приведенном ниже примере функция my_double_ls дважды выведет на экран имена всех файлов в заданном каталоге. Она принимает в качестве параметра имя каталога и в случае ошибки возвращает значение -1.
uses linux,stdio;
function my_double_ls(name:pchar):integer;
var
dp:PDIR;
d:pdirent;
begin
(* Открытие каталога с проверкой ошибок *)
dp:=opendir (name);
if dp=nil then
begin
my_double_ls:=-1;
exit;
end;
(* Продолжить обход каталога,
* выводя записи в нем, если
* индекс остается допустимым
*)
d:=readdir(dp);
while d<>nil do
begin
if d^.ino<>0 then
writeln(d^.name);
d:=readdir(dp);
end;
(* Вернуться к началу каталога ... *)
rewinddir(dp);
(* ... и снова вывести его содержимое *)
d:=readdir(dp);
while d<>nil do
begin
if d^.ino<>0 then
writeln(d^.name);
d:=readdir(dp);
end;
closedir(dp);
my_double_ls:=0;
end;
Порядок выводимых функцией my_double_ls имен файлов будет совпадать с порядком расположения файлов в каталоге. Если вызвать функцию my_double_ls в каталоге, содержащем три файла abc, bookmark и fred, то ее вывод может выглядеть так:
.
..
fred
bookmark
abc
.
..
fred
bookmark
abc
После выполнения системного вызова chdir
uses stdio;
function chdir(path:pchar):integer;
После выполнения системного вызова chdir каталог path становится текущим рабочим каталогом вызывающего процесса. Важно отметить, что эти изменения относятся только к процессу, который выполняет вызов chdir. Смена текущего каталога в программе не затрагивает запустивший программу командный интерпретатор, поэтому после выхода из программы пользователь окажется в том же рабочем каталоге, в котором он находился перед запуском программы, независимо от перемещений программы.
Системный вызов chdir завершится неудачей и вернет значение -1, если путь path не является корректным именем каталога или если вызывающий процесс не имеет доступ на выполнение (прохождение) для всех каталогов в пути.
Системный вызов может успешно использоваться, если нужно получить доступ к нескольким файлам в заданном каталоге. Смена каталога и задание имен файлов относительно нового каталога будет более эффективной, чем использование абсолютных имен файлов. Это связано с тем, что системе приходится поочередно проверять все каталоги в пути, пока не будет найдено искомое имя файла, поэтому уменьшение числа составляющих в пути файла сэкономит время. Например, вместо использования следующего фрагмента программы
fd1 := fdopen('/usr/ben/abc', Open_RDONLY);
fd2 := fdopen('/usr/ben/xyz', Open_RDWR);
можно использовать:
chdir('/usr/ben');
fd1 := fdopen('abc', Open_RDONLY);
fd2 := fdopen('xyz', Open_RDWR);
Функция getcwd возвращает указатель на
uses stdio;
function getcwd(name:pchar; size:longint):pchar;
uses linux;
Function TellDir(p:pdir):longint;
Функция getcwd возвращает указатель на имя текущего каталога. Следует помнить, что значение второго аргумента size должно быть больше длины имени возвращаемого пути не менее чем на единицу. В случае успеха имя текущего каталога копируется в массив, на который указывает переменная name. Если значений size равно нулю или меньше значения, необходимого для возвращения строки имени текущего каталога, то вызов завершится неудачей и вернет нулевой указатель. В некоторых реализациях, если переменная name содержит нулевой указатель, то функция getcwd сама запросит size байтов оперативной памяти; тем не менее, так как эта семантика зависит от системы, не рекомендуется вызывать функцию getcwd с нулевым указателем.
Функция TellDir помещает текущий каталог по указателю p, возвращая 0 в случае успешного завершения и -1 – при ошибке.
Альтернативой getcwd является определенная в модуле sysutils функция GetCurrentDir.
uses sysutils;
Function GetCurrentDir:String;
Эта короткая программа имитирует команду pwd:
(* Программа my_pwd - вывод рабочего каталога *)
uses sysutils;
procedure my_pwd;
begin
writeln(GetCurrentDir);
end;
begin
my_pwd;
end.
При невозможности выполнения fstat, сообщаем
uses linux,stdio,strings;
const
FTW_NS =100; (* При ошибке stat(2) *)
FTW_DNR=200; (* При ошибке opendir(3) *)
FTW_F =300; (* Обычный файл *)
FTW_D =400; (* Каталог *)
MAXNAMLEN=4000;
(* Удобное сокращение *)
function EQ(a,b:pchar):boolean;
begin
EQ:=(strcomp(a, b) = 0);
end;
type
func=function(name:pchar; var status:tstat; _type:integer):integer;
function ftw(directory:pchar; funcptr:func; depth:integer):integer;
var
dp:pdir;
p,fullpath:pchar;
i:integer;
e:pdirent;
sb:tstat;
seekpoint:longint;
begin
(* При невозможности выполнения fstat, сообщаем пользователю об этом *)
if not fstat(directory, Sb) then
begin
ftw:=funcptr(directory, Sb, FTW_NS);
exit;
end;
(* Если не каталог, вызываем пользовательскую функцию. *)
if ((Sb.mode and STAT_IFMT) <> STAT_IFDIR) then
(* Сообщение "FTW_F" может быть некорректным (вдруг это символическая ссылка? *)
begin
ftw:=funcptr(directory, Sb, FTW_F);
exit;
end;
(* Открываем каталог; при невозможности - сообщаем пользователю. *)
Dp := opendir(directory);
if dp = nil then
begin
ftw:=funcptr(directory, Sb, FTW_DNR);
exit;
end;
(* Определяем, желает ли пользователь продолжать. *)
i := funcptr(directory, Sb, FTW_D);
if i <> 0 then
begin
closedir(Dp);
ftw:=i;
exit;
end;
(* Готовим место для хранения поного пути. *)
i := strlen(directory);
fullpath := stralloc(i + 1 + MAXNAMLEN + 1);
if fullpath = nil then
begin
closedir(Dp);
ftw:=-1;
exit;
end;
strcopy(fullpath, directory);
p := @fullpath[i];
if (i<>0) and (p[-1] <> '/') then
begin
p^:='/';
inc(p);
end;
(* Читаем все элементы каталога. *)
E := readdir(Dp);
while E <> nil do
begin
if not EQ(E^.name, '.') and not EQ(E^.name, '..') then
function func (name:pchar; var status:tstat; _type:integer):integer;
begin
(* Тело функции *)
end;
Целочисленный аргумент _type может принимать одно из нескольких возможных значений, описывающих тип встретившегося объекта. Вот эти значения:
FTW_F |
Объект является файлом |
FTW_D |
Объект является каталогом |
FTW_DNR |
Объект является каталогом, который нельзя прочесть |
FTW_SL |
Объект является символьной ссылкой |
FTW_NS |
Объект не является символьной ссылкой, и для него нельзя успешно выполнить вызов fstat |
Если объект является каталогом, который нельзя прочесть (_type = FTW_DNR), то его потомки не будут обрабатываться. Если нельзя успешно выполнить функцию fstat (_type = FTW_NS), то передаваемая для объекта структура tstat будет иметь неопределенные значения.
Работа вызова будет продолжаться до тех пор, пока не будет завершен обход дерева или не возникнет ошибка внутри функции ftw. Обход также закончится, если определенная пользователем функция возвратит ненулевое значение. Тогда функция ftw прекратит работу и вернет значение, возвращенное функций пользователя. Ошибки внутри функции ftw приведут к возврату значения -1, тогда в переменной linuxerror будет выставлен соответствующий код ошибки.
Следующий пример использует функцию ftw для обхода поддерева каталогов, выводящего имена всех встретившихся файлов (каталогов) и права доступа к ним. Каталоги и символьные ссылки при выводе будут обозначаться дополнительной звездочкой.
Сначала рассмотрим функцию list, которая будет передаваться в качестве аргумента функции ftw.
function list(name:pchar; var status:tstat; _type:integer):integer;
begin
(* Если вызов stat завершился неудачей, просто вернуться *)
if (_type = FTW_NS) then
begin
list:=0;
exit;
end;
(*
* Иначе, вывести имя объекта,
* права доступа к нему и постфикс "*",
* если объект является каталогом или символьной ссылкой.
*)
if (_type = FTW_F) then
printf ('%-30s'#9'0%3o'#$a, [name, status.mode and octal(0777)])
uses linux;
Function FNMatch(const Pattern, Name:string):Boolean;
Function FSearch(Path:pathstr; DirList:string):Pathstr;
Function Glob(Const Path:Pathstr):PGlob;
Procedure GlobFree(Var P:Pglob);
FNMatch возвращает True, если имя файла в Name совпадает с шаблоном в Pattern. Шаблон может содержать знаки *
(совпадение с нулем или более символов) или ?
(совпадение с одиночными символом).
FSearch ищет в DirList, списке каталогов, разделенных двоеточием, файл, указанный в Path, возвращаю путь к найденному файлу или пустую строку.
Glob возвращает указатель на структуру tglob, содержащую имена всех файлов, отвечающих шаблону в Path. Возвращает nil при ошибке, устанавливая LinuxError.
GlobFree освобождает память, занятую структурой tglob.
Например:
Uses linux;
Var G1,G2 : PGlob;
begin
G1:=Glob ('*');
if LinuxError=0 then
begin
G2:=G1;
Writeln ('Files in this directory : ');
While g2<>Nil do
begin
Writeln (g2^.name);
g2:=g2^.next;
end;
GlobFree (g1);
end;
end.
Важное отличие между этими двумя
uses stdio;
procedure sync;
function fsync(filedes:integer):integer;
Важное отличие между этими двумя вызовами состоит в том, что вызов fsync не завершается до тех пор, пока все данные не будут записаны на диск. Вызов sync может завершиться, но запись данных при этом может быть не завершена, а только занесена в планировщик (более того, в некоторых реализациях вызов sync может быть ненужным и не иметь эффекта).
Функция sync не возвращает значения. Функция fsync будет возвращать нулевое значение в случае успеха и -1 – в случае ошибки. Вызов fsync может завершиться неудачей, если, например, переменная filedes содержит некорректный дескриптор файла.
Чтобы убедиться, что содержимое файловых систем на диске не слишком надолго отстает от времени, в системе UNIX регулярно производится вызов sync. Обычно период запуска sync равен 30 секундам, хотя этот параметр может изменяться системным администратором.
о файловой системе, заданной либо
uses linux;
Function FSStat(Path:Pathstr; Var buf:statfs):Boolean;
Function FSStat(Fd:longint; Var buf:stat):Boolean;
Обе функции возвращают информацию о файловой системе, заданной либо именем файла устройства path, либо дескриптором открытого файла fd. Параметр buf является указателем на структуру statfs, определенную модуле linux. Структура statfs включает, по меньшей мере, следующие элементы:
bsize:longint; |
Размер блока данных, при котором система имеет наибольшую производительность. Например, значение bsize может составлять при этом 8 Кбайт, что означает, что система обеспечивает более эффективный ввод/вывод при операциях с такими порциями данных |
bfree:longint; |
Полное число свободных блоков |
bavail:longint; |
Число свободных блоков, доступных непривилегированным процессам |
files:longint; |
Полное число номеров индексных дескрипторов |
ffree:longint; |
Полное число свободных номеров индексных дескрипторов |
fsid:longint; |
Идентификатор файловой системы |
namelen:longint; |
Максимальная длина файла |
Следующий пример делает примерно то же самое, что и стандартная команда df. Эта программа использует функцию fsstat для вывода числа свободных блоков и свободных индексных дескрипторов в файловой системе.
(* Программа fsys - вывод информации о файловой системе *)
(* Имя файловой системы передается в качестве аргумента *)
uses linux;
var
buf:statfs;
begin
if paramcount<>1 then
begin
writeln('Применение: fsys имя_файла');
halt(1);
end;
if not fsstat(paramstr(1), buf) then
begin
writeln('Ошибка вызова fsstat');
halt(2);
end;
writeln(paramstr(1),': свободных блоков ', buf.bfree, ', свободных индексов ', buf.ffree);
halt(0);
end.
Обе эти процедуры работают одинаково
uses stdio;
function pathconf(pathname:pchar;name:longint):integer;
function fpathconf(filedes, name:longint):integer;
Обе эти процедуры работают одинаково и возвращают значение для запрошенного ограничения или переменной. Различие между ними заключается в первом параметре: для процедуры pathconf это имя файла или каталога, а для процедуры fpathconf – дескриптор открытого файла. Второй параметр является значением одной из констант, определенных в файле stdio и обозначающих запрашиваемое ограничение.
Следующая программа lookup может использоваться для вывода системных ограничений для заданного файла/каталога. В этом примере программа lookup выводит наиболее интересные из этих значений для стандартного каталога /tmp:
(* Программа lookup - выводит установки ограничений файлов *)
uses stdio;
type table=record
val:integer;
name:pchar;
end;
var
tb:^table;
const options:array [0..3] of table=(
(val:_PC_LINK_MAX; name:'Максимальное число ссылок'),
(val:_PC_NAME_MAX; name:'Максимальная длина имени файла'),
(val:_PC_PATH_MAX; name:'Максимальная длина пути'),
(val:-1; name:nil)
);
begin
tb:=options;
while tb^.name<>nil do
begin
printf('%-32.31s%ld'#$a, [tb^.name, pathconf ('/tmp', tb^.val)]);
inc(tb);
end;
end.
На одной из систем эта программа вывела следующий результат:
Максимальное число ссылок 32767
Максимальная длина имени файла 256
Максимальная длина пути 1024
Эти значения относятся к каталогу /tmp. Максимально возможное число ссылок является характеристикой самого каталога, а максимальная длина имени файла относится к файлам в каталоге. Существуют также общесистемные ограничения (system-wide limits), они декларируются в файле <limits.h> и их значения могут быть определены при помощи похожей процедуры sysconf.
В результате успешного вызова fork
uses linux;
Function Fork:Longint;
В результате успешного вызова fork ядро создает новый процесс, который является почти точной копией вызывающего процесса. Другими словами, новый процесс выполняет копию той же программы, что и создавший его процесс, при этом все его объекты данных имеют те же самые значения, что и в вызывающем процессе, за одним важным исключением, которое вскоре обсудим.
Созданный процесс называется дочерним процессом (child process), а процесс, осуществивший вызов fork, называется родителем (parent).
После вызова родительский процесс и его вновь созданный потомок выполняются одновременно, при этом оба процесса продолжают выполнение с оператора, который следует сразу же за вызовом fork.
Идею, заключенную в вызове fork, быть может, достаточно сложно понять тем, кто привык к схеме последовательного программирования. Рис. 5.1 иллюстрирует это понятие. На рисунке рассматриваются три строки кода, состоящие из вызова writeln, за которым следует вызов fork, и еще один вызов
writeln.
writeln('One'); pid:=fork; |
|||||
writeln('Two'); |
< PC |
||||
A |
|
fork |
До |
||
После |
|||||
writeln('One'); pid:=fork; |
writeln('One'); pid:=fork; |
||||
writeln('Two'); |
< PC |
writeln('Two'); |
< PC |
||
A |
B |
Рис. 5.1. Вызов fork
Рисунок разбит на две части: До и После. Часть рисунка До показывает состояние до вызова fork. Существует единственный процесс A (его обозначили буквой А только для удобства, для системы это ничего не значит). Стрелка, обозначенная PC (program counter – программный счетчик), указывает на выполняемый в настоящий момент оператор. Так как стрелка указывает на первый оператор writeln, на стандартный вывод выдается тривиальное сообщение One.
Часть рисунка После показывает ситуацию сразу же после вызова fork. Теперь существуют два выполняющихся одновременно процесса: А и В. Процесс A – это тот же самый процесс, что и в части рисунка До. Процесс В – это новый процесс, порожденный вызовом fork. Этот процесс является копией процесса A за одним важным исключением – он имеет другое значение идентификатора процесса pid, но выполняет ту же самую программу, что и процесс А, то есть те же три строки исходного кода, приведенные на рисунке. В соответствии с введенной выше терминологией процесс А является родительским процессом, а процесс В – дочерним.
Две стрелки с надписью PC
в этой части рисунка показывают, что следующим оператором, который выполняется родителем и потомком после вызова fork, является вызов writeln. Другими словами, оба процесса А и В
продолжают выполнение с той же точки кода программы, хотя процесс В и является новым процессом для системы. Поэтому сообщение Two выводится дважды.