Адресация
Чтобы процессы могли связаться по сети, должен существовать механизм определения сетевого адреса (network address) компьютера, на котором находится другой процесс. В конечном счете адрес определяет физическое положение компьютера в сети. Обычно адреса состоят из нескольких частей, соответствующих различным уровням сети. Далее будут затронуты только те вопросы, без ответов на которые не обойтись при программировании с использованием сокетов.
Адресация Internet
Сейчас почти во всех глобальных сетях применима адресация IP (сокращение от Internet Protocol – межсетевой протокол, протокол сети Интернет).
Адрес IP состоит из четырех десятичных чисел, разделенных точками, например:
197.124.10.1
Эти четыре числа содержат достаточную информацию для определения сети назначения, а также компьютера в этой сети; собственно, термин Internet и означает «сеть сетей».
Сетевые вызовы UNIX не могут работать с IP адресами в таком формате. На программном уровне IP адреса хранятся в структуре типа in_addr_t. Обычно программистам не нужно знать внутреннее представление этого типа, так как для преобразования IP адреса в структуру типа in_addr_t предназначена процедура inet_addr.
Атрибуты процесса
С каждым процессом UNIX связан набор атрибутов, которые помогают системе управлять выполнением и планированием процессов, обеспечивать защиту файловой системы и так далее. Один из атрибутов, с которым мы уже встречались, – это идентификатор процесса, то есть число, которое однозначно идентифицирует процесс. Другие атрибуты простираются от окружения, которое является набором строк, определяемых программистом и находящихся вне области данных, до действующего идентификатора пользователя, определяющего права доступа процесса к файловой системе. В оставшейся части этой главы рассмотрим наиболее важные атрибуты процесса.
Бинарные файлы
Упражнение 13.24. Напишите программу, копирующую бинарный файл в обратном порядке байт.
var
fb,copyfb:file of byte;
size,n:longint;
b:byte;
{$I-}
begin
if paramcount<>2 then
begin
writeln('Используйте: ',paramstr(0),' входной_файл выходной файл');
exit;
end;
assign(fb,paramstr(1));
assign(copyfb,paramstr(2));
reset(fb);
if ioresult<>0 then
begin
writeln('Ошибка открытия файла: ',paramstr(1),' для чтения');
exit;
end;
rewrite(copyfb);
if ioresult<>0 then
begin
writeln('Ошибка открытия файла: ',paramstr(2),' для записи');
exit;
end;
n:=filesize(fb);
while n>=0 do
begin
//n:=n-2;
seek(fb,n-1);
//blockread(fb,b,1);
//blockwrite(copyfb,b,1);
read(fb,b);
write(copyfb,b);
n:=n-1;
end;
close(fb);
close(copyfb);
end.
Упражнение 13.25. Составьте программу кодировки и раскодировки файлов по заданному ключу (строке символов).
{I-}
var
fin,fout:file of byte;
pass:string;
size:longint;
b:byte;
begin
if paramcount<>2 then
begin
writeln('Используйте: ',paramstr(0),' входной_файл выходной файл');
exit;
end;
assign(fin,paramstr(1));
assign(fout,paramstr(2));
reset(fin);
if ioresult<>0 then
begin
writeln('Ошибка открытия файла: ',paramstr(1),' для чтения');
exit;
end;
rewrite(fout);
if ioresult<>0 then
begin
writeln('Ошибка открытия файла: ',paramstr(2),' для записи');
exit;
end;
writeln('Введите кодовое слово: ');
readln(pass);
size:=0;
while not(eof(fin)) do
begin
read(fin,b);
b:=b xor byte(pass[1 + (size mod length(pass))]);
write(fout,b);
inc(size);
end;
close(fin);
close(fout);
end.
Упражнение 13.26. Составьте аналог команды cmp.
type
mas=array [1..1] of byte;
var
fb,fbx:file of byte;
size,n,nx,i:longint;
t:boolean;
Блокирование сигналов
Если программа выполняет важную задачу, такую как обновление базы данных, то может понадобиться ее защита от прерывания на время выполнения таких критических действий. Как уже упоминалось, вместо игнорирования поступающих сигналов процесс может блокировать сигналы, это будет означать, что их выполнение будет отложено до тех пор, пока процесс не завершит выполнение
критического участка.
Блокировать определенные сигналы в процессе позволяет системный вызов sigprocmask, определенный следующим образом:
Блокировка записей при помощи вызова fcntl
О системном вызове управления файловым вводом/выводом fcntl уже упоминалось ранее. В дополнение к привычным функциям вызов fcntl может также использоваться для выполнения блокировки записей. Он предлагает два типа блокировки:
– блокировка чтения (read locks) – просто предотвращает установку другими процессами блокировки записи при помощи вызова fcntl. Несколько процессов могут одновременно выполнять блокировку чтения для одного и того же участка файла. Блокировка чтения может быть полезной, если, например, требуется предотвратить обновление данных, не скрывая их от просмотра другими пользователями;
– блокировка записи (write locks) – предотвращает установку другими процессами блокировку чтения или записи для файла. Другими словами, для заданного участка файла может существовать только одна блокировка записи одновременно. Блокировка записи может использоваться, например, для скрытия участков файла от просмотра при выполнении обновления.
Следует напомнить, что в соответствии со спецификацией XSI блокировка вызовом fcntl является всего лишь рекомендательной. Поэтому процессам необходимо явно согласовывать свои действия, чтобы блокировка вызовом fcntl была действенной (процессы не должны производить операции ввода/вывода без предварительного блокирования соответствующей области).
Для блокировки записей вызов fcntl используется следующим образом:
Чтение каталогов: вызовы readdir и rewinddir
После открытия каталога из него можно начать считывать записи.
Что вы должны знать
Эта книга не является учебником по системе UNIX или языку программирования Паскаль, а подробно исследует интерфейс системных вызовов UNIX. Чтобы использовать ее наилучшим образом, необходимо хорошо изучить следующие темы:
–
вход в систему UNIX;
– создание файлов при помощи одного из стандартных редакторов системы;
– древовидную структуру каталогов UNIX;
– основные команды работы с файлами и каталогами;
– создание и компиляцию простых программ на языке Паскаль (включая программы, текст которых находится в нескольких файлах);
– процедуры ввода/вывода;
– использование аргументов командной строки;
– применение man-системы (интерактивного справочного руководства системы). К сожалению, сейчас уже нельзя давать общие советы для работы со справочным руководством в различных системах, поскольку в формат руководства, прежде считавшийся стандартным, были внесены изменения несколькими производителями. Традиционно руководство было разбито на восемь разделов, каждый из которых был структурирован по алфавитному принципу. Наиболее важными являются три из них: раздел 1, описывающий команды; раздел 2, в котором представлены системные вызовы, и раздел 3, охватывающий функции стандартных библиотек.
Тем из вас, кто не знаком с какими-либо темами из приведенного списка, следует выполнить приведенные упражнения. Если потребуется дополнительная помощь, вы можете найти подходящее руководство, воспользовавшись библиографией в конце книги.
Наконец, стоит отметить, что для изучения информатики недостаточно простого чтения, поэтому на протяжении всей книги делается акцент на упражнения и примеры. Для выполнения упражнений вы должны иметь доступ к компьютеру с системой UNIX.
Упражнение 1. Объясните назначение следующих команд UNIX:
ls cat rm cp mv mkdir fpc
Упражнение 2. Создайте небольшой текстовый файл в вашем любимом текстовом редакторе. Создайте другой файл, содержащий пятикратно повторенный первый файл при помощи команды cat.
Подсчитайте число слов и символов в обоих файлах при помощи команды wc. Объясните полученный результат. Создайте подкаталог и поместите в него оба файла.
Упражнение 3. Создайте файл, содержащий список файлов в вашем начальном каталоге и в каталоге /bin.
Упражнение 4. Выведите при помощи одной команды число пользователей, находящихся в данный момент в системе.
Упражнение 5. Напишите, откомпилируйте и запустите на выполнение программу на языке Паскаль, которая выводит какое-либо приветствие.
Упражнение 6. Напишите, откомпилируйте и запустите на выполнение программу на языке Паскаль, которая печатает свои аргументы.
Упражнение 7. Напишите программу, которая подсчитывает и выводит число вводимых слов, строк и символов при помощи функций.
Упражнение 8. Создайте файл, содержащий процедуру на языке Паскаль, которая выводит сообщение 'hello, world'. Создайте отдельный файл основной программы, который вызывает эту процедуру. Откомпилируйте и выполните полученную программу, назвав ее hw.
Упражнение 9. Найдите в руководстве системы разделы, посвященные команде cat, процедуре printf и системному вызову write.
. Дата и время
Упражнение 13.62. Составьте аналог команды date.
ses crt,dos;
var y,h,d,dw,ch,m,s,ms:word;
BEGIN
getdate(y,h,d,dw);
case dw of
1:write('Пн');
2:write('Вт');
3:write('Ср');
4:write('Чт');
5:write('Пт');
6:write('Сбт');
7:write('Вс');
end;
write(' ');
case h of
1:write('Янв');
2:write('Фев');
3:write('Мар');
4:write('Апр');
5:write('Май');
6:write('Июн');
7:write('Июл');
8:write('Авг');
9:write('Сен');
10:write('Окт');
11:write('Ноя');
12:write('Дек');
end;
write(' ');
write(d,' ');
gettime(ch,m,s,ms);
write(ch,':',m,':',s,':',ms,' ',y);
readln;
END.
Упражнение 13.63. Составьте аналог команды cal.
uses crt,dos;
procedure cl(year,mes,pol_1:word);
var arr:array [0..7,1..7] of string[2];
i,j,kdm,n:byte;
v:boolean;
kd,k:word;
s:string[2];
begin
v:=false;
for i:=0 to 7 do
for j:=1 to 7 do
arr[i,j]:=' ';
if ((year mod 4=0)and(year mod 100<>0))or((year mod 4=0)and(year mod 100=0)and(year mod 400=0))
then v:=true
else if (year mod 100=0)and(year mod 400<>0)then v:=false;
case mes of
1:begin writeln('Янв');
kdm:=31;
kd:=0;
end;
2:begin writeln('Фев');
kd:=31;
if v then kdm:=29 else kdm:=28;
end;
3:begin writeln('Мар');
kdm:=31;
if v then kd:=60 else kd:=59;
end;
4:begin writeln('Апр');
kdm:=30;
if v then kd:=91 else kd:=90;
end;
5:begin writeln('Май');
kdm:=31;
if v then kd:=121 else kd:=120;
end;
6:begin writeln('Июн');
kdm:=30;
if v then kd:=152 else kd:=151;
end;
7:begin writeln('Июл');
kdm:=31;
if v then kd:=182 else kd:=181;
end;
8:begin writeln('Авг');
kdm:=31;
if v then kd:=213 else kd:=212;
end;
Действующие идентификаторы пользователей и групп
Необходимо сделать одно уточнение: создание файла определяется связанным с процессом действующим идентификатором пользователя euid
(effective user-id). Хотя процесс может быть запущен одним пользователем (скажем, keith), при определенных обстоятельствах он может получить права доступа другого пользователя (например, dina). Вскоре будет показано, как это можно осуществить. Идентификатор пользователя, запустившего процесс, называется истинным идентификатором пользователя
(real user-id, сокращенно ruid) этого процесса. Разумеется, в большинстве случаев действующий и истинный идентификаторы пользователя совпадают.
Аналогично с процессом связывается действующий идентификатор группы
(effective group-id, сокращенно egid), который может отличаться от истинного идентификатора группы (real group-id, сокращенно rgid).
Дописывание данных в конец файла
Как должно быть ясно из раздела 2.1.10, для дописывания данных в конец файла может использоваться следующий код:
(* Поиск конца файла *)
fdseek(filedes, 0, SEEK_END);
fdwrite(filedes, appbuf, BUFSIZE);
Тем не менее более изящный способ состоит в использовании одного из дополнительных флагов вызова fdopen, Open_APPEND. Если установлен этот флаг, то перед каждой записью указатель будет устанавливаться в конец файла. Это может быть полезно, если нужно лишь дополнить файл, застраховавшись от случайной перезаписи данных в начале файла.
Можно использовать флаг Open_APPEND следующим образом:
filedes := fdopen('yetanother', Open_WRONLY or Open_APPEND);
Каждый последующий вызов fdwrite будет дописывать данные в конец файла. Например:
fdwrite(filedes, appbuf, BUFSIZE);
Упражнение 2.14. Напишите процедуру fileopen, имеющую два аргумента: первый – строку, содержащую имя файла, и второй – строку, которая может иметь одно из следующих значений:
r – открыть файл только для чтения;
w – открыть файл только для записи;
rw – открыть файл для чтения и записи;
а – открыть файл для дописывания.
процедура fileopen должна возвращать дескриптор файла или код ошибки -1.
Дополнение о сокетах
Сокеты являются мощным и популярным способом взаимодействия между процессами разных компьютеров (в главе 10 они достаточно подробно были описаны). При необходимости получить больше информации по этой теме следует обратиться к специальной литературе. В качестве первого шага можно изучить следующие процедуры, которые позволяют получить информацию о сетевом окружении:[21]
gethostent getservbyname
gethostbyaddr getservbyport
gethostbyname getservent
Дополнительные права доступа для исполняемых файлов
Существуют еще три типа прав доступа к файлам, задающие особые атрибуты и обычно имеющие смысл только в том случае, если файл содержит исполняемую программу. Соответствующие восьмеричные значения и символьные имена также соответствуют определенным битам в коде доступа к файлу и обозначают следующее:
04000 STAT_ISUID Задать user-id при выполнении
02000 STAT_ISGID Задать group-id при выполнении
01000 STAT_ISVTX Сохранить сегмент кода (бит фиксации)
Если установлен флаг доступа STAT_ISUID, то при запуске на выполнение находящейся в файле программы система задает в качестве действующего идентификатора пользователя полученного процесса не идентификатор пользователя, запустившего процесс (как обычно), а идентификатор владельца файла. Процессу при этом присваиваются права доступа владельца файла, а не пользователя, запустившего процесс.
Подобный механизм может использоваться для управления доступом к критическим данным. Конфиденциальная информация может быть защищена от публичного доступа или изменения при помощи стандартных прав доступа на чтение/запись/выполнение. Владелец файла создает программу, которая будет предоставлять ограниченный доступ к файлу. Затем для файла программы устанавливается флаг доступа STAT_ISUID, что позволяет другим пользователям получать ограниченный доступ к файлу только при помощи данной программы. Очевидно, программа должна быть написана аккуратно во избежание случайного и умышленного нарушения защиты.[2]
Классический пример этого подхода представляет программа passwd. Администратора системы ожидают неприятности, если он позволит всем пользователям выполнять запись в файл паролей. Тем не менее все пользователи должны иногда изменять этот файл при смене своего пароля. Решить проблему позволяет программа passwd, так как ее владельцем является суперпользователь и для нее установлен флаг STAT_ISUID.
Не столь полезна установка флага STAT_ISGID, которая выполняет те же функции для идентификатора группы файла group-id. Если указанный флаг установлен, то при запуске файла на выполнение получившийся процесс получает действующий идентификатор группы egid владельца файла, а не пользователя, который запустил программу на выполнение.
Исторически бит STAT_ISVTX обычно использовался для исполняемых файлов и назывался флагом сохранения сегмента кода
(save-text-image), или битом фиксации
(sticky bit). В ранних версиях системы, если для файла был установлен этот бит, при его выполнении код программы оставался в файле подкачки до выключения системы. Поэтому при следующем запуске программы системе не приходилось искать файл в структуре каталогов системы, а можно было просто и быстро переместить программу в память из файла подкачки. В современных системах UNIX указанный бит является избыточным, и в спецификации XSI бит STAT_ISVTX определен только для каталогов. Использование STAT_ISVTX будет подробнее рассмотрено в главе 4.
Упражнение 3.4. Следующие примеры показывают, как команда ls выводит на экран права доступа set-user-id и group-id, соответственно:
r-sr-xr-x
r-xr-sr-x
При помощи команды ls -l найдите в каталогах /bin, /etc и /usr/bin файлы с необычными правами доступа (если это командные файлы оболочки и у вас есть право на чтение этих файлов, посмотрите, что они делают и надежно ли они защищены). Более опытные читатели могут ускорить поиск, воспользовавшись программой grep. Если вам не удастся найти файлы с необычными правами доступа, объясните, почему это произошло.
Дополнительные средства
В книге основное внимание было уделено вызовам и процедурам, которые позволяют дать основы системного программирования для ОС UNIX, и читатель к этому времени уже должен располагать средствами для решения большого числа задач. Конечно же, ОС UNIX является системой с богатыми возможностями, поэтому существует еще много процедур, обеспечивающих выполнение специальных функций. Этот последний раздел просто привлекает внимание к некоторым из них. Попытайтесь найти недостающую информацию в справочном руководстве системы.
Доступ к аргументам, передаваемым при вызове exec
Любая программа может получить доступ к аргументам активизировавшего ее вызова exec через параметры, передаваемые ей. Эти параметры описаны в модуле syslinux следующим образом:
var
argc:integer;
argv:ppchar;
envp:ppchar;
Такое описание должно быть знакомо большинству программистов на Си, так как похожий метод используется для доступа к аргументам командной строки при обычном старте программы – еще один признак того, что командный интерпретатор также использует для запуска процессов вызов exec. (Несколько предшествующих примеров и упражнений были составлены с учетом того, что читателям книги известен метод получения программой параметров ее командной строки. Ниже эта тема будет рассмотрена подробнее.)
В вышеприведенном определении значение переменной argc равно числу аргументов, переменная argv указывает на массив самих аргументов, а переменная envp – на массив строк окружения. Поэтому, если программа запускается на выполнение при помощи вызова execvp следующим образом:
const
argin:array [0..3] of pchar = ('команда', 'с', 'аргументами', nil);
execvp('prog', argin);
то в программе prog будут истинны следующие выражения (выражения вида argv[х] = 'ххх' следует считать фигуральным равенством, а не выражением языка Паскаль):
При использовании модуля syslinux | При использовании модуля system | ||
argc = 3 | paramcount = 2 | ||
argv[0] = 'команда' | paramstr(0) = 'команда' | ||
argv[1] = 'с' | paramstr(1) = 'с' | ||
argv[2] = 'аргументами' | paramstr(2) = 'аргументами' | ||
argv[3] = nil | paramstr(3) = nil |
В качестве простой иллюстрации этого метода рассмотрим следующую программу, которая печатает свои аргументы, за исключением нулевого, на стандартный вывод:[5]
а) с применением модуля system:
(* Программа myecho - вывод аргументов командной строки *)
var
i:integer;
begin
for i:=1 to paramcount do
write(paramstr(i), ' ');
writeln;
end.
б) с применением модуля syslinux:
(* Программа myecho - вывод аргументов командной строки *)
uses syslinux;
var
i:integer;
begin
for i:=1 to argc-1 do
write(argv[i], ' ');
writeln;
end.
Если вызвать эту программу в следующем фрагменте кода
const
argin:array [0..3] of pchar = ('myecho', 'hello', 'world', nil);
execvp(argin[0], argin);
то переменная argc в программе myecho будет иметь значение 3, и в результат на выходе программы получим:
hello world
Тот же самый результат можно получить при помощи команды оболочки:
$ ./myecho hello world
Упражнение 5.4. Напишите программу waitcmd, которая выполняет произвольную команду при изменении файла. Она должна принимать в качестве аргументов командной строки имя контролируемой файла и команду, которая должна выполняться в случае его изменения. Для слежения за файлом можно использовать вызов fstat. Программа не должна расходовать напрасно системные ресурсы, поэтому следует использовать процедуру sleep (представленную в упражнении 2.16), для приостановки выполнения программы waitcmd в течение заданного интервала времени, после того как она проверит файл. Как должна действовать программа, если файл изначально не существует?
Другие операции
Есть еще два типа операций, которые применимы к средствам межпроцессного взаимодействия. Во-первых, это операции управления, которые используются для опроса и изменения статуса объекта IPC, их функции выполняют вызовы msgctl, semctl и shmctl. Во-вторых, существуют операции, выполняющие основные функции IPC. Для каждого из средств межпроцессного взаимодействия существует набор операций, которые будут обсуждаться ниже в соответствующих пунктах. Например, есть две операции для работы с сообщениями: операция msgsnd помещает сообщение в очередь сообщений, а операция msgrcv считывает из нее сообщение.
Другие системные вызовы для работы с терминалом
Есть несколько дополнительных системных вызовов для работы с терминалом, позволяющих программисту до некоторой степени управлять очередями ввода и вывода, поддерживаемыми драйвером терминала. Эти вызовы определены следующим образом.
Еще об именах файлов
Для выделения из имени файла его частей можно воспользоваться функциями:
Файл
В системе UNIX информация находится в файлах. Типичные команды UNIX, работающие с файлами, включают в себя следующие:
$ vi my_test.pas
которая вызовет редактор vi
для создания и редактирования файла my_test.pas;
$ cat my_test.pas
которая выведет на терминал содержимое файла my_test.pas;
$ fpc my_test.pas
которая вызовет компилятор языка Паскаль для создания программы my_test из исходного файла my_test.pas, если файл my_test.pas не содержит ошибок.
Большинство файлов будет принадлежать к некоторой логической структуре, заданной пользователем, который их создает. Например, документ может состоять из слов, строк, абзацев и страниц. Тем не менее, с точки зрения системы, все файлы UNIX представляют собой простые неструктурированные последовательности байтов или символов. Предоставляемые системой примитивы позволяют получить доступ к отдельным байтам последовательно или в произвольном порядке. Не существует встроенных в файлы символов конца записи или конца файла, а также различных типов записей, которые нужно было бы согласовывать.
Эта простота является концептуальной для философии UNIX. Файл в системе UNIX является ясным и общим понятием, на основе которого могут быть сконструированы более сложные и специфические структуры (такие как индексная организация файлов). При этом безжалостно устраняются излишние подробности и особые случаи. Например, в обычном текстовом файле символ перехода на следующую строку (обычно символ перевода строки ASCII), определяющий конец строки текста, в системе UNIX представляет собой всего лишь один из символов, который может читаться и записываться системными утилитами и пользовательскими программами. Только программы, предполагающие, что на их вход подается набор строк, должны заботиться о семантике символа перевода строки.
Кроме этого, система UNIX не различает разные типы файлов. Файл может заключать в себе текст (например, файл, содержащий список покупок, или абзац, который вы сейчас читаете) или содержать «двоичные» данные (такие как откомпилированный код программы). В любом случае для оперирования файлом могут использоваться одни и те же примитивы или утилиты. Вследствие этого, в UNIX отсутствуют формальные схемы присваивания имен файлам, которые существуют в других операционных системах (тем не менее некоторые программы, например cc, следуют определенным простым условиям именования файлов). Имена файлов в системе UNIX совершенно произвольны и в системе SVR4 (System V Release 4) могут включать до 255 символов. Тем не менее, для того чтобы быть переносимыми в соответствии со спецификацией XSI, длина имен не должна превышать 14 символов – предела, заложенного в ранних версиях UNIX.
Файловая система proc
Упражнение 13.35. Напишите программу, печатающую: свои аргументы, переменные окружения, информацию обо всех открытых ею файлах и используемых каналах.
uses dos,linux,strings;
var
i,n,pid:integer;
s,fullname:array [0..200] of char;
temp:string;
d:pdir;
el:pdirent;
begin
i:=fdopen('/etc/passwd',Open_RDONLY);
n:=paramcount;
writeln('В командной строке ',paramcount+1, ' параметров');
for i:=0 to n do
writeln('Параметр ',i+1,' - ',paramstr(i));
writeln('Переменные окружения:');
n:=envcount;
for i:=1 to n do
writeln(envstr(i));
pid:=getpid;
strcopy(s,'/proc/');
str(pid,temp);
strpcopy(s+strlen(s),temp);
strcat(s,'/fd/');
d:=opendir(pchar(s));
el:=readdir(d);
writeln('Файлы, открытые процессом:');
while el<>nil do
begin
if (strcomp(el^.name,'.')<>0) and (strcomp(el^.name,'..')<>0) then
begin
strcopy(fullname,s);
strcat(fullname,el^.name);
writeln('Дескриптор ',el^.name,' соответствует ',readlink(fullname));
end;
el:=readdir(d);
end;
closedir(d);
end.
Упражнение 13.36. Создайте аналог команды ps.
uses linux,strings,sysutils; (*для системных вызовов Linux и работы со строками PChar*)
var
d:^TDir; (*указатель на запись для работы с каталогом*)
elem:^Dirent; (*указатель на запись, хранящую один элемент каталога*)
realname, (*имя процесса из файла status*)
fullpath (*полный путь к элементу каталога*)
:array [0..1000] of char;
number,coder:integer; (*номер процесса и код ошибки преобразования*)
f:text;
name:string;
begin
d:=opendir('/proc'); (*попытка открытия каталога для чтения*)
if d=nil then (*если попытка не удалась*)
begin
writeln('Ошибка вызова opendir для каталога /proc'); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
elem:=readdir(d); (*попытка чтения элемента каталога*)
while elem<>nil do (*пока не достигнут конец каталога*)
begin
(* пытаемся преобразовать имя элемента катлога в число*)
val(strpas(elem^.name),number,coder);
(*если это удается, катлог имеет числовое имя и соответствует процессу*)
if coder=0 then
begin
(*формируем путь к файлу status в виде /proc/номер_процесса/status *)
strcopy(fullpath,'/proc/');
strcat(fullpath,elem^.name);
strcat(fullpath,'/status');
(*открываем файл и чтиаем из него первую строку*)
assign(f,fullpath);
reset(f);
readln(f,name);
close(f);
(*вырезаем из строки ту ее часть, что соответствует имени процесса*)
strlcopy(realname,@name[7],length(name));
realname[length(name)-6]:=#0;
(*выводим номер и имя процесса*)
writeln(number,#9,realname);
end;
elem:=readdir(d); (*попытка чтения элемента каталога*)
end;
closedir(d); (*закрытие открытого opendir каталога*)
end.
Упражнение 13.37. Используя файловую систему /proc, получите информацию об открытых всеми процессами файлах.
uses linux,strings,sysutils; (*для системных вызовов Linux и работы со строками PChar*)
var
d,d1:^TDir; (*указатель на запись для работы с каталогом*)
elem,elem1:^Dirent; (*указатель на запись, хранящую один элекмент каталога*)
realname, (*имя процесса из файла status*)
fullpath,fullpath1 (*полный путь к элементу каталога*)
:array [0..1000] of char;
number,coder:integer; (*номер процесса и код ошибки преобразования*)
f:text;
name:string;
begin
d:=opendir('/proc'); (*попытка открытия каталога для чтения*)
if d=nil then (*если попытка не удалась*)
begin
writeln('Ошибка вызова opendir для каталога /proc'); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
elem:=readdir(d); (*попытка чтения элемента каталога*)
while elem<>nil do (*пока не достигнут конец каталога*)
begin
(* пытаемся преобразовать имя элемента катлога в число*)
val(strpas(elem^.name),number,coder);
(*если это удается, катлог имеет числовое имя и соответствует процессу*)
if coder=0 then
begin
(*формируем путь к файлу status в виде /proc/номер_процесса/fd *)
strcopy(fullpath,'/proc/');
strcat(fullpath,elem^.name);
strcat(fullpath,'/fd');
d1:=opendir(fullpath);
if d1=nil then
writeln('Для процесса ',number,' информация об открытых файлах недоступна')
else
begin
writeln('Процесс ',number,' открыл следующие файлы:');
elem1:=readdir(d1); (*попытка чтения элемента каталога*)
while elem1<>nil do (*пока не достигнут конец каталога*)
begin
strcopy(fullpath1,fullpath);
strcat(fullpath1,'/');
strcat(fullpath1,elem1^.name);
if (strcomp(elem1^.name,'.')<>0) and (strcomp(elem1^.name,'..')<>0) then
begin
(*realname[readlink(realname,fullpath1,999)]:=#0;*)
writeln(#9,readlink(strpas(fullpath1)));
end;
elem1:=readdir(d1); (*попытка чтения элемента каталога*)
end;
end;
end;
elem:=readdir(d); (*попытка чтения элемента каталога*)
end;
closedir(d); (*закрытие открытого opendir каталога*)
end.
Упражнение 13.38. Используя файловую систему /proc, получите информацию о типе, версии и дате выпуска операционной системы.
uses sysutils,crt,linux;
var
f:text;
count:integer;
ch:char;
begin
assign(f,'/proc/version');
reset(f);
if IOResult<>0 then
begin
writeln('Неудалось открыть файл, попробуйте другими средствами');
halt;
end;
textcolor(7);
write('Тип операционной системы - ');
read(f,ch);
textcolor(2);
write(ch);
while ch<>' ' do
begin
read(f,ch);
write(ch);
end;
writeln;
while ch=' ' do
begin
read(f,ch);
end;
while ch<>' 'do
begin
read(f,ch);
end;
while ch=' ' do
begin
read(f,ch);
end;
textcolor(7);
write('Версия операционной системы - ');
textcolor(2);
write(ch);
while ch<>' 'do
begin
read(f,ch);
write(ch);
end;
while ch<>'#' do
begin
read(f,ch);
end;
read(f,ch);
writeln;
textcolor(7);
write('Дата выпуска -');
textcolor(2);
while not eof(f)do
begin
read(f,ch);
write(ch);
end;
textcolor(7);
Close(f);
end.
Упражнение 13.39. Используя файловую систему /proc, получите информацию о процессоре (vendor_id, cpu family, model, model name, stepping, cpu MHz, cache size, fdiv_bug, hlt_bug, sep_bug, f00f_bug, coma_bug, fpu, fpu_exception, cpuid level, wp, flags, bogomips).
uses sysutils,linux;
var
f,count:integer;
ch:array [0..511] of byte;
begin
f:=fdopen('/proc/cpuinfo',Open_RDONLY);
if f=-1 then
begin
writeln('Невозможно открыть файл ');
halt;
end;
writeln('Information about CPU');
count:=fdread(f,ch,512);
while count>0 do
begin
fdwrite(1,ch,count);
count:=fdread(f,ch,512);
end;
fdClose(f);
end.
Упражнение 13.40. Используя файловую систему /proc, получите информацию об используемой памяти.
uses sysutils,linux;
var
f,count:integer;
ch:array [0..511] of char;
begin
f:=fdopen('/proc/meminfo',Open_RDONLY);
if f=-1 then
begin
writeln('Невозможно открыть файл для просмотра информации об используемой памяти');
halt;
end;
writeln('Information about MEMORY');
count:=fdread(f,ch,512);
while count>0 do
begin
fdwrite(1,ch,count);
count:=fdread(f,ch,512);
end;
fdClose(f);
end.
Файловые системы
Упражнение 13.34. Создайте аналог команды df.
uses linux;
var s:string;
p:statfs;
i,k: word;
fd:text;
buf:array[1..1000]of char;
pol:boolean;
var
size:array[1..3] of double;
sizepow:array[1..3] of integer;
const
letters:array [0..3] of char=('b','K','M','G');
function point_Mount(var f:string):string;
var str:string;j,k:integer;flag:boolean;
begin
str:='';
k:=0;
flag:=false;
for j:=1 to length(f) do
begin
if f[j]=' ' then begin flag:=true;inc(k);end;
if (flag=true)and(k=1)then
begin
str:=str+f[j];
end;
end;
delete(str,1,1);
point_Mount:=str;
end;
begin
assign(fd,'/etc/mtab');
reset(fd);
pol:=true;
Writeln('Файловая система Размер Испол Дост Исп% Подключено к');
while not eof(fd) do
begin
readln(fd,s);
pol:=fsstat(point_Mount(s),p);
if (pol)and(p.blocks<>0) then
begin
for i:=1 to length(s) do
begin
write(s[i]);
if s[i]=' ' then break;
end;
for k:=i to 1 do
write(' ');
size[1]:=1.*p.blocks*p.bsize;
size[2]:=1.*(p.blocks-p.bavail)*p.bsize;
size[3]:=1.*p.bavail*p.bsize;
for i:=1 to 3 do
begin
sizepow[i]:=0;
while size[i]>1024 do
begin
size[i]:=size[i]/1024;
inc(sizepow[i]);
end;
end;
writeln(size[1]:15:1,letters[sizepow[1]],{Истинный размер }
size[2]:10:1,letters[sizepow[2]], {Используемый размер}
size[3]:10:1,letters[sizepow[3]], {Доступно или свободно}
' ',((p.blocks-p.bavail)*100/p.blocks):4:0,'% ', {Процентное соотношение используемого места}
' ',point_Mount(s)); {Подключение}
end
end;
close(fd);
end.
Файловые системы UNIX
Как уже было рассмотрено, файлы могут быть организованы в различные каталоги, которые образуют иерархическую древовидную структуру. Каталоги могут быть сгруппированы вместе, образуя файловую систему (file system). Обычно с файловыми системами имеет дело только системный администратор UNIX. Они позволяют распределять структуру каталогов по нескольким различным физическим дискам или разделам диска, сохраняя однородность структуры с точки зрения пользователя.
Каждая файловая система начинается с каталога в иерархическом дереве. Это свойство позволяет системным администраторам разбивать иерархию файлов UNIX и отводить под ее части отдельные области на диске или даже распределять файловую структуру между несколькими физическими дисковыми устройствами. В большинстве случаев физическое разбиение файловой системы остается невидимым для пользователей.
Файловые системы также называются монтируемыми томами
(mountable volumes), поскольку их можно динамически монтировать и демонтировать
в виде целых поддеревьев в определенные точки общей древовидной структуры каталогов системы. Демонтирование файловой системы делает все ее содержимое временно недоступным для пользователей. Операционной системе могут быть доступны несколько файловых систем, но не все из них обязательно будут видны как части древовидной структуры.
Информация, содержащаяся в файловой системе, находится на разделе диска, доступном через файл устройства (device file), также называемый специальный файлом
(special file). Этот тип файлов будет описан ниже, а пока просто упомянем, что в системе UNIX каждая файловая система однозначно определяется некоторым именем файла.
Реальное расположение данных файловой системы на носителе никак не связано с высокоуровневым иерархическим представлением каталогов с точки зрения пользователя. Кроме того, расположение данных файловой системы не определяется спецификацией XSI – существуют разные реализации. Ядро может поддерживать одновременно несколько типов файловых систем с различной организацией хранения данных. Здесь будет описано только традиционное расположение.
Традиционная файловая система разбита на ряд логических частей. Каждая такая файловая система содержит четыре определенных секции: загрузочная область (bootstrap area), суперблок
(superblock), ряд блоков, зарезервированных для структур индексных дескрипторов (inode) файловой системы, и области, отведенной для блоков данных, образующих файлы этой файловой системы. Это расположение схематично представлено на рис. 4.6. Первый из этих блоков (блок с нулевым логическим номером, физически он может быть расположен где угодно внутри раздела диска) зарезервирован для использования в качестве загрузочного блока. Это означает, что он может содержать зависящую от оборудования загрузочную программу, которая используется для загрузки ОС UNIX при старте системы.
Блок 0 |
Загрузочный блок |
Блок 1 |
Суперблок |
Блоки 2…n |
Блоки индексных дескрипторов |
Блоки n+1…r |
Блоки данных |
Логический блок 1 в файловой системе называется суперблоком. Он содержит всю жизненно важную информацию о системе, например, полный размер файловой системы (r блоков на приведенном рисунке), число блоков, отведенных для индексных дескрипторов (n–2), дату и время последнего обновления файловой системы. Суперблок содержит также два списка. В первом из них находится часть цепочки номеров свободных блоков секции данных, а во втором – часть цепочки номеров свободных индексных дескрипторов. Эти два списка обеспечивают ускорение доступа к файловой системе при выделении новых блоков на диске для хранения дополнительных данных или при создании нового файла или каталога. Суперблок смонтированной файловой системы находится в памяти для обеспечения быстрого доступа к списку свободных блоков и свободных узлов. Эти списки в памяти пополняются с диска по мере их исчерпания.
Размер структуры индексных дескрипторов зависит от файловой системы; например, в определенных файловых системах она имеет размер 64 байта, а в других – 128 байт. Индексные дескрипторы последовательно нумеруются, начиная с единицы, поэтому для определения положения структуры индексного дескриптора с заданным номером, прочтенным из записи каталога (как это происходит при переходе в подкаталог или при открытии определенного файла каталога), используется совсем простой алгоритм.
Файловые системы создаются при помощи программы mkfs, и при ее запуске задаются размеры области индексных дескрипторов и области данных. В традиционных файловых системах размеры этих областей нельзя изменять динамически, поэтому можно было исчерпать пространство файловой системы одним из двух способов. Во-первых, это может произойти, если были использованы все блоки данных (даже если еще есть доступные номера индексных дескрипторов). Во-вторых, могут быть использованы все номера индексных дескрипторов (при создании большого числа мелких файлов), и, следовательно, дальнейшее создание новых файлов в файловой системе станет невозможным, даже если есть еще свободные блоки данных. В настоящее время современные файловые системы могут иметь переменный размер, и пространство под индексные дескрипторы часто выделяется динамически.
Теперь понятно, что номера индексных дескрипторов являются уникальными только в пределах файловой системы, вот почему невозможно использовать жесткие ссылки между файловыми системами.
Файлы блочных и символьных устройств
Файлы устройств UNIX разбиваются на две категории: блочные устройства (block devices) и символьные устройства (character devices):
–
семейство файлов блочных устройств соответствует устройствам класса дисковых накопителей (съемных и встроенных) и накопителей на магнитной ленте. Передача данных между ядром и этими устройствами осуществляется блоками стандартного размера. Все блочные устройства обеспечивают произвольный доступ. Внутри ядра доступ к этим устройствам управляется хорошо структурированным набором процедур и структур ядра. Этот общий интерфейс к блочным устройствам означает, что обычно драйверы блочных устройств очень похожи, различаясь только в низкоуровневом управлении заданным устройством;
– семейство файлов символьных устройств соответствует устройствам терминалов, модемных линий, устройствам печати, то есть тем устройствам, которые не используют блочный механизм структурированной пересылки данных. Произвольный доступ для символьных устройств может как поддерживаться, так и не поддерживаться. Данные передаются не блоками фиксированного размера, а в виде потоков байтов произвольной длины.
Важно заметить, что файловые системы могут находиться только на блочных устройствах, и блочные устройства имеют связанные с ними символьные устройства для быстрого и простого доступа, которые называются устройствами прямого доступа (raw device). Утилиты mkfs и fsck используют интерфейс прямого доступа.
ОС UNIX использует две конфигурационные таблицы для связи периферийного устройства с кодом его управления, эти таблицы называются таблицей блочных устройств (block device switch) и таблицей символьных устройств (character device switch). Обе таблицы проиндексированы при помощи значения старшего номера устройства (major device number), который записан в номере индексного дескриптора файла устройства. Последовательность передачи данных к периферийному устройству и от него выглядит так:
1. Системные вызовы fdread или fdwrite обращаются к индексному дескриптору файла устройства обычным способом.
2. Система проверяет флаг в структуре индексного дескриптора и определяет, является ли устройство блочным или символьным. Также извлекается старший номер устройства.
3. Старший номер используется для индексирования соответствующей таблицы устройств и нахождения процедуры драйвера устройства, нужной для непосредственного выполнения передачи данных.
Таким образом, порядок доступа к периферийным устройствам полностью согласуется с порядком доступа к обычным дисковым файлам.
Кроме старшего номера устройства, в индексном дескрипторе также записан второе значение, называемое младшим номером устройства (minor device number) и передаваемое процедурам драйвера устройства для точного задания номера порта на устройствах, которые поддерживают более одного порта, или для обозначения одного из разделов жесткого диска, обслуживаемых одним драйвером. Например, на 8-портовой плате терминала все линии будут иметь один и тот же старший номер устройства и, соответственно, тот же набор процедур драйвера устройства, но каждая конкретная линия будет иметь свой уникальный младший номе устройства в диапазоне от 0 до 7.
Файлы с несколькими именами
Любой файл UNIX может иметь несколько имен. Другими словами, один и тот же набор данных может быть связан с несколькими именами UNIX без необходимости создания копий файла. Поначалу это может показаться странным, но для экономии свободного пространства на диске и увеличения числа пользователей, использующих один и тот же файл, – весьма полезно.
Каждое такое имя называется жесткой ссылкой (hard link). Число связанных с файлом ссылок называется счетчиком ссылок (link count).
Новая жесткая ссылка создается при помощи системного вызова link, а существующая жесткая ссылка может быть удалена при помощи системного вызова unlink.
Следует отметить полную равноправность жестких ссылок на файл и настоящего имени файла. Нет способа отличить настоящее имя файла от созданной позднее жесткой ссылки. Это становится очевидным, если рассмотреть организацию файловой системы, – см. главу 4.
Флаг SEM_UNDO
Это еще один флаг, который может быть установлен в элементе sem_flg структуры sembuf. Он сообщает системе, что нужно автоматически «отменить» эту операцию после завершения процесса. Для отслеживания всей последовательности таких операций система поддерживает для семафора целочисленную переменную semadj. Важно понимать, что переменная semadj связана с процессами, и для разных процессов один и тот же семафор будет иметь различные значения semadj. Если при выполнении операции semop установлен флаг
SEM_UNDO, то значение переменной sem_num просто вычитается из значения semadj. При этом важен знак переменной sem_num: значение semadj уменьшается, если значение sem_num положительное, и увеличивается, если оно отрицательное. После выхода из процесса система прибавляет все значения semadj к соответствующим семафорам и, таким образом, сводит на нет эффект от всех вызовов semop. В общем случае флаг SEM_UNDO должен быть всегда установлен, кроме тех случаев, когда значения, устанавливаемые процессом, должны сохраняться после завершения процесса.
Функции ttyname и isatty
Теперь представим две полезных функции, которые будем использовать в следующих примерах. Функция ttyname возвращает имя терминального устройства, связанного с дескриптором открытого файла, а функция isatty возвращает значение true (то есть истинно в терминах языка Паскаль), если дескриптор файла описывает терминальное устройство, и false (ложно) – в противном случае.
. Генератор лексических анализаторов lex
Упражнение 13.66. Составьте вариант программы подсчета служебных слов языка Си, не учитывающий появление этих слов, заключенных в кавычки.
%{
uses lexlib;
const
_AND=1;
_ASM=2;
_ARRAY=3;
_BEGIN=4;
_CASE=5;
_CONST=6;
_CONSTRUCTOR=7;
_DESTRUCTOR=8;
_DIV=9;
_DO=10;
_DOWNTO=11;
_ELSE=12;
_END=13;
_EXPORTS=14;
_FILE=15;
_FOR=16;
_FUNCTION=17;
_GOTO=18;
_IF=19;
_IMPLEMENTATION=20;
_IN=21;
_INHERITED=22;
_INLINE=23;
_INTERFACE=24;
_LABEL=25;
_LIBRARY=26;
_MOD=27;
_NIL=28;
_NOT=29;
_OBJECT=30;
_OF=31;
_OR=32;
_PACKED=33;
_PROCEDURE=34;
_PROGRAM=35;
_RECORD=36;
_REPEAT=37;
_SET=38;
_SHL=39;
_SHR=40;
_STRING=41;
_THEN=42;
_TO=43;
_TYPE=44;
_UNIT=45;
_UNTIL=46;
_USES=47;
_VAR=48;
_WHILE=49;
_WITH=50;
_XOR=51;
_STR=52;
_IDENT=53;
%}
letter [a-zA-Z]
digit [0-9]
%%
'[^']*' begin yydone:=true; yyretval:=_STR; end;
\"[^\"]\" begin yydone:=true; yyretval:=_STR; end;
"end." begin yydone:=true; yyretval:=_END; end;
and begin yydone:=true; yyretval:=_AND; end;
asm begin yydone:=true; yyretval:=_ASM; end;
array begin yydone:=true; yyretval:=_ARRAY; end;
begin begin yydone:=true; yyretval:=_BEGIN; end;
case begin yydone:=true; yyretval:=_CASE; end;
const begin yydone:=true; yyretval:=_CONST; end;
constructor begin yydone:=true; yyretval:=_CONSTRUCTOR; end;
destructor begin yydone:=true; yyretval:=_DESTRUCTOR; end;
div begin yydone:=true; yyretval:=_DIV; end;
do begin yydone:=true; yyretval:=_DO; end;
downto begin yydone:=true; yyretval:=_DOWNTO; end;
else begin yydone:=true; yyretval:=_ELSE; end;
end begin yydone:=true; yyretval:=_END; end;
exports begin yydone:=true; yyretval:=_EXPORTS; end;
file begin yydone:=true; yyretval:=_FILE; end;
for begin yydone:=true; yyretval:=_FOR; end;
function begin yydone:=true; yyretval:=_FUNCTION; end;
Межпроцессное взаимодействие при помощи программных каналов
Если два или несколько процессов совместно выполняют одну и ту же задачу, то они неизбежно должны использовать общие данные. Хотя сигналы и могут быть полезны для синхронизации процессов или для обработки исключительных ситуаций или ошибок, они совершенно не подходят для передачи данных от одного процесса к другому. Один из возможных путей разрешения этой проблемы заключается в совместном использовании файлов, так как ничто не мешает нескольким процессам одновременно выполнять операции чтения или записи для одного и того же файла. Тем не менее совместный доступ к файлам может оказаться неэффективным и потребует специальных мер предосторожности для избежания конфликтов.
Для решения этих проблем система
UNIX обеспечивает конструкцию, которая называется программными каналами (pipe). (В следующих главах будут также изучены некоторые другие средства коммуникации процессов.) Программный канал (или просто канал) служит для установления односторонний связи, соединяющей один процесс с другим, и является еще одним видом обобщенного ввода/вывода системы UNIX. Как увидим далее, процесс может посылать данные в канал при помощи системного вызова fdwrite, а другой процесс может принимать данные из канала при помощи системного вызова
fdread.
Основные понятия и терминология
В этой главе сделан краткий обзор основных идей и терминологии, которые будут использоваться в книге. Начнем с понятия файл
(file).
Работа с файлами
Файлы не определяются полностью содержащимися в них данными. Каждый файл UNIX содержит ряд простых дополнительных свойств, необходимых для администрирования этой сложной многопользовательской системы. В данной главе будут изучены дополнительные свойства и оперирующие ими системные вызовы.
Группы процессов и идентификаторы группы процессов
Система UNIX позволяет легко помещать процессы в группы. Например, если в командной строке задано, что процессы связаны при помощи программного канала, они обычно помещаются в одну группу процессов. На рис. 5.5 показана такая типичная группа процессов, установленная из командной строки.
who | awk '{print $1}' | sort -u | |||||||||||||||
Рис. 5.5. Группа процессов
Группы процессов удобны для работы с набором процессов в целом, с помощью механизма межпроцессного взаимодействия, который называется сигналами, о чем будет сказано подробнее в главе 6. Обычно сигнал «посылается» отдельному процессу и может вызывать завершение этого процесса, но можно послать сигнал и целой группе процессов.
Каждая группа процессов (process group) обозначается идентификатором группы процессов (process group-id), имеющим тип pid_t. Процесс, идентификатор которого совпадает с идентификатором группы процессов, считается лидером (leader) группы процессов, и при его завершении выполняются особые действия. Первоначально процесс наследует идентификатор группы во время вызова
fork или ехес.
Процесс может получить свой идентификатор группы при помощи системного вызова getpgrp:
Идентификатор процесса
Как было уже отмечено в начале этого раздела, вызов fork не имеет аргументов и возвращает идентификатор процесса
pid типа longint. Пример вызова:
uses linux;
var
pid:longint;
pid := fork;
Родитель и потомок отличаются значением переменной pid. В родительском процессе значение переменной pid будет ненулевым положительным числом, для потомка же оно равно нулю. Так как возвращаемые в родительском и дочернем процессе значения различаются, то программист может задавать различные действия для двух процессов.
Значение, возвращаемое родительскому процессу в переменной pid, называется идентификатором процесса (process-id) дочернего процесса. Это число идентифицирует процесс в системе аналогично идентификатору пользователя. Поскольку все процессы порождаются при помощи вызова fork, то каждый процесс UNIX имеет уникальный идентификатор процесса.
Следующая короткая программа более наглядно показывает работу вызова fork и использование идентификатора процесса:
(* Программа spawn - демонстрация вызова fork *)
uses linux;
var
pid:longint; (* process-id в родительском процессе *)
begin
writeln ('Пока всего один процесс');
writeln ('Вызов fork...');
pid := fork; (* создание нового процесса *)
if pid = 0 then
writeln ('Дочерний процесс')
else if (pid > 0) then
writeln ('Родительский процесс, pid потомка ', pid)
else
writeln ('Ошибка вызова fork, потомок не создан');
end.
Оператор if, следующий за вызовом fork, имеет три ветви. Первая определяет дочерний процесс, соответствующий нулевому значению переменной pid. Вторая задает действия для родительского процесса, соответствуя положительному значению переменной pid. Третья ветвь неявно соответствует отрицательному, а на самом деле равному -1, значению переменной pid, которое возвращается, если вызову fork не удается создать дочерний процесс. Это может означать, что вызывающий процесс попытался нарушить одно из двух ограничений; первое из них – системное ограничение на число процессов; второе ограничивает число процессов, одновременно выполняющихся и запущенных одним пользователем. В обоих случаях переменная linuxerror содержит код ошибки Sys_EAGAIN. Обратите также внимание на то, что поскольку оба процесса, созданных программой, будут выполняться одновременно без синхронизации, то нет гарантии, что вывод родительского и дочернего процессов не будет смешиваться.
Перед тем как продолжить, стоит обсудить, зачем нужен вызов fork, поскольку сам по себе он может показаться бессмысленным. Существенный момент заключается в том, что вызов fork обретает ценность в сочетании с другими средствами
UNIX. Например, возможно, что родительский и дочерний процессы будут выполнять различные, но связанные задачи, организуя совместную работу при помощи одного из механизмов межпроцессного взаимодействия, такого как сигналы или каналы (описываемые в следующих главах). Другим средством, часто используемым совместно с вызовом fork, является системный вызов ехес, позволяющий выполнять другие программы, и который будет рассмотрен в следующем разделе.
Упражнение 5.1. Программа может осуществлять вызов fork несколько раз. Аналогично каждый дочерний процесс может вызывать fork, порождая своих потомков. Чтобы доказать это, напишите программу, которая создает два подпроцесса, а они, в свою очередь, – свой подпроцесс. После каждого вызова fork каждый родительский процесс должен использовать функцию writeln для вывода идентификаторов своих дочерних процессов.
Идентификатор процесса
Как уже отмечено в начале этой главы, система присваивает каждому процессу неотрицательное число, которое называется идентификатором процесса. В любой момент времени идентификатор процесса является уникальным, хотя после завершения процесса он может использоваться снова для другого процесса. Некоторые идентификаторы процесса зарезервированы системой для особых процессов. Процесс с идентификатором 0, хотя он и называется планировщиком (scheduler), на самом деле является процессом подкачки памяти (swapper). Процесс с идентификатором 1 – это процесс инициализации, выполняющий программу /etc/init. Этот процесс, явно или неявно, является предком всех других процессов в системе UNIX.
Программа может получить свой идентификатор процесса при помощи следующего системного вызова:
pid := getpid;
Аналогично вызов getppid возвращает идентификатор родителя вызывающего процесса:
ppid := getppid;
Например:
Uses linux;
begin
Writeln ('Process Id = ',getpid,' Parent process Id = ',getppid);
end.
Следующая процедура gentemp использует вызов getpid для формирования уникального имени временного файла. Это имя имеет форму:
/tmp/tmp<pid>.<no>
Суффикс номера <nо> увеличивается на единицу при каждом вызове процедуры gentemp. Процедура также вызывает функцию access, чтобы убедиться, что файл еще не существует:
uses linux,strings;
const
num:integer=0;
namebuf:array [0..19] of char='';
prefix='/tmp/tmp';
function gentemp:pchar;
var
length:integer;
pid:longint;
begin
pid := getpid; (* получить идентификатор процесса *)
(* Стандартные процедуры работы со строками *)
strcopy (namebuf, prefix);
length := strlen (namebuf);
(* Добавить к имени файла идентификатор процесса *)
itoa (pid, @namebuf[length]);
strcat (namebuf, '.');
length := strlen (namebuf);
repeat
(* Добавить суффикс с номером *)
itoa(num, @namebuf[length]);
inc(num);
until(not access (namebuf, F_OK));
gentemp:=namebuf;
end;
Процедура itoa просто преобразует целое число в эквивалентную строку:
(* Функция itoa - преобразует целое число в строку *)
function itoa(i:integer;str:pchar):integer;
var
power, j : integer;
begin
j := i;
power := 1;
while j >= 10 do
begin
power := power * 10;
j := j div 10;
end;
while power > 0 do
begin
str^ := char(byte('0') + i div power);
inc(str);
i := i mod power;
power := power div 10;
end;
str^ := #0;
end;
Обратите внимание на способ преобразования цифры в ее символьный эквивалент в первом операторе во втором цикле for – он опирается на знание таблицы символов ASCII. Следует также отметить, что большую часть работы можно было бы выполнить гораздо проще при помощи процедуры sprintf. Описание процедуры sprintf смотрите в главе 11.
Упражнение 5.11. Переделайте процедуру gentemp так, чтобы она принимала в качестве аргумента префикс имени временного файла.
Идентификаторы пользователя и группы
С каждым процессом связаны истинные идентификаторы пользователя и группы. Это всегда идентификатор пользователя и текущий идентификатор группы запустившего процесс пользователя.
Действующие идентификаторы пользователя и группы используются для определения возможности доступа процесса к файлу. Чаще всего, эти идентификаторы совпадают с истинными идентификаторами пользователя и группы. Равенство нарушается, если процесс или один из его предков имеет установленные биты доступа set-user-id или set-group-id. Например, если для файла программы установлен бит set-user-id, то при запуске программы на выполнение при помощи вызова ехес действующим идентификатором пользователя становится идентификатор владельца файла, а не запустившего процесс пользователя.
Для получения связанных с процессом идентификаторов пользователя и группы существует несколько системных вызовов. Следующий фрагмент программы демонстрирует их:
uses linux;
var
uid, euid, gid, egid : longint;
begin
(* Получить истинный идентификатор пользователя *)
uid := getuid;
(* Получить действующий идентификатор пользователя *)
euid := geteuid;
(* Получить истинный идентификатор группы *)
gid := getgid;
(* Получить действующий идентификатор группы *)
egid := getegid;
end.
Для задания действующих идентификаторов пользователя и группы процесса также существуют два вызова:
uses stdio;
var
newuid, newgid:longint;
.
.
.
(* Задать действующий идентификатор пользователя *)
status := setuid(newuid);
(* Задать действующий идентификатор группы *)
status := setgid(newgid);
Процесс, запущенный непривилегированным пользователем (то есть любым пользователем, кроме суперпользователя) может менять действующие идентификаторы пользователя и группы только на истинные.[6]
Суперпользователю, как всегда, предоставляется полная свобода. Обе процедуры возвращают нулевое значение в случае успеха, и -1 – в случае неудачи.
Упражнение 5.14. Напишите процедуру, которая получает истинные идентификаторы пользователя и группы вызывающего процесса, а затем преобразует их в символьную форму и записывает в лог-файл.
Имена сигналов
Сигналы не могут непосредственно переносить информацию, что ограничивает их применимость в качестве общего механизма межпроцессного взаимодействия. Тем не менее каждому типу сигналов присвоено мнемоническое имя (например, SIGINT), которое указывает, для чего обычно используется сигнал этого типа. Имена сигналов определены в модуле linux при помощи директивы const. Как и следовало ожидать, эти имена соответствуют небольшим положительным целым числам. Например, сигнал SIGINT обычно определяется так:
const SIGINT = 2; (* прерывание (rubout) *)
Большинство типов сигналов
UNIX предназначены для использования ядром, хотя есть несколько сигналов, которые посылаются от процесса к процессу. Ниже приведен описанный в спецификации XSI полный список стандартных сигналов и их значение. Для удобства список сигналов отсортирован в алфавитном порядке. При первом чтении этот список может быть пропущен.
– SIGABRT – сигнал прерывания процесса (process abort signal). Посылается процессу при вызове им функции
abort. В результате сигнала SIGABRT произойдет то, что спецификация XSI описывает как аварийное завершение (abnormal termination), авост. Следствием этого в реализациях UNIX является сброс образа памяти (core dump, иногда переводится как «дамп памяти») с выводом сообщения Quit - core dumped. Образ памяти процесса сохраняется в файле на диске для изучения с помощью отладчика;
– SIGALRM – сигнал таймера (alarm clock). Посылается процессу ядром при срабатывании таймера. Каждый процесс может устанавливать не менее трех таймеров. Первый из них измеряет прошедшее реальное время. Этот таймер устанавливается самим процессом при помощи системного вызова alarm (или установки значения первого параметра в более редко применяющемся вызове setitimer равным ITIMER_REAL). Вызов alarm будет описан в разделе 6.4.2. При необходимости больше узнать о вызове setitimer следует обратиться к справочному руководству системы;
– SIGBUS – сигнал ошибки на шине (bus error). Этот сигнал посылается при возникновении некоторой аппаратной ошибки. Смысл ошибки на шине определяется конкретной реализацией (обычно он генерируется при попытке обращения к допустимому виртуальному адресу, для которого нет физической страницы). Данный сигнал, так же как и сигнал SIGABRT, вызывает аварийное завершение;
– SIGCHLD – сигнал останова или завершения дочернего процесса (child process terminated or stopped). Если дочерний процесс останавливается или завершается, то ядро сообщит об этом родительскому процессу, послав ему сигнал SIGCHLD. По умолчанию родительский процесс игнорирует этот сигнал, поэтому, если в родительском процессе необходимо получать сведения о завершении дочерних процессов, то нужно перехватывать этот сигнал;
– SIGCONT – продолжение работы остановленного процесса
(continue executing if stopped). Этот сигнал управления процессом, который продолжит выполнение процесса, если он был остановлен; в противном случае процесс будет игнорировать этот сигнал. Это сигнал обратный сигналу SIGSTOP;
– SIGHUP – сигнал освобождения линии (hangup signal). Посылается ядром всем процессам, подключенным к управляющему терминалу (control terminal) при отключении терминала. (Обычно управляющий терминал группы процесса является терминалом пользователя, хотя это и не всегда так. Это понятие изучается более подробно в главе 9.) Он также посылается всем членам сеанса, если завершает работу лидер сеанса (обычно процесс командного интерпретатора), связанного с управляющим терминалом. Это гарантирует, что если не были предприняты специальные меры, то при выходе пользователя из системы завершаются все фоновые процессы, запущенные им (подробно об этом написано в разделе 5.10);
– SIGILL – недопустимая команда процессора (illegal instruction). Посылается операционной системой, если процесс пытается выполнить недопустимую машинную команду. Иногда этот сигнал может возникнуть из-за того, что программа каким-либо образом повредила свой код, хотя это и маловероятно. Более вероятной представляется попытка выполнения вещественной операции, не поддерживаемой оборудованием. В результате сигнала SIGILL происходит аварийное завершение программы;
– SIGINT – сигнал прерывания программы (interrupt). Посылается ядром всем процессам сеанса, связанного с терминалом, когда пользователь нажимает клавишу прерывания. Это также обычный способ остановки выполняющейся программы;
– SIGKILL – сигнал уничтожения процесса (kill). Это довольно специфически сигнал, который посылается от одного процесса к другому и приводит к немедленному прекращению работы получающего сигнал процесса. Иногда он также посылается системой (например, при завершении работы системы). Сигнал SIGKILL – один из двух сигналов, которые не могут игнорироваться или перехватываться (то есть обрабатываться при помощи определенной пользователем процедуры);
– SIGPIPE – сигнал о попытке записи в канал или сокет, для которых принимающий процесс уже завершил работу (write on a pipe or socket when recipient is terminated).
Программные каналы и сокеты являются другими средствами межпроцессного взаимодействия, которые обсудим в следующих главах. Там же будет рассмотрен и сигнал SIGPIPE;
– SIGPOLL – сигнал о возникновении одного из опрашиваемых событий (pollable event). Этот сигнал генерируется ядром, когда некоторый открытый дескриптор файла становится готовым для ввода или вывода. Тем не менее более удобный способ организации слежения за состояниями некоторого множества открытых файловых дескрипторов заключается в использовании системного вызова select, который подробно описан в главе 7;
– SIGPROF – сигнал профилирующего таймера (profiling time expired). Как было уже упомянуто для сигнала SIGALARM, любой процесс может установить не менее трех таймеров. Второй из этих таймеров может использоваться для измерения времени выполнения процесса в пользовательском и системном режимах. Сигнал SIGPROF генерируется, когда истекает время, установленное в этом таймере, и поэтому может быть использован средством профилирования программы. (Таймер устанавливается заданием первого параметра функции setitimer равным ITIMER_PROF.);
– SIGQUIT – сигнал о выходе (quit). Очень похожий на сигнал SIGINT, этот сигнал посылается ядром, когда пользователь нажимает клавишу выхода используемого терминала. Значение клавиши выхода по умолчанию соответствует символу ASCII FS или Ctrl-\. В отличие от SIGINT, этот сигнал приводит к аварийному завершению и сбросу образа памяти;
– SIGSEGV – обращение к некорректному адресу памяти (invalid memory reference). Сокращение SEGV в названии сигнала означает нарушение границ сегментов памяти (segmentation violation). Сигнал генерируется, если процесс пытается обратиться к неверному адресу памяти. Получение сигнала SIGSEGV приводит к аварийному завершению процесса;
– SIGSTOP – сигнал останова (stop executing). Это сигнал управления заданиями, который останавливает процесс. Его, как и сигнал SIGKILL, нельзя проигнорировать или перехватить;
– SIGSYS – некорректный системный вызов (invalid system call). Посылается ядром, если процесс пытается выполнить некорректный системный вызов. Это еще один сигнал, приводящий к аварийному завершению;
– SIGTERM – программный сигнал завершения (software termination signal). По соглашению, используется для завершения процесса (как и следует из его названия). Программист может использовать этот сигнал для того, чтобы дать процессу время для «наведение порядка», прежде чем посылать ему сигнал SIGKILL. Команда kill по умолчанию посылает именно этот сигнал;
– SIGTRAP – сигнал трассировочного прерывания (trace trap). Это особый сигнал, который в сочетании с системным вызовом ptrace используется отладчиками, такими как sdb, adb, и gdb. Поскольку он предназначен для отладки, его рассмотрение в рамках данной книги не требуется. По умолчанию сигнал SIGTRAP приводит к аварийному завершению;
– SIGTSTP – терминальный сигнал остановки (terminal stop signal). Этот сигнал формируется при нажатии специальной клавиши останова (обычно Ctrl+Z). Сигнал SIGTSTP аналогичен сигналу SIGSTOP, но его можно перехватить или игнорировать;
– SIGTTIN – сигнал о попытке ввода с терминала фоновым процессом (background process attempting read).
Если процесс выполняется в фоновом режиме и пытается выполнить чтение с управляющего терминала, то ему посылается сигнал SIGTTIN. Действие сигнала по умолчанию – остановка процесса;
– SIGTTOU – сигнал о попытке вывода на терминал фоновым процессом (background process attempting write). Аналогичен сигналу SIGTTIN, но генерируется, если фоновый процесс пытается выполнить запись в управляющий терминал. И снова действие по умолчанию – остановка процесса;
– SIGURG – сигнал о поступлении в буфер сокета срочных данных (high bandwidth data is available at a socket).
Этот сигнал сообщает процессу, что по сетевому соединению получены срочные внеочередные данные;
– SIGUSR1 и SIGUSR2 – пользовательские сигналы (user defined signals 1 and 2). Так же, как и сигнал SIGTERM, эти сигналы никогда не посылаются ядром и могут использоваться для любых целей по выбору пользователя;
– SIGVTALRM – сигнал виртуального таймера (virtual timer expired). Как уже упоминалось для сигналов SIGALRM и SIGPROF, каждый процесс может ими не менее трех таймеров. Последний из этих таймеров можно установить так, чтобы он измерял время, которое процесс выполняет в пользовательском режиме. (Таймер устанавливается заданием первого параметра функции setitimer равным ITIMER_VIRTUAL);
– SIGXCPU – сигнал о превышении лимита процессорного времени (CPU time limit exceeded). Этот сигнал посылается процессу, если суммарное процессорное время, занятое его работой, превысило установленный предел. Действие по умолчанию – аварийное завершение;
– SIGXFSZ – сигнал о превышении предела на размер файла (file size limit exceeded). Этот сигнал генерируется, если процесс превысит максимально допустимый размер файла. Действие по умолчанию – аварийное завершение.
Могут встретиться и некоторые другие сигналы, но их наличие зависит от конкретной реализации системы; его не требует спецификация XSI. Большая часть этих сигналов также используется ядром для индикации ошибок, например SIGEMT – прерывание эмулятора (emulator trap) часто обозначает отказ оборудования и зависит от конкретной реализации.[7]
Имена устройств UNIX
Подключенные к системе UNIX периферийные устройства (диски, терминале) принтеры, дисковые массивы и так далее) доступны при помощи их имен в файловой системе. Эти файлы называются файлами устройств (device files). Соответствующие файловым системам разделы дисков также относятся к классу объектов, представленных этими специальными файлами.
В отличие от обычных дисковых файлов, чтение и запись в файлы устройств приводит к пересылке данных напрямую между системой и соответствующим периферийным устройством.
Обычно эти специальные файлы находятся в каталоге /dev. Поэтому, например, имена
/dev/tty00
/dev/console
/dev/pts/as (псевдотерминал для сетевого доступа)
могут соответствовать трем портам терминалов системы, а имена
/dev/lp
/dev/rmt0
/dev/rmt/0cbn
могут обозначать матричный принтер и два накопителя на магнитной ленте. Имена разделов диска могут иметь разнообразный формат, например:
/dev/dsk/c0b0t0d0s3
/dev/dsk/hd0d
В командах оболочки и в программах файлы устройств могут использоваться так же, как и обычные файлы, например, команды
$ cat fred > /dev/lp
$ cat fred > /dev/rmt0
выведут файл fred на принтер и накопитель на магнитной ленте соответственно (если это позволяют права доступа). Очевидно, что пытаться таким образом оперировать разделами диска с файловыми системами – огромный риск. Одна неосторожная команда может привести к случайной потере большого объема ценных данных. Кроме того, если бы права доступа к таким файлам устройств были бы не очень строгими, то продвинутые пользователи могли бы обойти ограничения прав доступа, наложенные на файлы в файловой системе. Поэтому системные администраторы должны задавать для файлов дисковых разделов соответствующие права доступа, чтобы иметь уверенность в том, что такие действия невозможны.
Для доступа к файлам устройств в программе могут использоваться вызовы fdopen, fdclose, fdread и fdwrite, например, программа
uses linux;
var
i,fd:integer;
begin
fd := fdopen ('/dev/tty', Open_WRONLY);
for i := 1 to 100 do
fdwrite(fd, 'x', 1);
fdclose(fd);
end.
приведет к выводу 100 символов х на порт терминала tty00. Конечно, работа с терминалом является отдельной важной темой, поэтому она подробнее будет рассмотрена в главе 9.
Именованные каналы, или FIFO
Каналы являются изящным и мощным механизмом межпроцессного взаимодействия. Тем не менее они имеют ряд недостатков.
Первый, и наиболее серьезный из них, заключается в том, что каналы могут использоваться только для связи процессов, имеющих общее происхождение, таких как родительский процесс и его потомок. Это ограничение становится видным при попытке разработать настоящую «серверную» программу, которая выполняется постоянно, обеспечивая системный сервис. Примерами таких программ являются серверы управления сетью и спулеры печати. В идеале клиентские процессы должны иметь возможность стартовать, подключаться к не связанному с ними серверному процессу при помощи канала, а затем снова отключаться от него. К сожалению, такую модель при помощи обычных каналов реализовать нельзя.
Второй недостаток каналов заключается в том, что они не могут существовать постоянно. Они каждый раз должны создаваться заново, а после завершения обращающегося к ним процесса уничтожаются.
Для восполнения этих недостатков существует разновидность канала, называемая именованным каналом, или файлом типа FIFO
(сокращение от first-in first-out, то есть «первый вошел/первым вышел»). В отношении вызовов fdread и fdwrite именованные каналы идентичны обычным. Тем не менее, в отличие от обычных каналов, именованные каналы являются постоянными и им присвоено имя файла системы UNIX. Именованный канал также имеет владельца, размер и связанные с ним права доступа. Он может быть открыт, закрыт и удален, как и любой файл UNIX, но при чтении или записи ведет себя аналогично каналу.
Прежде чем рассматривать применение каналов FIFO на программном уровне, рассмотрим их использование на уровне команд. Для создания именованного канала используется команда mknod:
$ /etc/mknod channel p
Первый аргумент channel является именем канала FIFO (в качестве него можно задать любое допустимое имя UNIX). Параметр р
команды mknod указывает, что нужно создать именованный канал. Этот параметр необходим, так как команда mknod также используется для создания файлов устройств.
Некоторые атрибуты вновь созданного канала FIFO можно вывести при помощи команды ls:
$ ls -l channel
prw-rw-r- 1 ben usr 0 Aug 1 21:05 channel
Символ р
в первой колонке обозначает, что channel является файлом типа FIFO. Обратите внимание на права доступа к именованному каналу channel (чтение/запись для владельца и группы владельца, только чтение для всех остальных пользователей); владельца и группу владельца (ben, usr); размер (0 байт, то есть в настоящий момент канал пуст) и время создания.
При помощи стандартных команд
UNIX можно выполнять чтение из канала FIFO и запись в него, например:
$ cat < channel
Если выполнить эту команду сразу же после создания именованного канала channel, то она «зависнет». Это происходит из-за того, что процесс, открывающий канал FIFO на чтение, по умолчанию будет блокирован до тех пор, пока другой процесс не попытается открыть канал FIFO для записи. Аналогично процесс, пытающийся открыть канал FIFO для записи, будет блокирован до тех пор, пока другой процесс не попытается открыть его для чтения. Это благоразумный подход, так как он экономит системные ресурсы и облегчает координацию работы программы. Вследствие этого, при необходимости создания одновременно как записывающего, так и читающего процессов, потребуется запустить один из них в фоновом режиме (или с другого терминала, или псевдотерминала xterm графического интерфейса), например:
$ cat < channel &
102
$ ls -l > channel; wait
total 17
prw-rw-r- 1 ben usr 0 Aug 1 21:05 channel
-rw-rw-r- 1 ben usr 0 Aug 1 21:06 f
-rw-rw-r- 1 ben usr 937 Jul 27 22:30 fifos
-rw-rw-r- 1 ben usr 7152 Jul 27 22:11 pipes.cont
Проанализируем подробнее этот результат. Содержимое каталога вначале выводится при помощи команды ls, а затем записывается в канал FIFO. Ожидающая команда cat затем считывает данные из канала FIFO и выводит их на экран. После этого процесс, выполняющий команду cat, завершает работу. Это происходит из-за того, что канал FIFO больше не открыт для записи, чтение из него будет безуспешным, как и для обычного канала, что команда cat понимает как достижение конца файла. Команда же wait заставляет командный интерпретатор ждать завершения команды cat перед тем, как снова вывести приглашение командной строки.
Информация о файловой системе
Для устройств, которые представляют файловые системы, применимы две функции, сообщающие основную информацию о файловой системе, – полное число блоков, число свободных блоков, число свободных индексных дескрипторов и т.д. Это функции fsstat.
Интерфейс сокетов
Для хранения информации об адресе и порте адресата (абонента) существуют стандартные структуры. Обобщенная структура адреса сокета определяется в модуле sockets следующим образом:
TSockAddr=packed Record
family:word; (* Семейство адресов *)
data :array [0..13] of char; (* Адрес сокета *)
end;
Эта структура называется обобщенным сокетом (generic socket), так как в действительности применяются различные типы сокетов в зависимости от того, используются ли они в качестве средства межпроцессного взаимодействия на одном и том же компьютере или для связи процессов через сеть. Сокеты для связи через сеть имеют следующую форму:
uses sockets;
TInetSockAddr = packed Record
family : Word; (* Семейство адресов *)
port : Word; (* Номер порта *)
addr : Cardinal; (* IP-адрес *)
pad : array [1..8] of byte; (* Поле выравнивания *)
end;
Интернационализация
Многие версии ОС UNIX могут поддерживать различные национальные среды – позволяют учитывать языковые среды, менять порядок сортировки строк, задавать символы денежных единиц, форматы чисел и т.д. Попробуйте найти процедуры setlocale или catopen. Справочное руководство системы может содержать дополнительные сведения по этой теме под заголовком environ.
Использование каналов в программе
Каналы создаются в программе при помощи системного вызова AssignPipe. В случае удачного завершения вызов сообщает два дескриптора файла: один для записи в канал, а другой для чтения из него. Вызов AssignPipe определяется следующим образом:
Использование каталогов при программировании
Как уже упоминалось, для работы с каталогами существует особое семейство системных вызовов. Главным образом эти вызовы работают со структурой dirent, которая определена в модуле linux и содержит следующие элементы:
PDirent = ^Dirent;
Dirent = Record
ino, (* Номер индексного дескриптора *)
off : longint;
reclen : word;
name : string[255] (* Имя файла *)
end;
Спецификация XSI не определяет размер name, но гарантирует, что число байтов, предшествующих нулевому символу, будет меньше, чем число, хранящееся в переменной _PC_NAME_MAX, определенной в заголовочном файле <unistd.h>. Обратите внимание, что нулевое значение переменной ino обозначает пустую запись в каталоге.
Использование системного вызова select для работы с несколькими каналами
Для простых приложений применение неблокирующих операций чтения и записи работает прекрасно. Для работы с множеством каналов одновременно существует другое решение, которое заключается в использовании системного вызова select.
Представьте ситуацию, когда родительский процесс выступает в качестве серверного процесса и может иметь произвольное число связанных с ним клиентских (дочерних) процессов, как показано на рис. 7.4.
В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 7.3.
Дочерний процесс 1 | Родительский процесс | ||||||||
fdwrite() | fdw1 > > | > > fdr1 | fdread() | ||||||
Дочерний процесс 2 | |||||||||
fdwrite() | fdw2 > > | > > fdr2 | fdread() | ||||||
Рис. 7.4. Клиент/сервер с использованием каналов
В этом случае серверный процесс должен как-то справляться с ситуацией, когда одновременно в нескольких каналах может находиться информация, ожидающая обработки. Кроме того, если ни в одном из каналов нет ожидающих данных, то может иметь смысл приостановить работу серверного процесса до их появления, а не опрашивать постоянно каналы. Если информация поступает более чем по одному каналу, то серверный процесс должен знать обо всех таких каналах для того, чтобы работать с ними в правильном порядке (например, согласно их приоритетам).
Это можно сделать при помощи системного вызова
select (существует также аналогичный вызов poll). Системный вызов
select используется не только для каналов, но и для обычных файлов, терминальных устройств, именованных каналов (которые будут рассмотрены в разделе 7.2) и сокетов (им посвящена глава 10). Системный вызов select показывает, какие дескрипторы файлов из заданных наборов готовы для чтения, записи или ожидают обработки ошибок. Иногда серверный процесс не должен совсем прекращать работу, даже если не происходит никаких co6ытий, поэтому в вызове select также можно задать предельное время ожидания.
Использование стандартного вывода диагностики
Стандартный вывод диагностики является особым файловым дескриптором, который по принятому соглашению зарезервирован для сообщений об ошибках и для предупреждений, что позволяет программе отделить обычный вывод от сообщений об ошибках. Например, использование стандартного вывода диагностики позволяет программе выводить сообщения об ошибках на терминал, в то время как стандартный вывод записывается в файл. Тем не менее при необходимости стандартный вывод диагностики может быть перенаправлен аналогично перенаправлению стандартного вывода. Например, часто используется такая форма команды запуска системы make:
$ make > log.out 2>log.err
В результате все сообщения об ошибках работы make направляются в файл log.err, а стандартный вывод направляется в файл log.out.
Можно выводить сообщения в стандартный вывод диагностики при помощи системного вызова write со значением дескриптора файла равным 2:
var
msg:array [0..5] of char='boob'#$a;
.
.
fdwrite(2, msg, 5);
Тем не менее это достаточно грубый и громоздкий способ. Мы приведем лучшее решение в конце этой главы.
Изменение группы процесса
В оболочке UNIX, поддерживающей управление заданиями, может потребоваться переместить процесс в другую группу процессов. Управление заданиями позволяет оболочке запускать несколько групп процессов (заданий) и контролировать, какие группы процессов должны выполняться на переднем плане и, следовательно, иметь доступ к терминалу, а какие должны выполняться в фоне. Управление заданиями организуется при помощи сигналов.
Процесс может создать новую группу процессов или присоединиться к существующей при помощи системного вызова setpgid:
Изменение свойств терминала: структура termios
На уровне оболочки пользователь может вызвать команду stty для изменения свойств дисциплины линии связи терминала. Программа может сделать практически то же самое, используя структуру termios вместе с соответствующими функциями. Обратите внимание, что в более старых системах для этого использовался системный вызов ioctl (сокращение от
I/O control –
управление вводом/выводом), его применение было описано в первом издании этой книги. Вызов ioctl предназначен для более общих целей и теперь разделен на несколько конкретных вызовов. Совокупность этих вызовов обеспечивает общий программный интерфейс ко всем асинхронным портам связи, независимо от свойств их оборудования.
Структуру termios можно представлять себе как объект, способный описать общее состояние терминала в соответствии с набором флагов, поддерживаемым системой для любого терминального устройства. Точное определение структуры termios будет вскоре рассмотрено. Структуры termios могут заполняться текущими установками терминала при помощи вызова tcgetattr, определенного следующим образом:
Изменение владельца при помощи вызова chown
Вызов chown используется для изменения владельца и группы файла.
Эффективность вызовов fdread и fdwrite
Процедура copyfile дает возможность оценить эффективность примитивов доступа к файлам в зависимости от размера буфера. Один из методов заключается просто в компиляции copyfile с различными значениями BUFSIZE, а затем в измерении времени ее выполнения при помощи команды UNIX time. Мы сделали это, используя программу
(* Программа для тестирования функции copyfile *)
begin
copyfile('test.in', 'test.out');
end.
и получили при копировании одного и того же большого файла (68307 байт) на компьютере с системой SVR4 UNIX для диска, разбитого на блоки по 512 байт, результаты, приведенные в табл. 2.2.
Таблица 2.2. Результаты тестирования функции copyfile
BUFSIZE | Real time | User time | System time | ||||
1 | 0:24.49 | 0:3.13 | 0:21.16 | ||||
64 | 0:0.46 | 0:0.12 | 0:0.33 | ||||
512 | 0:0.12 | 0:0.02 | 0:0.08 | ||||
4096 | 0:0.07 | 0:0.00 | 0:0.05 | ||||
8192 | 0:0.07 | 0:0.01 | 0:0.05 |
Формат данных в таблице отражает вывод команды time. В первом столбце приведены значения BUFSIZE, во втором – действительное время выполнения процесса в минутах, секундах и десятых долях секунды. В третьем столбце приведено «пользовательское» время, то есть время, занятое частями программы, не являющимися системными вызовами. Из-за дискретности используемого таймера одно из значений в таблице ошибочно записано как нулевое. В последнем, четвертом, столбце приведено время, затраченное ядром на обслуживание системных вызовов. Как видно из таблицы, третий и четвертый столбцы в сумме не дают действительное время выполнения. Это связано с тем, что в системе UNIX одновременно выполняется несколько процессов. Не все время тратится на выполнение ваших программ!
Полученные результаты достаточно убедительны – чтение и запись по одному байту дает очень низкую производительность, тогда как увеличение размера буфера значительно повышает производительность. Наибольшая производительность достигается, если BUFSIZE кратно размеру блока диска на диске, как видно из результатов, для значений BUFSIZE 512, 4096 и 8192 байта.
Следует также отметить, что большая часть прироста (но не всего) эффективности получается просто от уменьшения числа системных вызовов. Переключение между программой и ядром может обойтись достаточно дорого. В общем случае, если нужна максимальная производительность, следует минимизировать число генерируемых программой системных вызовов.
Функция fdFlush позволяет опустошить файловый буфер ядра UNIX для того, чтобы файл был действительно записан на диск.
Эхо-отображение вводимых символов и опережающий ввод с клавиатуры
Поскольку терминалы используются для взаимодействия между людьми и компьютерными программами, драйвер терминала UNIX
поддерживает множество дополнительных средств, облегчающих жизнь пользователям.
Возможно, самым элементарным из этих дополнительных средств является «эхо», то есть отображение вводимых с клавиатуры символов на экране. Оно позволяет увидеть на экране символ «А», когда вы печатаете «А» на клавиатуре. Подключенные к системам UNIX терминалы обычно работают в полнодуплексном (full-duplex) режиме; это означает, что за эхо-отображение символов на экране отвечает система UNIX, а не терминал. Следовательно, при наборе символа он вначале передается терминалом системе UNIX. После его получения дисциплина линии связи сразу же помещает его копию в очередь вывода терминала. Затем символ выводится на экран терминала. Если вернуться к рис. 9.1, то увидим, что символ при этом пересылается по пути от (С) к (В), а затем сразу же направляется по пути от (В) к (D). Все может произойти до того, как программа (А) успеет считать символ. Это приводит к интересному явлению, когда символы, вводимые с клавиатуры в то время, когда программа осуществляет вывод на экран, отображаются в середине вывода. В операционных системах некоторых семейств (не UNIX) отображение вводимых символов на экране может подавляться до тех пор, пока программа не сможет прочитать их.
Каналы и системный вызов ехес
Вспомним, как можно создать канал между двумя программами с помощью командного интерпретатора:
$ ls | wc
Как это происходит? Ответ состоит из двух частей. Во-первых, командный интерпретатор использует тот факт, что открытые дескрипторы файлов остаются открытыми (по умолчанию) после вызова ехес. Это означает, что два файловых дескриптора канала, которые были открыты до выполнения комбинации вызовов fork/ехес, останутся открытыми и когда дочерний процесс начнет выполнение новой программы. Во-вторых, перед вызовом ехес командный интерпретатор соединяет стандартный вывод программы ls
с входом канала, а стандартный ввод программы wc – с выходом канала. Это можно сделать при помощи вызова fcntl или dup2, как было показано в упражнении 5.10. Так как значения дескрипторов файлов, соответствующих стандартному вводу, стандартному выводу и стандартному выводу диагностики, равны 0, 1 и 2 соответственно, то можно, например, соединить стандартный вывод с другим дескриптором файла, используя вызов dup2 следующим образом. Обратите внимание, что перед переназначением вызов dup2 закрывает файл, представленный его вторым параметром.
(* Вызов dup2 будет копировать дескриптор файла 1 *)
dup2(filedes, 1);
.
.
.
(* Теперь программа будет записывать свой стандартный *)
(* вывод в файл, заданный дескриптором filedes *)
.
.
.
Следующий пример, программа join, демонстрирует механизм каналов, задействованный в упрощенном командном интерпретаторе. Программа join имеет два параметра, com1 и com2, каждый из которых соответствует выполняемой команде. Оба параметра в действительности являются массивами строк, которые будут переданы вызову execvp.
Родительский процесс | |||||||||
wait() | |||||||||
Потомок дочернего процесса (com1) | Дочерний процесс (com2) | ||||||||
> > fdin | fdread() | ||||||||
^ | (stdin) | ||||||||
fdwrite() | fdout > > | ||||||||
(stdout) |
Рис. 7.5. Программа join
Программа join запустит обе программы на выполнение и свяжет стандартный вывод программы com1 со стандартным вводом программы com2. Работа программы join изображена на рис. 7.5 и может быть описана следующей схемой (без учета обработки ошибок):
процесс порождает дочерний процесс и ожидает действий от него
дочерний процесс продолжает работу
дочерний процесс создает канал
затем дочерний процесс порождает еще один дочерний процесс
В потомке дочернего процесса:
стандартный вывод подключается
к входу канала при помощи вызова dup2
ненужные дескрипторы файлов закрываются
при помощи вызова ехес запускается программа,
заданная параметром 'com1'
В первом дочернем процессе:
стандартный ввод подключается
к выходу канала при помощи вызова dup2
ненужные дескрипторы файлов закрываются
при помощи вызова ехес запускается программа,
заданная параметром 'com2'
Далее следует реализация программы join; она также использует процедуру fatal, представленную в разделе 7.1.5.
(* Программа join - соединяет две программы каналом *)
function join (com1, com2:ppchar):integer;
var
fdin,fdout:longint;
status:integer;
begin
(* Создать дочерний процесс для выполнения команд *)
case fork of
-1: (* ошибка *)
fatal ('Ошибка 1 вызова fork в программе join');
0: (* дочерний процесс *)
;
else (* родительский процесс *)
begin
wait(@status);
join:=status;
exit;
end;
end;
(* Остаток процедуры, выполняемой дочерним процессом *)
(* Создать канал *)
if not assignpipe(fdin,fdout) then
fatal ('Ошибка вызова pipe в программе join');
(* Создать еще один процесс *)
case fork of
-1:
(* ошибка *)
fatal ('Ошибка 2 вызова fork в программе join');
0:
begin
(* процесс, выполняющий запись *)
dup2 (fdout, 1); (* направить ст. вывод в канал *)
fdclose (fdin); (* сохранить дескрипторы файлов *)
fdclose (fdout);
execvp (com1[0], com1, envp);
(* Если execvp возвращает значение, то произошла ошибка *)
fatal ('Ошибка 1 вызова execvp в программе join');
end;
else
begin
(* процесс, выполняющий чтение *)
dup2 (fdin, 0); (* направить ст. ввод из канала *)
fdclose (fdin);
fdclose (fdout);
execvp (com2[0], com2, envp);
fatal ('Ошибка 2 вызова execvp в программе join');
end;
end;
end;
Эту процедуру можно вызвать следующим образом:
uses linux, stdio;
const
one:array [0..3] of pchar = ('ls', '-l', '/usr/lib', nil);
two:array [0..2] of pchar = ('grep', '^d', nil);
var
ret:integer;
begin
ret := join (one, two);
writeln ('Возврат из программы join ', ret);
halt (0);
end.
Упражнение 7.3. Как можно обобщить подход, показанный в программе join, для связи нескольких процессов при помощи каналов?
Упражнение 7.4. Добавьте возможность работы с каналами в командный интерпретатор smallsh, представленный в предыдущей главе.
Упражнение 7.5. Придумайте метод, позволяющий родительскому процессу запускать программу в качестве дочернего процесса, а затем считывать ее стандартный вывод при помощи канала. Стоит отметить, что эта идея лежит в основе процедур popen/pipeopen и pclose/pipeclose, которые входят в стандартную библиотеку ввода/вывода. Процедуры popen/pipeopen и pclose/pipeclose избавляют программиста от большинства утомительных деталей согласования вызовов fork, ехес, fdclose, dup или dup2. Эти процедуры обсуждаются в главе 11.
Каналы на уровне команд
Большинство пользователей UNIX уже сталкивались с конвейерами команд:
$ pr doc | lр
Этот конвейер организует совместную работу команд pr и lр. Символ | в командной строке сообщает командному интерпретатору, что необходимо создать канал, соединяющий стандартный вывод команды pr
со стандартным вводом команды lр. В результате этой команды на матричный принтер будет выведена разбитая на страницы версия файла doc.
Разобьем командную строку на составные части. Программа pr слева от символа, обозначающего канал, ничего не знает о том, что ее стандартный вывод посылается в канал. Она выполняет обычную запись в свой стандартный вывод, не предпринимая никаких особых мер. Аналогично программа lр
справа выполи чтение точно так же, как если бы она получала свой стандартный ввод с клавиатуры или из обычного файла.[10]
Результат в целом будет таким же, как при выполнении следующей последовательности команд:
$ pr doc > tmpfile
$ lр < tmpfile
$ rm tmpfile
Управление потоком в канале осуществляется автоматически и прозрачно для процесса. Поэтому, если программа pr будет выводить информацию слишком быстро, то ее выполнение будет приостановлено. После того как программа lр
догонит программу pr, и количество данных, находящихся в канале, упадет до приемлемого уровня, выполнение программы pr
продолжится.
Каналы являются одной из самых сильных и характерных особенностей ОС UNIX, доступных даже с уровня командного интерпретатора. Они позволяют легко соединять между собой произвольные последовательности команд. Поэтому программы UNIX могут разрабатываться как простые инструменты, осуществляющие чтение из стандартного ввода, запись в стандартный вывод и выполняют одну, четко определенную задачу. При помощи каналов из этих основных блоков могут быть построены более сложные командные строки, например, команда
$ who | wc -l
направляет вывод программы who в программу подсчета числа слов wc, а задание параметра -l
в программе wc
определяет, что необходимо подсчитывать только число строк. Таким образом, в конечном итоге программа
wc выводит число находящихся в системе пользователей (иногда нужно исключить из суммы первую строку-заголовок вывода who).
Канонический режим, редактирование строки и специальные символы
Терминал может быть настроен на самые разные режимы в соответствии с типом работающих с ним программ; работа этих режимов поддерживается соответствующей дисциплиной линии связи. Например, экранному редактору может понадобиться максимальная свобода действий, поэтому он может перевести терминал в режим прямого доступа (raw mode), при котором дисциплина линии связи просто передает символы программе по мере их поступления, без всякой обработки.
Вместе с тем ОС UNIX не была бы универсальной программной средой, если бы всем программам приходилось бы иметь дело с деталями управления терминалом. Поэтому стандартная дисциплина линии связи обеспечивает также режим работы, специально приспособленный для простого интерактивного использования на основе построчного ввода. Этот режим называется каноническим режим терминала (canonical mode) и используется командным интерпретатором, редактором ed и аналогичными программами.
В каноническом режиме драйвер терминала выполняет специальные действия при нажатии специальных клавиш. Многие из этих действий относятся к редактированию строки. Вследствие этого, если терминал находится в каноническом режиме, то ввод в программе осуществляется целыми строками (подробнее об этом ниже).
Наиболее часто используемой в каноническом режиме клавишей редактирования является клавиша erase, нажатие которой приводит к стиранию предыдущего символа в строке. Например, следующий клавиатурный ввод
$ whp<erase>o
за которым следует перевод строки, приведет к тому, что драйвер терминала передаст командному интерпретатору строку
who. Если терминал настроен правильно, то символ р должен быть стерт с экрана.
В качестве символа erase пользователем может быть задано любое значение ASCII. Наиболее часто для этих целей используется символ ASCII backspace (возврат).
На уровне оболочки проще всего поменять этот символ при помощи команды stty, например
$ stty erase "^h"
задает в качестве символа erase комбинацию Ctrl+H,
которая является еще одним именем для символа возврата. Обратите внимание, что зачастую можно набрать строку "^h", а не нажимать саму комбинацию клавиш Ctrl+H.
Ниже следует систематическое описание других символов, определенных в спецификации XSI и имеющих особое значение для оболочки в каноническом режиме. Если не указано иначе, их точные значения могут быть установлены пользователем или администратором системы.
kill |
Приводит к стиранию всех символов до начала строки. Поэтому входная последовательность $ echo < kill > who за которой следует новая строка, приводит к выполнению команды who. По умолчанию значение символа kill равно Ctrl+?, часто также используются комбинации клавиш Ctrl+X и Ctrl+U. Можно поменять символ kill при помощи команды: $ stty kill <новый_символ> |
intr |
Символ прерывания. Если пользователь набирает его, то программе, выполняющей чтение с терминала, а также всем другим процессам, которые считают терминал своим управляющим терминалом, посылается сигнал SIGINT. Такие программы, как командный интерпретатор, перехватывают этот сигнал, поскольку по умолчанию сигнал SIGINT приводит к завершению программы. Одно из значений для символа intr по умолчанию равно символу ASCII delete (удалить), который часто называется DEL. Вместо него может также использоваться символ Ctrl+C. Для изменения текущего значения символа intr на уровне оболочки используйте команду: $ stty intr <новый_символ> |
quit |
Обратитесь к главе 6 за описанием обработки сигналов quit. Если пользователь набирает этот символ, то группе процессов, связанной с терминалом, посылается сигнал SIGQUIT. Командный интерпретатор перехватывает этот сигнал, остальные пользовательские процессы также имеют возможность перехватить и обработать этот сигнал. Как уже описывалось в главе 6, действием этого сигнала по умолчанию является сброс образа памяти процесса на диск и аварийное завершение, сопровождаемое выводом сообщения «Quit – core dumped». Обычно символ quit – это символ ASCII FS, или Ctrl+\. Его можно изменить, выполнив команду: $ stty quit <новый_символ> |
eof |
Этот символ используется для обозначения окончания входного потока с терминала (для этого он должен быть единственным символом в начале новой строки). Стандартным начальным значением для него является символ ASCII eof, известный также как Ctrl+D. Его можно изменить, выполнив команду: $ stty eof <новый_символ> |
nl |
Это обычный разделитель строк. Он всегда имеет значение ASCII символа line-feed (перевод строки), который соответствует символу newline (новая строка) в языке С. Он не может быть изменен или установлен пользователем. На терминалах, которые посылают вместо символа line-feed символ carriage-return (возврат каретки), дисциплина линии связи может быть настроена так, чтобы преобразовывать возврат каретки в перевод строки |
еоl |
Еще один разделитель строк, который действует так же, как и nl. Обычно не используется и поэтому имеет по умолчанию значение символа ASCII NULL |
stop |
Обычно имеет значение Ctrl+S и в некоторых реализациях может быть изменен пользователем. Используется для временной приостановки записи вывода на терминал. Этот символ особенно полезен при применении старомодных терминалов, так как он может использоваться для того, чтобы приостановить вывод прежде, чем он исчезнет за границей экрана терминала |
start |
Обычно имеет значение Ctrl+Q. Может ли он быть изменен, также зависит от конкретной реализации. Он используется для продолжения вывода, приостановленного при помощи комбинации клавиш Ctrl+S. Если комбинация Ctrl+S не была нажата, то Ctrl+Q игнорируется |
susp |
Если пользователь набирает этот символ, то группе процессов, связанной с терминалом, посылается сигнал SIGTSTP. При этом выполнение группы приоритетных процессов останавливается, и она переводится в фоновый режим. Обычное значение символа suspend равно Ctrl+Z. Его также можно изменить, выполнив команду: $ stty susp <новый_символ> |
можно отменить, набрав перед ними символ обратной косой черты (\). При этом связанная с символом функция не выполняется, и символ посылается программе без изменений. Например, строка
aa\<erase> b <erase> с
приведет к тому, что программе, выполняющей чтение с терминала, будет передана строка аа\<erase> с.
Каталоги
Упражнение 13.28. Напишите аналог команды ls -l.
uses linux,strings,sysutils; (*для системных вызовов Linux и работы со строками PChar*)
function ctime(var time_t:longint):pchar;cdecl;external 'c';
function gettype(t:word):char;forward; (*тип объекта ф.с. в формате команды ls*)
(*тип объекта ф.с. в формате команды ls*)
function gettype(t:word):char;
begin
if S_ISDIR(t) then (*проверка на каталог*)
gettype:='d'
else
if S_ISREG(t) then (*проверка на обычный файл*)
gettype:='-'
else
if S_ISBLK(t) then (*проверка на блочное устройство*)
gettype:='b'
else
if S_ISCHR(t) then (*проверка на символьное устройство*)
gettype:='c'
else
if S_ISFIFO(t) then (*проверка на именованный программный канал*)
gettype:='p'
else
if S_ISLNK(t) then (*проверка на сиволическую ссылку*)
gettype:='l'
else
gettype:='?';
end;
function getrights(r:word):string;
var
u, (*права для владельца*)
g, (*права для группы*)
o, (*права для всех остальных*)
s, (*специальные права*)
i:integer;
res:string; (*права в символьной форме*)
const
o7777=(1 shl 12)-1; (*восьмеричная константа = все 12 бит прав заданы *)
o10 =8; (*010 *)
o100 =64; (*0100 *)
o1000=512; (*01000*)
symrights:array [0..7] of string=( (*базовые комбинации прав в символьной форме*)
'---', (*0 = 000*)
'--x', (*1 = 001*)
'-w-', (*2 = 010*)
'-wx', (*3 = 011*)
'r--', (*4 = 100*)
'r-x', (*5 = 101*)
'rw-', (*6 = 110*)
'rwx' (*7 = 111*)
);
spec='tss'; (*массив специальных прав доступа*)
begin
(*обрезаем старшие биты, не относящиеся к правам доступа (тип файла и т.п.)*)
r:=r and o7777;(*восьмеричная константа 10000-1==1*8^4-1==1*(2^3)^4-1==2^12-1 *)
(* выделяем числовые права для владельца, группы, остальных + специальные*)
o:=r mod o10;
s:=r div o1000;
u:=(r div o100) mod o10;
g:=(r mod o100) div o10;
res:=symrights[u]+symrights[g]+symrights[o];(*формируем символьыне права из базовых троек*)
for i:=1 to 3 do (*цикл проверки наличия чпециальных прав*)
if s and (1 shl (i-1)) <> 0 then (*если право установлено*)
if res[12-3*i]='x' then (*если есть обычное право на выполнение*)
res[12-3*i]:=spec[i] (*заносим маленькую букву*)
else
res[12-3*i]:=upcase(spec[i]); (*иначе - большую*)
getrights:=res; (*возвращаем результат - 9-символьное представление 12-битных прав*)
end;
var
d:^TDir; (*указатель на запись для работы с каталогом*)
elem:^Dirent; (*указатель на запись, хранящую один элекмент каталога*)
tekkat, (*строка для хранения имени каталога*)
fullpath (*полный путь к элементу каталога*)
:array [0..1000] of char;
st:stat; (*для хранения информации о файле или каталоге*)
begin
if paramcount=0 then (*если в командной строке не указан каталог*)
strcopy(tekkat,'.') (*то в качестве каталога используем текущий*)
else
tekkat:=paramstr(1); (*иначе используем каталог из командной строки*)
if not access(pchar(tekkat),F_OK or R_OK) then (*F_OK - проверка сущестования объекта ф.с.*)
begin
writeln('Каталог ', tekkat, ' не существует или недоступен для чтения'); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
if not fstat(pchar(tekkat),st) then (*попытка получения информации о файле или каталоге*)
begin
writeln('Ошибка получения информации о каталоге ', tekkat); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
if not S_ISDIR(st.mode) then (*проверка на каталог*)
begin
writeln(tekkat, ' - не каталог'); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
d:=opendir(tekkat); (*попытка открытия каталога для чтения*)
if d=nil then (*если попытка не удалась*)
begin
writeln(' Ошибка вызова opendir для каталога ', tekkat); (*диагностика*)
halt(1); (*возврат в предыдущую программу*)
end;
elem:=readdir(d); (*попытка чтения элемента каталога*)
while elem<>nil do
begin
(*формирование полного имени элемента каталога*)
strcopy(fullpath,tekkat); (*копируем имя текущего каталога в начало полного имени*)
if strcomp(tekkat,'/')<>0 then(*если текущий каталог - не корневой*)
begin
if fullpath[strlen(fullpath)-1]='/' then (*если в конце имени каталога слэш*)
fullpath[strlen(fullpath)-1]:=#0; (*заменяем его признаком конца строки*)
strcat(fullpath,'/'); (*добавляем после имени каталога слэш-разделитель*)
end;
strcat(fullpath,elem^.name); (*и имя элемента каталога*)
if not fstat(pchar(fullpath),st) then (*попытка получения информации о файле или каталоге*)
begin
writeln('Ошибка получения информации о ', fullpath); (*диагностика*)
continue; (*возврат в предыдущую программу*)
end;
{gmtime_r(st.mtime,mytm);}
writeln(gettype(st.mode),getrights(st.mode),st.nlink:5,
' ',st.size:10,' ',ctime(st.mtime), elem^.name); (*вывод имени элемента каталога*)
elem:=readdir(d); (*попытка чтения элемента каталога*)
end;
closedir(d); (*закрытие открытого opendir каталога*)
end.
Упражнение 13.29. Составьте аналог команды vdir.
uses linux,strings,sysutils;
function getname(uid:integer):string;
const w='/etc/passwd';
var ts,nam1,namb1:string;
tx:text;
begin
assign(tx,w);
reset(tx);
while not EOF (tx) do
begin
readln(tx,ts);
uid:=pos(':',ts);
nam1:=copy(ts,1,uid-1);
delete(ts,1,uid);
uid:=pos(':',ts);
delete(ts,1,uid);
namb1:=copy(ts,1,uid-1);
if namb1='500' then
write(nam1)
end;
close(tx);
getname:=nam1;
end;
function getgroup(gid:integer):string;
const q='/etc/group';
var ts,nam,namb:string;
t:text;
begin
assign(t,q);
reset(t);
while not EOF (t) do
begin
readln(t,ts);
gid:=pos(':',ts);
nam:=copy(ts,1,gid-1);
delete(ts,1,gid);
gid:=pos(':',ts);
delete(ts,1,gid);
namb:=copy(ts,1,gid-1);
if namb='500' then
write(nam);
end;
close(t);
getgroup:=nam;
end;
function gettype(mode:integer):char;
begin
if S_ISREG(mode) then
gettype:='-'
else
if S_ISDIR(mode) then
gettype:='d'
else
if S_ISCHR(mode) then
gettype:='c'
else
if S_ISBLK(mode) then
gettype:='b'
else
if S_ISFIFO(mode) then
gettype:='p'
else
gettype:='l';
end;
function getrights(mode:integer):string;
const
sympr:array [0..7] of string=(
'---', {0}
'--x', {1}
'-w-', {2}
'-wx', {3}
'r--', {4}
'r-x', {5}
'rw-', {6}
'rwx' {7}
);
specsympr:array [0..7] of string=(
'---', {0}
'--t', {1}
'-s-', {2}
'-st', {3}
's--', {4}
's-t', {5}
'ss-', {6}
'sst' {7}
);
var
s,u,g,o,i:integer;
res:string;
begin
mode:=mode and octal(7777);
u:=(mode div octal(100)) mod octal(10);
g:=(mode mod octal(100)) div octal(10);
o:=mode mod octal(10);
s:=mode div octal(1000);
res:=sympr[u]+sympr[g]+sympr[o];
for i:=1 to 3 do
if specsympr[s][i]<>'-' then
begin
if res[3*i]='-' then
res[3*i]:=upcase(specsympr[s][i])
else
res[3*i]:=specsympr[s][i];
end;
getrights:=res;
end;
var
d:PDIR;
el:pdirent;
st:stat;
res:integer;
dt:tdatetime;
polniypath,name:array [0..2000] of char;
begin
if paramcount = 0 then
name:='.'
else
name:=paramstr(1);
d:=opendir(name);
if d=nil then
begin
writeln('Ошибка открытия текущего каталога');
halt(0);
end;
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
{writeln(polniypath,' ',s.size);}
dt:=filedatetodatetime(st.mtime);
write(gettype(st.mode),getrights(st.mode),st.nlink:5,
getname(st.uid),' ',getgroup(st.gid),st.size:10,' ',datetimetostr(dt),' ' );
writeln(el^.name);
end;
el:=readdir(d);
end;
closedir(d);
end.
Упражнение 13.30. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего каталога (файла с именем ".") без сортировки имен по алфавиту. Предусмотрите чтение каталога, чье имя задается как аргумент программы. Имена "." и ".." не выдавать.
uses linux,strings,sysutils,crt;
{$linklib c}
type
plong=^longint;
function ctime(r:plong):pchar;cdecl;external;
function strchr(s:string;c:char):boolean;
var
i:integer;
begin
for i:=1 to length(s) do
if s[i]=c then
begin
strchr:=true;
exit;
end;
strchr:=false;
end;
function getall(w:string;uid:integer):string;
{const w='/etc/passwd';}
var ts,nam1,namb1:string;
tx:text;
d:integer;
begin
assign(tx,w);
reset(tx);
while not EOF (tx) do
begin
readln(tx,ts);
d:=pos(':',ts);
nam1:=copy(ts,1,d-1);
delete(ts,1,d+2);
d:=pos(':',ts);
{delete(ts,1,d);}
namb1:=copy(ts,1,d-1);
val(namb1,d);
{writeln('имя = ',nam1,', номер=',namb1);}
if d=uid then
break;
end;
close(tx);
getall:=nam1;
end;
function getname(uid:integer):string;
begin
getname:=getall('/etc/passwd',uid);
end;
function getgroup(gid:integer):string;
begin
getgroup:=getall('/etc/group',gid);
end;
function gettype(mode:integer):char;
begin
if S_ISREG(mode) then
gettype:='-'
else
if S_ISDIR(mode) then
gettype:='d'
else
if S_ISCHR(mode) then
gettype:='c'
else
if S_ISBLK(mode) then
gettype:='b'
else
if S_ISFIFO(mode) then
gettype:='p'
else
gettype:='l';
end;
function getrights(mode:integer):string;
const
sympr:array [0..7] of string=(
'---', {0}
'--x', {1}
'-w-', {2}
'-wx', {3}
'r--', {4}
'r-x', {5}
'rw-', {6}
'rwx' {7}
);
specsympr:array [0..7] of string=(
'---', {0}
'--t', {1}
'-s-', {2}
'-st', {3}
's--', {4}
's-t', {5}
'ss-', {6}
'sst' {7}
);
var
s,u,g,o,i:integer;
res:string;
begin
mode:=mode and octal(7777);
u:=(mode div octal(100)) mod octal(10);
g:=(mode mod octal(100)) div octal(10);
o:=mode mod octal(10);
s:=mode div octal(1000);
res:=sympr[u]+sympr[g]+sympr[o];
for i:=1 to 3 do
if specsympr[s][i]<>'-' then
begin
if res[3*i]='-' then
res[3*i]:=upcase(specsympr[s][i])
else
res[3*i]:=specsympr[s][i];
end;
getrights:=res;
end;
procedure obhod(name:pchar);
var
d:PDIR;
el:pdirent;
st:stat;
res:integer;
dt:tdatetime;
polniypath,datetime:array [0..2000] of char;
i,k:integer;
begin
d:=opendir(name);
if d=nil then
begin
writeln('Ошибка открытия каталога ',name);
exit;
end;
i:=0;
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln(' Ошибка вызова stat для ',polniypath)
else
begin
(*
strcopy(datetime,ctime(@st.mtime)+4);
datetime[12]:=#0;
write(gettype(st.mode),getrights(st.mode),st.nlink:5,' ',
getname(st.uid):10,' ',getgroup(st.gid):10,' ',st.size:10,' ',datetime,' ' );
*)
if(gettype(st.mode)='d') then
textcolor(9);
if(gettype(st.mode)='-') and strchr(getrights(st.mode),'x') then
textcolor(lightgreen);
if(gettype(st.mode)='p') then
textcolor(brown);
if(gettype(st.mode)='l') then
textcolor(lightblue);
if (gettype(st.mode)='c') or (gettype(st.mode)='b') then
textcolor(yellow);
write(el^.name);
for k:=strlen(el^.name) to 15 do
write(' ');
textcolor(7);
end;
el:=readdir(d);
inc(i);
if(i mod 5=0)then writeln;
end;
closedir(d);
if(i mod 5<>0)then writeln;
end;
var
name:array [0..2000] of char;
begin
if paramcount = 0 then
name:='.'
else
name:=paramstr(1);
obhod(name);
end.
Упражнение 13.31. Напишите программу удаления файлов и каталогов, заданных в командной строке. Программа должна удалять каталоги рекурсивно и отказываться удалять файлы устройств.
uses linux,strings,sysutils,crt;
{$linklib c}
type
plong=^longint;
function gettype(mode:integer):char;
begin
if S_ISREG(mode) then
gettype:='-'
else
if S_ISDIR(mode) then
gettype:='d'
else
if S_ISCHR(mode) then
gettype:='c'
else
if S_ISBLK(mode) then
gettype:='b'
else
if S_ISFIFO(mode) then
gettype:='p'
else
gettype:='l';
end;
function obhod(name:pchar):boolean;
var
flag:boolean;
d:PDIR;
el:pdirent;
st:stat;
res:integer;
polniypath:array [0..2000] of char;
begin
flag:=true;
d:=opendir(name);
if d=nil then
begin
writeln('Ошибка открытия каталога ',name);
exit;
end;
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
if not (gettype(st.mode) in ['b','c','d']) then
begin
writeln('Стираю файл ',polniypath);
//unlink(polniypath);
if not unlink(polniypath) then
begin
writeln('невозможно стереть файл ',polniypath);
flag:=false;(*ошибка удаления файла - нельзя будет стереть каталог*)
end;
end;
end;
el:=readdir(d);
end;
closedir(d);
d:=opendir(name);
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
if (gettype(st.mode)='d') and
(strcomp(el^.name,'.')<>0) and
(strcomp(el^.name,'..')<>0) then
begin
writeln('Переход в каталог ',polniypath);
if not obhod(polniypath) then
flag:=false;
end;
end;
el:=readdir(d);
end;
closedir(d);
if not flag then
writeln('Каталог ',name,
' не будет стерт, т.к. в нем не удалось стереть часть файлов или каталогов')
else
begin
{$i-}
rmdir(name);
if ioresult <> 0 then
begin
writeln('Ошибка удаления каталога ',name);
flag:=false;
end;
end;
writeln('Для каталога ',name, ' получен ',flag);
obhod:=flag;
end;
var
name:array [0..2000] of char;
begin
if paramcount<>0 then
begin
name:=paramstr(1);
obhod(name);
end
else
writeln('С особой осторожностью используйте: ',paramstr(0),' удаляемый каталог');
end.
Упражнение 13.32. Напишите функцию рекурсивного обхода дерева подкаталогов и печати имен всех файлов в нем с выдачей атрибутов в форме команды ls -l.
uses linux,strings,sysutils;
{$linklib c}
type
plong=^longint;
function ctime(r:plong):pchar;cdecl;external;
function getall(w:string;uid:integer):string;
{const w='/etc/passwd';}
var ts,nam1,namb1:string;
tx:text;
d:integer;
begin
assign(tx,w);
reset(tx);
while not EOF (tx) do
begin
readln(tx,ts);
d:=pos(':',ts);
nam1:=copy(ts,1,d-1);
delete(ts,1,d+2);
d:=pos(':',ts);
{delete(ts,1,d);}
namb1:=copy(ts,1,d-1);
val(namb1,d);
{writeln('имя = ',nam1,', номер=',namb1);}
if d=uid then
break;
end;
close(tx);
getall:=nam1;
end;
function getname(uid:integer):string;
begin
getname:=getall('/etc/passwd',uid);
end;
function getgroup(gid:integer):string;
begin
getgroup:=getall('/etc/group',gid);
end;
function gettype(mode:integer):char;
begin
if S_ISREG(mode) then
gettype:='-'
else
if S_ISDIR(mode) then
gettype:='d'
else
if S_ISCHR(mode) then
gettype:='c'
else
if S_ISBLK(mode) then
gettype:='b'
else
if S_ISFIFO(mode) then
gettype:='p'
else
gettype:='l';
end;
function getrights(mode:integer):string;
const
sympr:array [0..7] of string=(
'---', {0}
'--x', {1}
'-w-', {2}
'-wx', {3}
'r--', {4}
'r-x', {5}
'rw-', {6}
'rwx' {7}
);
specsympr:array [0..7] of string=(
'---', {0}
'--t', {1}
'-s-', {2}
'-st', {3}
's--', {4}
's-t', {5}
'ss-', {6}
'sst' {7}
);
var
s,u,g,o,i:integer;
res:string;
begin
mode:=mode and octal(7777);
u:=(mode div octal(100)) mod octal(10);
g:=(mode mod octal(100)) div octal(10);
o:=mode mod octal(10);
s:=mode div octal(1000);
res:=sympr[u]+sympr[g]+sympr[o];
for i:=1 to 3 do
if specsympr[s][i]<>'-' then
begin
if res[3*i]='-' then
res[3*i]:=upcase(specsympr[s][i])
else
res[3*i]:=specsympr[s][i];
end;
getrights:=res;
end;
procedure obhod(name:pchar);
var
d:PDIR;
el:pdirent;
st:stat;
res:integer;
dt:tdatetime;
polniypath,datetime:array [0..2000] of char;
begin
d:=opendir(name);
if d=nil then
begin
writeln('Ошибка открытия каталога ',name);
exit;
end;
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln(' Ошибка вызова stat для ',polniypath)
else
begin
strcopy(datetime,ctime(@st.mtime)+4);
datetime[12]:=#0;
write(gettype(st.mode),getrights(st.mode),st.nlink:5,' ',
getname(st.uid):10,' ',getgroup(st.gid):10,' ',st.size:10,' ',datetime,' ' );
writeln(el^.name);
end;
el:=readdir(d);
end;
closedir(d);
d:=opendir(name);
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
if S_ISDIR(st.mode) then
begin
if (strcomp(el^.name,'.')<>0) and (strcomp(el^.name,'..')<>0) then
begin
writeln;
writeln(polniypath,':');
obhod(polniypath);
end;
end;
end;
el:=readdir(d);
end;
closedir(d);
end;
var
name:array [0..2000] of char;
begin
if paramcount = 0 then
name:='.'
else
name:=paramstr(1);
obhod(name);
end.
Упражнение 13.33. Напишите программу удаления каталога, которая удаляет все файлы в нем и, рекурсивно, все его подкаталоги.
uses linux,strings,sysutils,crt;
{$linklib c}
type
plong=^longint;
function gettype(mode:integer):char;
begin
if S_ISREG(mode) then
gettype:='-'
else
if S_ISDIR(mode) then
gettype:='d'
else
if S_ISCHR(mode) then
gettype:='c'
else
if S_ISBLK(mode) then
gettype:='b'
else
if S_ISFIFO(mode) then
gettype:='p'
else
gettype:='l';
end;
function obhod(name:pchar):boolean;
var
flag:boolean;
d:PDIR;
el:pdirent;
st:stat;
res:integer;
polniypath:array [0..2000] of char;
begin
flag:=true;
d:=opendir(name);
if d=nil then
begin
writeln('Ошибка открытия каталога ',name);
exit;
end;
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
if not (gettype(st.mode) = 'd') then
begin
writeln('Стираю файл ',polniypath);
//unlink(polniypath);
if not unlink(polniypath) then
begin
writeln('невозможно стереть файл ',polniypath);
flag:=false;(*ошибка удаления файла - нельзя будет стереть каталог*)
end;
end;
end;
el:=readdir(d);
end;
closedir(d);
d:=opendir(name);
el:=readdir(d);
while el<>nil do
begin
polniypath:=name;
if strcomp(name,'/')=0 then
strcat(polniypath,el^.name)
else
begin
if name[strlen(name)-1]<>'/' then
strcat(polniypath,'/');
strcat(polniypath,el^.name);
end;
if not fstat(pchar(polniypath),st) then
writeln('Ошибка вызова stat для ',polniypath)
else
begin
if (gettype(st.mode)='d') and
(strcomp(el^.name,'.')<>0) and
(strcomp(el^.name,'..')<>0) then
begin
writeln('Переход в каталог ',polniypath);
if not obhod(polniypath) then
flag:=false;
end;
end;
el:=readdir(d);
end;
closedir(d);
if not flag then
writeln('Каталог ',name,
' не будет стерт, т.к. в нем не удалось стереть часть файлов или каталогов')
else
begin
{$i-}
rmdir(name);
if ioresult <> 0 then
begin
writeln('Ошибка удаления каталога ',name);
flag:=false;
end;
end;
writeln('Для каталога ',name, ' получен ',flag);
obhod:=flag;
end;
var
name:array [0..2000] of char;
begin
if paramcount<>0 then
begin
name:=paramstr(1);
obhod(name);
end
else
writeln('С особой осторожностью используйте: ',paramstr(0),' удаляемый каталог');
end.
Каталоги и пути
Важное понятие, связанное с файлами, – каталог (directory). Каталоги представляют собой набор файлов, позволяя сформировать некоторую логическую структуру содержащейся в системе информации. Например, каждый пользователь обычно имеет начальный каталог, в котором он работает, а команды, системные библиотеки и программы администрирования обычно расположены в своих определенных каталогах. Кроме файлов, каталоги также могут содержать произвольное число подкаталогов, которые, в свою очередь, также могут содержать подкаталоги, и так далее. Фактически каталоги могут иметь любую глубину вложенности. Таким образом, файлы UNIX организованы в иерархической древовидной структуре, в которой каждый узел, кроме конечных, соответствует каталогу. Вершиной этого дерева является один каталог, которая обычно называется корневым каталогом (root directory).
/ | |||||||||||||||||
| | |||||||||||||||||
usr | |||||||||||||||||
keith | ben | ||||||||||||||||
| | | | ||||||||||||||||
book | file1 | file2 | book | ||||||||||||||
chap1 | chap2 | chap1 | chap2 |
Рис. 1.1. Пример дерева каталогов
Подробное рассмотрение структуры каталогов системы UNIX содержится в главе 4. Тем не менее, поскольку в книге постоянно будет идти речь о файлах UNIX, стоит отметить, что полные их имена, которые называются путями
(pathnames), отражают эту древовидную структуру. Каждый путь задает последовательность ведущих к файлу каталогов. Например, полное имя
/usr/keith/book/chap1
можно разбить на следующие части: первый символ / означает, что путь начинается с корневого каталога, то есть путь дает абсолютное положение файла (absolute pathname). Затем идет usr – подкаталог корневого каталога. Каталог keith находится еще на один уровень ниже и поэтому является подкаталогом /usr. Аналогично каталог book является подкаталогом /usr/keith. Последняя часть, chap1, может быть и каталогом, и обычным файлом, поскольку каталоги именуются точно так же, как и обычные файлы. На рис. 1.1 показан пример дерева каталогов, содержащих этот путь.
Путь, который не начинается с символа /, называется относительным путем (relative pathname) и задает маршрут к файлу от текущего рабочего каталога
(current working directory) пользователя. Например, полное имя
chap1/intro.txt
описывает файл intro.txt, который находится в подкаталоге chap1 текущего каталога. В самом простом случае имя
intro.txt
просто обозначает файл intro.txt в текущем каталоге. Снова заметим: для того чтобы программа была действительно переносимой, каждая из частей полного имени файла должна быть не длиннее 14 символов.
Каталоги с точки зрения пользователя
Даже случайный пользователь системы UNIX будет иметь некоторое представление о том, как выглядит структура каталогов системы. Тем не менее для полноты изложения кратко опишем обычное расположение файлов с точки зрения пользователя.
В сущности каталоги являются просто списками имен файлов, которые обеспечивают способ разбиения файлов на логически связанные группы. Например, каждый пользователь имеет домашний каталог (home directory), в который попадает при входе в систему и где может создавать файлы и работать с ними. Смысл этого, очевидно, состоит в том, чтобы разделить файлы отдельных пользователей. Аналогично программы, доступные всем пользователем, такие как cat или ls, помещаются в общеизвестные каталоги, обычно /bin или /usr/bin. Используя общепринятую метафору, каталоги можно сравнить с ящиками в шкафу, в которых хранятся папки файлов с документами.
Вместе с тем у каталогов есть некоторые преимущества по сравнению с ящиками в шкафу, так как кроме файлов они также могут включать другие каталоги, которые называются подкаталогами (subdirectories) и позволяют организовать следующие уровни классификации. Подкаталоги, в свою очередь, могут содержа другие подкаталоги и так далее. Допустим любой уровень вложенности, хотя может быть ограничение на длину абсолютного пути файла (см. пункт 4.6.4).
Фактически файловую структуру UNIX можно представить в виде иерархической структуры, напоминающей перевернутое дерево. Упрощенное дерево каталогов показано на рис. 4.1 (это тот же пример, что и рисунок в главе 1). Конечно же, любая реальная система будет иметь более сложную структуру.
/ | |||||||||||||||||
| | |||||||||||||||||
usr | |||||||||||||||||
keith | ben | ||||||||||||||||
| | | | ||||||||||||||||
book | file1 | file2 | book | ||||||||||||||
chap1 | chap2 | chap1 | chap2 |
Рис. 4.1. Пример дерева каталогов
На вершине этого дерева, так же как и на вершине любого дерева каталогов UNIX, находится единственный каталог, который называется корневым каталогом (root directory) и имеет очень короткое имя /. Все узлы дерева, кроме конечных, например узлы keith или ben, всегда являются каталогами. Конечные узлы, например узлы file1 или file2, являются файлами или пустыми каталогами. В настоящее время в большинстве систем UNIX имена каталогов могут содержать до 255 символов, но, так же как и в случае с файлами, для обеспечения совместимости со старыми версиями системы их длина не должна превышать 14 символов.
В нашем примере узлы keith и ben являются подкаталогами родительского каталога usr. В каталоге keith находятся три элемента: два обычных файла file1 и file2 и подкаталог book. Каталог keith является родительским для каталога book. В свою очередь каталог book содержит два файла chap1 и chap2. Как было показано в главе 1, положение файла в иерархии может быть задано заданием пути к нему. Например, полное имя файла chap2 в каталоге keith, включающее путь к нему, будет /usr/keith/book/chap2. Аналогично можно указать и полное имя каталога. Полное имя каталога ben будет /usr/ben.
Следует обратить внимание, что каталог /usr/ben/book также содержит два файла с именами chap1 и chap2. Они не обязательно связаны со своими тезками в каталоге /usr/keith/book, так как только полное имя файла однозначно идентифицирует его. Тот факт, что в разных каталогах могут находиться файлы с одинаковыми именами, означает, что пользователям нет необходимости изобретать странные и уникальные имена для файлов.