Программирование для Linux. Профессиональный подход

Митчелл Марк

Оулдем Джеффри

Самьюэл Алекс

Часть II

Секреты Linux

 

 

Глава 6

Устройства

 

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

В Linux драйверы устройств являются частью ядра и могут подключаться к ядру статически либо по запросу в виде модулей. Драйверы недоступны напрямую пользовательским процессам. Но в Linux имеется особый механизм — специальные файловые объекты, позволяющие процессам взаимодействовать с драйверами, а через них — с аппаратными устройствами. Такие объекты являются частью операционной системы, поэтому программы могут открывать их, читать из них данные и осуществлять запись в них точно так же, как если бы это быта обычные файлы. С помощью низкоуровневых вызовов (описаны в приложении Б, "Низкоуровневый ввод-вывод") или стандартных библиотечных функций ввода-вывода программы могут обмениваться данными с устройствами через файловые объекты

В Linux есть также ряд файловых объектов, предназначенных для доступа к ядру, а не к драйверам устройств. Такие объекты не связаны с аппаратными устройствами. Они реализуют специальные функции, используемые приложениями и системными программами.

Будьте осторожны при доступе к устройствам!

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

 

6.1. Типы устройств

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

■ Символьные (байт-ориентированные) устройства читают и записывают данные в виде потока байтов. Сюда входят последовательные и параллельные порты, накопители на магнитной ленте, терминалы и звуковые платы.

■ Блочные (блок-ориентированные) устройства читают и записывают данные блоками фиксированного размера. В отличие от символьных устройств блочные устройства предоставляют произвольный доступ к своим данным. В качестве примера можно назвать жесткий диск.

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

Опасность доступа к блочному устройству

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

Приложениям иногда приходится иметь дело с символьными устройствами: об этом пойдет речь в разделе 6.5, "Специальные устройства".

 

6.2. Номера устройств

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

Например, устройству со старшим номером 3 соответствует основной контроллер IDE. К этому контроллеру могут быть подключены два устройства (жесткие диски, накопитель на магнитной лейте или дисковод CD-ROM). "Главному" устройству будет соответствовать младший номер 0, а "подчиненному" устройству — номер 64. Отдельные разделы главного устройства (если он поддерживает разбивку на разделы) будут иметь младшие номера 1, 2, 3 и т.д. Разделы подчиненного устройства представляются младшими номерами 65, 66, 67 и т.д.

Список старших номеров устройств можно узнать в документации к исходным текстам ядра Linux. Во многих дистрибутивах эта информация хранится в файле /usr/src/Linux/Documentation/devices.txt. В специальном файле /proc/devices перечислены старшие номера устройств, соответствующие загруженным в данный момент драйверам (о файловой системе /proc рассказывается в главе 7, "Файловая система /proc").

 

6.3. Файловые ссылки на устройства

 

Ссылки на устройства напоминают обычные файлы. Их можно перемещать с помощью команды mv и удалять командой rm. Правда, если попытаться скопировать такую ссылку с помощью команды cp, из устройства будут прочитаны данные (при условии что устройство поддерживает операцию чтения) и эти данные перенесутся в указанный файл. При попытке перезаписи ссылки в соответствующее устройство будут записаны данные.

Ссылка на устройство создается с помощью команды mknod (документация вызывается так. man 1 mknod) или функции mknod() (документация вызывается так: man 2 mknod). Создание ссылки не означает, что драйвер устройства или само устройство автоматически станут доступными. Ссылка является лишь своего рода порталом, через который происходит взаимодействие с драйвером. Создавать такие ссылки разрешается только процессам суперпользователя.

Первый аргумент команды mknod задает путь, под которым ссылка появится в файловой системе. Второй аргумент равен b для блочного устройства и с для символьного устройства. Старший и младший номера устройства задаются в третьем и четвертом аргументах соответственно. Например, следующая команда создает в текущем каталоге ссылку на символьное устройство lp0. Старший номер устройства — 6, младший — 0. Эти номера соответствуют первому параллельному порту Linux.

% mknod ./lp0 с 6 0

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

Команда ls особым образом помечает ссылки на устройства. Если вызвать ее с флагом -l или -o, то первый символ в каждой строке будет обозначать тип записи. Знак - (дефис) соответствует обычному файлу, буква d — каталогу, b — блочному устройству, c — символьному устройству. В последних двух случаях команда ls вместо размера файла отображает старший и младший номера устройства. Давайте, к примеру, получим информацию о ссылке на символьное устройство, которую мы только что создали:

% ls -l lp0

crw-r----- 1 root root 6, 0 Mar 7 17:03 lp0

В распоряжении программ имеется функция stat(), которая позволяет не только узнать, какому устройству — символьному или блочному— соответствует ссылка, но и определить номера устройства. Эта функция описана в приложении Б, "Низкоуровневый ввод-вывод".

Удалить ссылку на устройство (не сам драйвер) можно с помощью команды rm:

% rm ./lp0

 

6.3.1. Каталог /dev

В Linux имеется каталог /dev, в котором содержатся ссылки на все символьные и блочные устройства, известные системе. Имена этих ссылок стандартизированы

Например, главное устройство, подключенное к основному контроллеру IDE, имеет старший и младший номера 3 и 0 соответственно, а его стандартное имя — /dev/hda. Если данное устройство поддерживает разделы, то первый раздел (младший номер 1) будет называться /dev/hda1. Проверим это:

% ls -l /dev/hda /dev/hda1

brw-rw---- 1 root disk 3, 0 May 5 1998 /dev/hda

brw-rw---- 1 root disk 3, 1 May 5 1998 /dev/hda1

Здесь же будет находиться и ссылка на параллельный порт, которую мы создали выше:

% ls -l /dev/lp0

crw-rw---- 1 root daemon 6, 0 May 5 1998 /dev/lp0

В большинстве случаев нет необходимости с помощью команды mknod создавать собственные ссылки. Достаточно скопировать нужные ссылки из каталога /dev. У программ, не располагающих привилегиями суперпользователя, нет другого выбора, кроме как пользоваться имеющимися ссылками. Обычно новые ссылки создаются только системными администраторами и разработчиками драйверов. В Linux имеются специальные средства, упрощающие администраторам процесс создания ссылок с правильными именами.

 

6.3.2. Доступ к устройству путем открытия файла

Как работать с аппаратными устройствами? В случае символьного устройства ответ прост: откройте ссылку на устройство как обычный файл и осуществляйте чтение-запись традиционным образом. Например, если к первому параллельному порту подключен принтер, то распечатать файл document.txt можно, направив его непосредственно на устройство /dev/lp0:

% cat document.txt > /dev/lp0

Чтобы эта команда завершилась успешно, необходимо иметь право записи в файл принтера. Во многих Linux-системах таким правом обладают лишь пользователь root и системный демон печати (lpd). Кроме того, результат работы принтера зависит от того, как он интерпретирует посылаемые ему данные. Одни принтеры распечатывают текстовые файлы, другие — нет. PostScript-принтеры распечатывают файлы формата PostScript.

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

int fd = open("/dev/lp0", O_WRONLY);

write(fd, buffer, bufffer_length);

close(fd);

 

6.4. Аппаратные устройства

В табл. 6.1 перечислены распространенные блочные устройства. "Родственные" устройства именуются схожим образом (например, второй раздел первого SCSI-диска называется /dev/sda2). Эта информация будет полезна при анализе файла /proc/mounts на предмет того, какие файловые системы смонтированы в настоящий момент (об этом рассказывается в разделе 7.5, "Дисководы, точки монтирования и файловые системы").

Таблица 6.1. Распространенные блочные устройства

Устройство Имя Старший номер Младший номер
Первый дисковод гибких дисков /dev/fd0 2 0
Второй дисковод гибких дисков /dev/fd1 2 1
Основной IDE-контроллер, главное устройство /dev/hda 3 0
Основной IDE-контроллер, главное устройство, первый раздел /dev/hda1 3 1
Основной IDE-контроллер, подчиненное устройство /dev/hdb 3 64
Основной IDE-контроллер, подчиненное устройство, первый раздел /dev/hdb1 3 65
Дополнительный IDE-контроллер, главное устройство /dev/hdc 22 0
Дополнительный IDE-контроллер, подчиненное устройство /dev/hdd 22 64
Первый SCSI-диск /dev/sda 8 0
Первый SCSI-диск, первый раздел /dev/sda1 8 1
Второй SCSI диск /dev/sdb 8 16
Второй SCSI-диск, первый раздел /dev/sdb1 8 17
Первый SCSI-дисковод CD-ROM /dev/scd0 11 0
Второй SCSI-дисковод CD-ROM /dev/scd1 11 1

В табл. 6.2 перечислены распространенные символьные устройства.

Таблица 6.2. Распространенные символьные устройства

Устройство Имя Старший номер Младший номер
Параллельный порт 0 /dev/lp0 или /dev/par0 6 0
Параллельный порт 1 /dev/lp1 или /dev/par1 6 1
Первый последовательный порт /dev/ttyS0 4 64
Второй последовательный порт /dev/ttyS1 4 65
IDE-накопитель на магнитной ленте /dev/ht0 37 0
Первый SCSI-накопитель на магнитной ленте /dev/st0 9 0
Второй SCSI-накопитель на магнитной ленте /dev/st1 9 1
Системная консоль /dev/console 5 1
Первый виртуальный терминал /dev/tty1 4 1
Второй виртуальный терминал /dev/tty2 4 2
Текущее терминальное устройство процесса /dev/tty 5 0
Звуковая плата /dev/audio 14 4

К некоторым аппаратным компонентам можно получить доступ сразу через несколько символьных устройств. Чаще всего этим устройствам соответствует разная семантика доступа. Например, если в системе есть ленточное IDE-устройство /dev/ht0, то Linux автоматически перематывает ленту в дисководе, когда программа закрывает дескриптор файла устройства. С помощью ссылки /dev/nht0 можно обратиться к тому же ленточному накопителю, но режим автоматической перемотки в нем будет отключен. Иногда в системе есть ссылки наподобие /dev/cua0. Это старые интерфейсы последовательных портов, таких как /dev/ttyS0.

Иногда требуется записывать данные непосредственно в символьные устройства. Рассмотрим примеры.

■ Терминальная программа напрямую обращается к модему через устройство последовательного порта. Данные, записываемые в устройство, передаются по модему на удаленный компьютер.

■ Программа резервного копирования записывает данные непосредственно на ленту. Такая программа может реализовывать свои собственные алгоритмы сжатия и проверки ошибок.

■ Программа обращается к первому виртуальному терминалу, записывая данные в устройство /dev/tty1.

Терминальным окнам, работающим в графической среде, и окнам сеансов удаленной регистрации назначаются не виртуальные терминалы, а псевдотерминалы (о них говорится в разделе 6.6, "Псевдотерминалы")

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

Например, программа может попросить пользователя ввести пароль. Из соображений безопасности требуется проигнорировать перенаправление стандартных потоков ввода и вывода и прочитать пароль с терминала независимо от того, как пользователь вызвал программу. Для этого можно открыть файл /dev/tty, всегда соответствующий текущему терминальному устройству процесса. Запишите в данный файл строку приглашения, а затем прочитайте пароль. Это не позволит пользователю передать программе пароль из файла с помощью следующего синтаксиса:

% secure_program < my-password.txt

■ Программа воспроизводит аудиофайл через звуковую плату, посылая аудиоданные в устройство /dev/audio. Эти данные должны быть представлены в формате Sun (такие файлы обычно имеют расширение .au).

Например, во многие дистрибутивы Linux входит файл /usr/share/sndconfig/sample.au. Попробуйте воспроизвести его с помощью такой команды:

% cat /usr/share/sndconfig/sample.au > /dev/audio

Те, кто хотят включить звук в свои программы, должны использовать специальные сервисы и библиотеки функций работы со звуком, имеющиеся в Linux. В графической среде Gnome есть демон EsounD (доступен по адресу http://www.tux.org/~riclude/EsounD.html), в KDE — программа aRts (http://space.twc.de/~stefan/kde/arts-mcop-doc/). Благодаря этим средствам приложения, обращающиеся к звуковой плате, лучше взаимодействуют друг с другом.

 

6.5. Специальные устройства

 

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

 

6.5.1. /dev/null

Устройство /dev/null служит двум целям.

■ Linux удаляет любые данные, направляемые в устройство /dev/null. В тех случаях, когда выводные данные программы не нужны, в качестве выходного файла назначают устройство /dev/null, например:

% verbose_command > /dev/null

■ При чтении из устройства /dev/null всегда возвращается признак конца строки. Если открыть файл /dev/null с помощью функции open() и попытаться прочесть данные из него с помощью функции read(), функция вернет 0 байтов. При копировании файла /dev/null в другое место будет создан пустой файл нулевой длины:

% cp /dev/null empty-file

% ls -l empty-file

-rw-rw---- 1 samuel samuel 0 Mar 8 00:27 empty-file

 

6.5.2. /dev/zero

Устройство /dev/zero ведет себя так, как если бы оно было файлом бесконечной длины, заполненным одними нулями. Сколько бы данных ни запрашивалось из этого файла, ОС Linux "сгенерирует" достаточное количество кулевых байтов.

Чтобы проверить это, запустите программу hexdump, представленную в листинге Б.4 приложения Б, "Низкоуровневый ввод-вывод". Программа отображает содержимое файла /dev/zero в шестнадцатеричном виде:

% ./hexdump /dev/zero

0x000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0x000010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0x000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0x000030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

...

Чтобы прервать работу программы, нажмите .

Файл /dev/zero используется в функциях выделения памяти, которые отображают этот файл в памяти, чтобы инициализировать выделяемые сегменты нулями. Об этом рассказывается в разделах 5,3.5, "Другие применения функции mmap()", и 8.9. "Функция mprotect(): задание прав доступа к памяти".

 

6.5.3. /dev/full

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

Вот что получится, если попытаться осуществить запись в устройство /dev/full с помощью команды cp:

% cp /etc/fstab /dev/full

cp: /dev/full: No space left on device

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

 

6.5.4. Устройства генерирования случайных чисел

Специальные устройства /dev/random и /dev/urandom предоставляют доступ к средствам генерирования случайных чисел, встроенным в ядро Linux.

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

Чтобы получить настоящие случайные числа, необходим внешний "источник хаоса". Ядро Linux знает о таком источнике: это вы сами! Замеряя задержки между действиями пользователя, в частности нажатиями клавиш и перемещениями мыши, ядро способно генерировать непредсказуемый поток действительно случайных чисел. Получить доступ к этому потопу можно путем чтения из устройств /dev/random и /dev/urandom.

Разница между устройствами проявляется, когда запас случайных чисел в ядре Linux заканчивается. Если попытаться прочесть большое количество байтов из устройства /dev/random и при этом не выполнять никаких пользовательских действий (не нажимать клавиши, не перемещать мышь и т.п.), система заблокирует операцию чтения. Только когда пользователь проявит какую-то активность, система сгенерирует дополнительные случайные числа и передаст их программе.

Попытайтесь, к примеру, отобразить содержимое файла /dev/random с помощью команды od. В каждой строке выходных данных содержится 16 случайных байтов.

% od -t x1 /dev/random

0000000 2с 9с 7а db 2е 79 3d 65 36 c2 e3 1b 52 75 1е 1а

0000020 d3 6d 1e a7 91 05 2d 4d c3 a6 de 54 29 f4 46 04

0000040 b3 b0 8d 94 21 57 f3 90 61 dd 26 ac 94 c3 b9 3a

0000060 05 a3 02 cb 22 0a be c9 45 dd a6 59 40 22 53 d4

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

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

Следующая команда будет выполняться до тех пор. пока пользователь не нажмет :

% od -t x1 /dev/urandom

0000000 62 71 d6 3e af dd de 62 c0 42 78 bd 29 9c 69 49

0000020 26 3b 95 be b9 6c 15 16 38 fd 7e 34 f0 ba ее c3

0000040 95 31 e5 2c 8d 8a dd f4 c4 3b 9b 44 2f 20 d1 54

...

Поучить доступ в программе к генератору случайных чисел несложно. В листинге 6.1 показана функция, которая генерирует случайное число, читая байты из файла /dev/random. Помните, что операция чтения из этого файла окажется заблокированной в случае нехватки случайных чисел. Если важна скорость работы функции и можно смириться с тем, что некоторые числа окажутся псевдослучайными, воспользуйтесь файлом /dev/urandom.

Листинг 6.1. ( random_number.c ) Генерирование случайного числа с помощью файла /dev/random

#include

#include

#include

#include

#include

/* Функция возвращает случайное число в диапазоне от MIN до МАХ

   включительно. Случайная последовательность байтов читается из

   файла /dev/random. */

int random_number(int min, int max) {

 /* Дескриптор файла /dev/random сохраняется в статической

    переменной, чтобы не приходилось повторно открывать файл

    при каждом следующем вызове функции. */

 static int dev_random_fd = -1;

 char* next_random_byte;

 int bytes_to_read;

 unsigned random_value;

 /* Убеждаемся, что аргумент MAX больше, чем MIN. */

 assert(max > min);

 /* Если функция вызывается впервые, открываем файл /dev/random

    и сохраняем его дескриптор. */

 if (dev_random_fd == -1) {

  dev_random_fd = open("/dev/random", O_RDONLY);

  assert(dev_random_fd != -1);

 }

 /* Читаем столько байтов, сколько необходимо для заполнения

    целочисленной переменной. */

 next_random_byte = (char*)&random_value;

 bytes_to_read = sizeof(random_value);

 /* Цикл выполняется до тех пор, пока не будет прочитано

    требуемое количество байтов. Поскольку файл /dev/random

    заполняется в результате пользовательских действий,

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

    может быть заблокирована или возвращать

    лишь один байт за раз. */

 do {

  int bytes_read;

  bytes_read =

   read(dev_random_fd, next_random_byte, bytes_to_read);

  bytes_to_read -= bytes_read;

  next_random_byte += bytes_read;

 } while (bytes_to_read > 0);

 /* Вычисляем случайное число в правильном диапазоне. */

 return min + (random_value % (max - min + 1));

}

 

6.5.5. Устройства обратной связи

Устройство обратной связи позволяет сымитировать блочное устройство с помощью обычного дискового файла. Представьте жесткий диск, в котором данные находятся не в дорожках и секторах, а в файле с именем disk-image (естественно, сам этот файл должен размещаться на реальном диске, размер которого больше имитируемого).

Устройства обратной связи называются /dev/loop0, /dev/loop1 и т.д. Каждому из них соответствует одно виртуальное блочное устройство. Создавать такие устройства может только суперпользователь.

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

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

1. Создайте пустой файл, который будет содержать образ ВФС. Размер файла должен соответствовать видимому размеру виртуальной файловой системы после ее монтирования.

Проще всего создать файл фиксированного размера с помощью команды dd. Эта команда копирует блоки (по умолчанию каждый из них имеет размер 512 байтов) из одного файла в другой. Лучший источник байтов для копирования — устройство /dev/zero.

Файл disk-image размером 10 Мбайт создается следующим образом:

% dd if=/dev/zero of=/trap/disk-image count=20480

20480+0 records in

20480+0 records out

% ls -l /tmp/disk-image

-rw-rw---- 1 root root 10485760 Mar 8 01:56 /trap/disk-image

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

Файловая система может иметь любой тип. Команда mke2fs создает файловую систему типа ext2 (чаще всего используется в жестких дисках Linux-систем). Поскольку команда обычно работает с блочными устройствами, она потребует подтверждение:

% mke2fs -q /tmp/disk-image

mke2fs 1.18, 11-Nov-1999 for EXT2 FS 0.5b, 95/08/09

disk-image is not a block special device.

Proceed anyway? (y,n) y

Опция -q подавляет вывод статистики файловой системы.

Теперь файл disk-image содержит новую файловую систему, как если бы это был жесткий диск емкостью 10 Мбайт.

3. Смонтируйте файловую систему с использованием устройства обратной связи. Для этого введите команду mount, указав файл образа диска в качестве устройства монтирования. Необходимо также задать опцию -о loop= устройство_обратной_связи . Ниже показаны команды, которые это делают. Помните, что только суперпользователь может работать с устройством обратной связи. Первая команда создает каталог /tmp/virtual-fs, который станет точкой монтирования ВФС.

% mkdir /tmp/virtual-fs

% mount -о loop=/dev/loop0 /tmp/disk-image /tmp/virtual-fs

Теперь образ диска смонтирован подобно обычному жесткому диску емкостью 10 Мбайт.

% df -h /tmp/virtual-fs

Filesystem Size Used Avail Use% Mounted on

/tmp/disk-image 9.7M 13k 9.2M 0% /tmp/virtual-fs

Для работы с новой файловой системой применяются обычные команды:

% cd /tmp/virtual-fs

% echo 'Hello, world!' > test.txt

% ls -l total 13

drwxr-xr-x 2 root root 12288 Mar 8 02:00 lost+found

-rw-rw---- 1 root root 14 Mar 8 02:12 test.txt

% cat test.txt

Hello, world!

Каталог lost+found автоматически добавляется командой mke2fs.

По завершении работы с виртуальной файловой системой ее следует демонтировать:

% cd /tmp

% umount /tmp/virtual-fs

При желании файл disk-image можно удалить или смонтировать позднее, чтобы получить доступ к файлам ВФС. Можно даже скопировать файл на другой компьютер и смонтировать его там — вся файловая система будет воссоздана в неизменном виде.

Файловую систему можно не создавать с нуля, а скопировать непосредственно с устройства, например с компакт-диска. Если в системе есть IDE-дисковод CD-ROM, ему будет соответствовать имя устройства наподобие /dev/hda. Имя устройства для SCSI-дисковода будет примерно таким: /dev/scd0. В системе может также существовать символическая ссылка /dev/cdrom. Чтобы узнать, какое конкретно устройство закреплено за дисководом CDROM, просмотрите файл /etc/fstab.

Достаточно скопировать содержимое устройства в файл. В результате будет создан полный образ файловой системы компакт-диска, вставленного в дисковод. Например:

% cp /dev/cdrom /tmp/cdrom-image

Такая команда может выполняться несколько минут, в зависимости от емкости компакт-диска и скорости дисковода.

Теперь можно монтировать образ компакт-диска даже при отсутствии самого накопителя в дисководе. Например, следующая команда назначает точкой монтирования каталог /mnt/cdrom:

% mount -о loop=/dev/loop0 /tmp/cdrom-image /mnt/cdrom

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

 

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

 

Если запустить команду mount без аргументов, будет выдан список всех смонтированных файловых систем. Одна из строк выглядит примерно так:

none on /dev/pts type devpts (rw,gid=5,mode=620)

Она указывает на то, что файловая система специального типа devpts смонтирована в каталоге /dev/pts. Эта файловая система не связана ни с каким аппаратным устройством, создается ядром Linux и напоминает файловую систему /proc (о ней пойдет речь в главе 7, ''Файловая система /proc").

Подобно каталогу /dev каталог /dev/pts содержит ссылки на устройства, но создается ядром динамически. Его "наполнение" меняется, отражая состояние работающей системы. Все записи этого каталога соответствуют псевдотерминалам. ОС Linux создает псевдотерминал для каждого открываемого терминального окна и помещает ссылку на него в каталог /dev/pts. Псевдотерминалы ведут себя аналогично терминальным устройствам: они принимают данные с клавиатуры и отображают текст, передаваемый им программами. Номер псевдотерминала является именем его записи в каталоге /dev/pts.

 

6.6.1. Пример работы с псевдотерминалом

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

% ps -o pid,tty,cmd

  PID TTY   CMD

28832 pts/4 bash

29287 pts/4 ps -o pid,tty,cmd

В данном случае терминальному окну соответствует псевдотерминал 4.

У каждого псевдотерминала есть запись в каталоге /dev/pts:

% ls -l /dev/pts/4

crw--w---- 1 samuel tty 136, 4 Mar 8 02:56 /dev/pts/4

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

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

Попробуйте открыть новое терминальное окно и определить номер псевдотерминала, выполнив команду ps -o pid,tty,cmd. Теперь откройте другое окно и направьте какие-то данные на псевдотерминал. Например, если его номер 7, введите такую команду:

% echo "Hello, other window!" > /dev/pts/7

Заданная строка отобразится в первом окне. Когда терминальное окно будет закрыто, запись 7 исчезнет из каталога /dev/pts.

Если ввести команду ps в терминальном окне, работающем в текстовом режиме, окажется, что ему соответствует обычное терминальное устройство, а не псевдотерминал:

% ps -о pid,tty,cmd

  PID TTY  CMD

29325 tty1 -bash

29353 tty1 ps -o pid,tty,cmd

 

6.7. Функция ioctl()

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

Многие коды запросов перечислены на man-странице ioctl_list. При работе с функцией ioctl() нужно хорошо понимать, как работает драйвер соответствующего устройства. В принципе, эти вопросы выходят за рамки нашей книги, но все же приведем небольшой пример.

Листинг 6.2. ( cdrom-eject.c ) Извлечение компакт-диска из дисковода

#include

#include

#include

#include

#include

#include

int main(int argc, char* argv[]) {

 /* Открытие файла устройства, указанного в командной строке. */

 int fd = open(argv[1], O_RDONLY);

 /* Извлечение компакт-диска из дисковода. */

 ioctl(fd, CDROMEJECT);

 /* Закрытие файла. */

 close(fd);

 return 0;

}

В листинге 6.2 представлена короткая программа, которая запрашивает извлечение компакт-диска из дисковода CD-ROM. Программа принимает единственный аргумент командной строки: имя дисковода CD-ROM. Программа открывает файл устройства и вызывает функцию ioctl() с кодом запроса CDROMEJECT. Этот код определен в файле и служит устройству указанием извлечь компакт-диск из дисковода.

Например, если в системе имеется IDE-дисковод CD-ROM, подключенный в качестве главного устройства к дополнительному IDE-контроллеру, соответствующий файл устройства будет называться /dev/hdc. Тогда компакт-диск извлекается из дисковода с помощью такой команды:

% ./cdrom-eject /dev/hdc

 

Глава 7

Файловая система /proc

 

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

none on /proc type proc (rw)

Она указывает на специальную файловую систему /proc. Поле none говорит о том, что эта система не связана с аппаратным устройством, например жестким диском. Она является своего рода "окном" в ядро Linux. Файлам в системе /proc не соответствуют реальные файлы на физическом устройстве. Это особые объекты, которые ведут себя подобно файлам, открывал доступ к параметрам, служебным структурам и статистической информации ядра. "Содержимое" таких файлов генерируется ядром динамически в процессе чтения из файла. Осуществляя запись в некоторые файлы, можно менять конфигурацию работающего ядра системы. Рассмотрим пример:

% ls -l /proc/version

-r--r--r-- 1 root root 0 Jan 17 18:09 /proc/version

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

Что находится в файле /proc/version? Он содержит строку, описывающую номер версии ядра Linux. Сюда входит информация, возвращаемая системным вызовом uname() (описан в разделе 8.15, "Функция uname()"), а также номер версии компилятора, с помощью которого было создано ядро. Чтение из файла /proc/version осуществляется самым обычным образом, например с помощью команды cat:

% cat /proc/version

Linux version 2.2.14-5.0 ([email protected])

(gcc version egcs-2.91.66 19990314/Linux

(egcs-1.1.2 release)) #1 Tue Mar 7 21:07:39 EST 2000

Многие элементы файловой системы /proc описаны на man-странице proc (раздел 5). В этой главе будут рассмотрены те из них, которые чаще всего используются программистами и полезны при отладке.

Читатели, которых интересуют детали функционирования файловой системы /proc, могут просмотреть ее исходные коды в каталоге /usr/src/linux/fs/proc/.

 

7.1. Извлечение информации из файловой системы /proc

Большинство элементов файловой системы /proc выдает информацию в отформатированном виде. Например, файл /proc/cpuinfo содержит сведения о процессоре (или процессорах, если это многопроцессорный компьютер). Выходная информация представляется в виде таблицы значений, по одному на строку. Каждое значение сопровождается символическим идентификатором.

При обращении к файлу /proc/cpuinfo будет выдана примерно следующая информация:

% cat /proc/cpuinfo

processor     : 0

vendor_id     : GenuineIntel

cpu family    : 6

model         : 5

model name    : Pentium II (Deschutes)

stepping      : 2

cpu MHz       : 400.913520

cache size    : 512 KB

fdiv_bug      : no

hlt_bug       : no

sep_bug       : no

f00f_bug      : no

coma_bug      : no

fpu           : yes

fpu_exception : yes

cpuid level   : 2

wp            : yes

flags         : fpu vme de pse tsc msr рае mce cx8 apic sep

mtrr pge mce cmov pat pse36 mmx fxsr

bogomips      : 399.77

Интерпретация некоторых значений даны в разделе 7.3.1. "Центральный процессор". Если нужно получить одно из этих значений в программе, проще всего загрузить файл в память и просканировать его функцией sscanf(). В листинге 7.1 показано, как это сделать. В программе имеется функция get_cpu_clock_speed(), которая загружает файл /proc/cpuinfo и определят частоту процессора.

Листинг 7.1. ( clock-speed.c ) Определение частоты процессора путем анализа файла /proc/cpuinfo

#include

#include

/* Определение частоты процессора в мегагерцах на

   основании данных файла /proc/cpuinfo. В

   многопроцессорной системе будет найдена частота

   первого процессора. В случае ошибки возвращается нуль. */

float get_cpu_clock_speed() {

 FILE* fр;

 char buffer[1024];

 size_t bytes_read;

 char* match;

 float clock_speed;

 /* Загрузка всего файла /proc/cpuinfo в буфер. */

 fp = fopen("/proc/cpuinfo", "r");

 bytes_read = fread(buffer, 1, sizeof(buffer), fp);

 fclose(fp);

 /* Выход, если прочитать файл не удалось или буфер оказался

    слишком маленьким. */

 if (bytes_read == 0 || bytes_read = sizeof(buffer))

  return 0;

 /* Буфер завершается нулевым символом. */

 buffer[bytes_read] = '\0';

 /* Поиск строки, содержащей метку "cpu MHz". */

 match = strstr(buffer, "cpu MHz");

 if (match == NULL)

  return 0;

 /* Анализ строки и выделение из нее значения частоты

    процессора. */

 sscanf(match, "cpu MHz ; %f" &clock_speed);

 return clock_speed;

}

int main() {

 printf("CPU clock speed: %4.0f Mhz\n",

  get_cpu_clock_speed());

 return 0;

}

He забывайте о том. что имена, семантика и формат представления элементов файловой системы /proc меняются при обновлении ядра Linux. Программа должна вести себя корректно в случае, если нужный файл отсутствует или имеет иной формат.

 

7.2. Каталоги процессов

 

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

В каталогах процессов находятся следующие файлы.

■ cmdline. Содержит список аргументов процесса; описан в разделе 7.2.2, "Список аргументов процесса".

■ cwd. Является символической ссылкой на текущий рабочий каталог процесса (задаётся, к примеру, функцией chdir()).

■ environ. Содержит переменные среды процесса; описан в разделе 7.2.3, "Переменные среды процесса".

■ exe. Является символической ссылкой на исполняемый файл процесса; описан в разделе 7.2.4. "Исполняемый файл процесса".

■ fd. Является подкаталогом, в котором содержатся ссылки на файлы, открытые процессом: описан в разделе 7.2.5, "Дескрипторы файлов процесса".

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

■ root. Является символической ссылкой на корневой каталог процесса (обычно это /). Корневой каталог можно сменить с помощью команды chroot или функции chroot().

■ stat. Содержит статистическую информацию о процессе. Эти же данные представлены в файле status, но здесь они находятся в неотформатированном виде и записаны в одну строку. Такой формат труден для восприятия, зато проще в плане синтаксического анализа.

■ statm. Содержит информацию об использовании памяти процессом, описан в разделе 7.2.6. "Статистика использования процессом памяти".

■ status. Содержит статистическую информацию о процессе, причем в отформатированном виде; описан в разделе 7 2.7, "Статистика процесса".

■ cpu. Этот файл появляется только в симметричных многопроцессорных системах и содержит информацию об использовании процессорного времени (пользователями и системой).

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

 

7.2.1. Файл /proc/self

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

Например, программа, представленная в листинге 7.2, с помощью файла /proc/self определяет свой идентификатор процесса (это делается лишь в демонстрационных целях, гораздо проще пользоваться функцией getpid(), описанной в разделе 3.1.1, "Идентификаторы процессов"). Для чтения содержимого символической ссылки вызывается функция readlink() (описана в разделе 8.11, "Функция readlink(): чтение символических ссылок").

Листинг 7.2. ( get-pid.c ) Получение идентификатора процесса из файла /proc/self

#include

#include

#include

/* Определение идентификатора вызывающего процесса

   на основании символической ссылки /proc/self. */

pid_t get_pid_from_proc_self() {

 char target[32];

 int pid;

 /* Чтение содержимого символической ссылки. */

 readlink("/proc/self", target, sizeof(target));

 /* Адресатом ссылки является каталог, имя которого соответствует

    идентификатору процесса. */

 sscanf(target, "%d", &pid);

 return (pid_t)pid;

}

int main() {

 printf("/proc/self reports process id %d\n",

  (int)get_pid_from_proc_self());

 printf("getpid() reports process id %d\n", (int)getpid());

 return 0;

}

 

7.2.2. Список аргументов процесса

Файл cmdline в файловой системе /proc содержит список аргументов процесса (см. раздел 2.1.1. "Список аргументов"). Этот список представлен одной строкой, в которой аргументы отделяются друг от друга нулевыми символами. Большинство функций работы со строками предполагает, что нулевым символом оканчивается вся строка, поэтому они не смогут правильно обработать файл cmdline.

В листинге 2.1 приводилась программа, которая отображала переданный ей список аргументов. Теперь, когда мы узнали назначение файлов cmdline файловой системы /proc, можно написать программу, отображающую список аргументов другого процесса. Ее текст показан в листинге 7.3. Поскольку в строке файла cmdline может содержаться несколько нулевых символов, ее длину нельзя определить с помощью функции strlen() (она лишь подсчитывает число символов, пока не встретится нулевой символ). Приходится полагаться на функцию read(), которая возвращает число прочитанных байтов.

Листинг 7.3. ( print-arg-list.c ) Отображение списка аргументов указанного процесса

#include

#include

#include

#include

#include

#include

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

   с заданным идентификатором. */

void print_process_arg_list(pid_t pid) {

 int fd;

 char filename[24];

 char arg_list[1024];

 size_t length;

 char* next_arg;

 /* Определение полного имени файла cmdline

    для заданного процесса. */

 snprintf(filename, sizeof(filename), "/proc/%d/cmdline",

  (int)pid);

 /* Чтение содержимого файла. */

 fd = open(filename, O_RDONLY);

 length = read(fd, arg_list, sizeof(arg_list));

 close(fd);

 /* Функция read() не помещает в конец текста нулевой символ,

    поэтому его приходится добавлять отдельно. */

 arg_list[length] = '\0';

 /* Перебор аргументов. Аргументы отделяются друг от друга

    нулевыми символами. */

 next_arg = arg_list;

 while (next_arg < arg_list + length) {

  /* Вывод аргументов. Каждый из них оканчивается нулевым

     символом и потому интерпретируется как обычная строка. */

  printf("%s\n", next_arg);

  /* Переход к следующем аргументу. Поскольку каждый аргумент

     заканчивается нулевым символом, функция strlen() вычисляет

     длину отдельного аргумента, а не всего списка. */

  next_arg += strlen(next_arg) + 1;

 }

}

int main(int argc, char* argv[]) {

 pid_t pid = (pid_t)atoi(argv[1]);

 print_process_arg_list(pid);

 return 0;

}

Предположим, к примеру, что номер процесса системного демона syslogd равен 372.

% ps 372

 PID TTY STAT TIME COMMAND

 372 ?   S    0:00 syslogd -m 0

% ./print-arg-list 372

syslogd

-m

0

В данном случае программа print-arg-list, сообщает о том, что демон syslogd вызван с аргументами -m 0.

 

7.2.3. Переменные среды процесса

Файл environ содержит список переменных среды, в которой работает процесс (см. раздел 2.1.6, "Среда выполнения"). Как и в случае файла cmdline, элементы списка разделяются нулевыми символами. Формат элемента таков: ПЕРЕМЕННАЯ = значение .

Представленная в листинге 7.4 программа является обобщением программы, которая была показана в листинге 2.3. В данном случае программа принимает в командной строке идентификатор процесса и отображает список его переменных среды, извлекаемый из файловой системы /proc.

Листинг 7.4. ( print-environment.c ) Отображение переменных среды процесса

#include

#include

#include

#include

#include

#include

/* Вывод переменных среды (по одной в строке) процесса

   с заданным идентификатором. */

void print_process_environment(pid_t pid) {

 int fd;

 char filename[24];

 char environment[8192];

 size_t length;

 char* next_var;

 /* Определение полного имени файла environ

    для заданного процесса. */

 snprintf(filename, sizeof(filename), "/proc/%d/environ",

  (int)pid);

 /* Чтение содержимого файла. */

 fd = open(filename, O_RDONLY);

 length = read(fd, environment, sizeof (environment));

 close(fd);

 /* Функция read() не помещает в конец текста нулевой символ,

    поэтому его приходится добавлять отдельно. */

 environment[length] = ' \0';

 /* Перебор переменных. Элементы списка отделяются друг от друга

    нулевыми символами. */

 next_var = environment;

 while (next_var < environment + length) {

  /* Вывод элементов списка. Каждый из них оканчивается нулевым

     символом и потому интерпретируется как обычная строка. */

  printf("%s\n", next_var);

  /* Переход к следующей переменной. Поскольку каждый элемент

     списка заканчивается нулевым символом, функция strlen()

     вычисляет длину отдельного элемента, а не всего списка. */

  next_var += strlen(next_var) + 1;

 }

}

int main(int argc, char* argv[]) {

 pid_t pid = (pid_t)atoi(argv[1]);

 print_process_environment(pid);

 return 0;

}

 

7.2.4. Исполняемый файл процесса

Файл exe указывает на исполняемый файл процесса. В разделе 2.1.1, "Список аргументов", говорилось о том, что имя исполняемого файла обычно передается в качестве первого элемента списка аргументов. Но это лишь распространенное соглашение. Программу можно запустить с произвольным списком аргументов. Файл exe файловой системы /proc — это более надежный способ узнать, какой исполняемый файл запущен процессом.

Во многих программах путь ко вспомогательным файлам задан относительно исполняемого файла, поэтому важно знать, где именно он находится. Функция get_executable_path() в листинге 7.5 определяет путевое имя текущего исполняемого файла, проверяя символическую ссылку /proc/self/exe.

Листинг 7.5. ( get-exe-path.c ) Определение путевого имени текущего исполняемого файла

#include

#include

#include

#include

/* Нахождение путевого имени текущего исполняемого файла.

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

   равна LEN. Возвращается число символов в имени либо

   -1 в случае ошибки. */

size_t get_executable_path(char* buffer, size_t len) {

 char* path_end;

 /* чтение содержимого символической ссылки /proc/self/exe. */

 if (readlink("/proc/self/exe", buffer, len) <= 0)

  return -1;

 /* Нахождение последней косой черты, отделяющей путевое имя. */

 path_end = strrchr(buffer, '/');

 if (path_end == NULL)

  return -1;

 /* Переход к символу, стоящему за последней косой чертой. */

 ++path_end;

 /* Усечение полной строки до путевого имени. */

 *path_end = '\0';

 /* Длина путевого имени — это число символов до последней

    косой черты. */

 return (size_t)(path_end - buffer);

}

int main() {

 char path[PATH_MAX];

 get_executable_path(path, sizeof (path));

 printf("this program is in the directory %e\n", path);

 return 0;

}

 

7.2.5. Дескрипторы файлов процесса

Элемент fd файловой системы /proc — это подкаталог, в котором содержатся записи обо всех файлах, открытых процессом. Каждая запись представляет собой символическую ссылку на файл или устройство. Через эти ссылки можно осуществлять чтение и запись данных. Имена ссылок соответствуют номерам дескрипторов.

Рассмотрим небольшой трюк. Откройте новое терминальное окно и найдите с помощью команды ps идентификатор процесса, соответствующий интерпретатору команд:

% ps

 PID TTY       TIME CMD

1261 pts/4 00:00:00 bash

2455 pts/4 00:00:00 ps

В данном случае процесс идентификатора команд (bash) имеет идентификатор 1261. Теперь откройте второе окно и просмотрите содержимое подкаталога fd этого процесса:

% ls -l /proc/1261/fd total 0

lrwx------ 1 samuel samuel 64 Jan 30 01:02 0 -> /dev/pts/4

lrwx------ 1 samuel samuel 64 Jan 30 01:02 1 -> /dev/pts/4

lrwx------ 1 samuel samuel 64 Jan 30 01:02 2 -> /dev/pts/4

(В выводе могут присутствовать дополнительные строки, соответствующие другим открытым файлам.) Вспомните в разделе 2.1.4, "Стандартный ввод-вывод", рассказывалось о том. что дескрипторы 0, 1 и 2 закрепляются за стандартными потоками ввода, вывода и ошибок соответственно. Таким образом, при записи в файл /proc/1261/fd/1 данные будут направляться в устройство, связанное с потоком stdout интерпретатора команд, т.е. на псевдотерминал первого окна. Попробуйте ввести следующую команду

% echo "Hello, world." >> /proc/1261/fd/1

Сообщение "Hello, world." появится в первом окне.

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

Листинг 7.6. ( open-and-spin.c ) Открытие файла для чтения

#include

#include

#include

#include

#include

int main(int argc, char* argv[]) {

 const char* const filename = argv[1];

 int fd = open(filename, O_RDONLY);

 printf("in process %d, file descriptor %d is open to %s\n",

  (int)getpid(), (int)fd, filename);

 while (1);

 return 0;

}

Запустите программу в терминальном окне:

% ./open-and-spin /etc/fstab

in process 2570, file descriptor 3 is open to /etc/fstab

Теперь откройте другое окно и проверьте подкаталог fd процесса с указанным номером:

% ls -l /proc/2570/fd

total 0

lrwx------ 1 samuel samuel 64 Jan 30 01:30 0 -> /dev/pts/2

lrwx------ 1 samuel samuel 64 Jan 30 01:30 1 -> /dev/pts/2

lrwx------ 1 samuel samuel 64 Jan 30 01:30 2 -> /dev/pts/2

lr-x------ 1 samuel samuel 64 Jan 30 01:30 3 -> /etc/fstab

Как видите, появилась, ссылка 3, которая соответствует дескриптору файла /etc/fstab, открытого программой.

Программа может открывать дескрипторы не только файлов, но также сокетов и каналов. В таких случаях адресатом символической ссылки будет строка "socket" или "pipe", а не имя файла либо устройства.

 

7.2.6. Статистика использования процессом памяти

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

■ общий размер процесса;

■ размер резидентной части процесса;

■ память, совместно используемая с другими процессами (например, загруженные библиотеки или нетронутые страницы, созданные в режиме "копирование при записи");

■ текстовый размер процесса, т.е. размер сегмента кода исполняемого файла;

■ размер совместно используемых библиотек, загруженных процессом;

■ память, выделенная под стек процесса;

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

 

7.2.7. Статистика процесса

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

 

7.3. Аппаратная информация

 

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

 

7.3.1. Центральный процессор

Как уже говорилось, файл /proc/cpuinfo содержит информацию о центральном процессоре (или процессорах, если их больше одного). В поле "processor" перечислены номера процессоров. В случае однопроцессорной системы там будет стоять 0. Благодаря полям "vendor_id", "cpu family", "model" и "stepping" можно точно узнать модель и модификацию процессора. В поле "flags" показано, какие флат процессора установлены. Это самая важная информация. Она определяет, какие функции процессора доступны. Например, флаг "mmx" говорит о том, что поддерживаются расширенные инструкции MMX.

Большая часть информации, содержащейся в файле /proc/cpuinfo, извлекается с помощью ассемблерной инструкции cpuid процессоров семейства x86. С помощью этой низкоуровневой инструкции программы могут получать сведения о центральном процессоре. Подробнее узнать об этой инструкции можно в руководстве IA-32 Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference, доступном по адресу http://developer.intel.com/design.

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

 

7.3.2. Аппаратные устройства

В файле /proc/devices содержится список старших номеров символьных и блочных устройств, имеющихся в системе. Подробнее об этом рассказывалось в главе 6. "Устройства".

 

7.3.3. Шина PCI

В файле /proc/pci перечислены устройства, подключенные к шине (или шинам) PCI. Сюда входят реальные PCI-платы, а также устройства, встроенные в материнскую плату, плюс графические платы AGP. В каждой строке указан тип устройства, идентификатор устройства и его поставщика, имя устройства (если есть), информация о функциональных возможностях устройства и сведения о ресурсах PCI-шины, используемых устройством

 

7.3.4. Последовательные порты

Файл /proc/tty/driver/serial содержит конфигурационную и статистическую информацию о последовательных портах. Эти порты нумеруются начиная с нуля. Работать с настройками порта позволяет также команда setserial, но файл /proc/tty/driver/serial, помимо всего прочего, включает дополнительные статистические данные о счетчиках прерываний каждого порта.

Например, следующая строка описывает последовательный порт 1 (COM2 в Windows):

1: uart:16550А port:2F8 irq:3 baud:9600 tx:11 rx:0

Здесь говорится о том, что последовательный порт оснащен микросхемой UART 16550А, использует порт ввода-вывода 0x218 и прерывание 3 и работает со скоростью 9600 бод. Через этот порт было передано 11 запросов на прерывание и получено 0 таких запросов.

 

7.4. Информация о ядре

 

В файловой системе /proc есть много элементов, содержащих информацию о настройках и состоянии ядра. Некоторые из них находятся на верхнем уровне файловой системы, а некоторые скрыты в каталоге /proс/sys/kernel.

 

7.4.1. Версия ядра

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

% cat /proc/version

Linux version 2.2.14-5.0 ([email protected])

(gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release))

#1 Tue Mar 7 21:07:39 EST 2000

Здесь сказано, что в системе используется ядро Linux версии 2.2.14, которое было скомпилировано программой EGCS версии 1.1.2 (эта программа является предшественницей широко распространенного в настоящее время пакета GCC).

Для наиболее важных параметров, а именно названия операционной системы и номера версии/модификации ядра, созданы отдельные записи в файловой системе /proc. Это файлы /proc/sys/kernel/ostype, /proc/sys/kernel/osrelease и /proc/sys/kernel/version.

% cat /proc/sys/kernel/ostype Linux

% cat /proc/sys/kernel/osrelease 2.2.14-5.0

% cat /proc/sys/kernel/version #1 Tue Mar 7 21:07:39 EST 2000

 

7.4.2. Имя компьютера и домена

В файлах /proc/sys/kernel/hostname и /proc/sys/kernel/domainname содержатся имя компьютера и имя домена соответственно. Эту же информацию возвращает функция uname(), описанная в разделе 8.15, "Функция uname()".

 

7.4.3. Использование памяти

Файл /proc/meminfo хранит сведения об использовании системной памяти. Указываются данные как о физической памяти, так и об области подкачки. Во второй и третьей строках значения даны в байтах, в остальных строках — в килобайтах. Приведем пример:

% cat /proc/meminfo

        total:    used:     free:  shared:  buffers: cached:

Mem:  529694720 519610368 10084352 82612224 10977280 82108416

Swap: 271392766 44003328  227389440

MemTotal:  517280 kB

MemFree:     9848 kB

MemShared:  80676 kB

Buffers:    10720 kB

Cached:     80184 kB

BigTotal:       0 kB

BigFree:        0 kB

SwapTotal: 265032 kB

SwapFree:  222060 kB

Как видите, в системе имеется 512 Мбайт ОЗУ, из которых 9 Мбайт свободно. Для области подкачки выделено 258 Мбайт, из которых свободно 216 Мбайт. В строке, соответствующей физической памяти, показаны три других значения.

■ В колонке "shared" отображается общий объем совместно используемой памяти, выделенной в системе.

■ В колонке "buffers" отображается объем памяти, выделенной для буферов блочных устройств. Эти буферы используются драйверами устройств для временного хранения считываемых и записываемых блоков данных.

■ В колонке "cached" отображается объем памяти, выделенной для страничного кэш-буфера. В этом буфере сохраняются страницы файлов, отображаемых в памяти.

Ту же самую информацию можно получить с помощью команды free.

 

7.5. Дисководы, точки монтирования и файловые системы

 

В файловой системе /proc находится также информация о присутствующих в системе дисковых устройствах и смонтированных на них файловых системах.

 

7.5.1. Файловые системы

Файл /proc/filesystems хранит информацию об известных ядру типах файловых систем. Этот список не очень полезен, так как он не полный: файловые системы могут подключаться и отключаться динамически в виде модулей ядра. В файле /proc/filesystems перечислены типы файловых систем, которые либо статически подключены к ядру, либо присутствуют в настоящий момент.

 

7.5.2. Диски и разделы

В файловой системе /proc находятся данные об устройствах, подключенных как к IDE-так и к SCSI-контроллерам (если таковые имеются). Обычно в каталоге /proc/ide есть один или два подкаталога (ide0 и ide1) для основного и дополнительного IDE-контроллеров системы. В этих подкаталогах будут другие подкаталоги, которые соответствуют физическим устройствам, подключенным к контроллерам. В случае, если устройство не распознано системой, подкаталог не создается. В табл. 7.1 указаны путевые имена каталогов для четырех возможных IDE-устройств.

Таблица 7.1. Каталоги, соответствующие четырем возможным IDE-устройствам

Контроллер Устройство Подкаталог
Основной Главное /рroc/ide/ide0/hda/
Основной Подчиненное /proc/ide/ide0/hdb/
Дополнительный Главное /proc/ide/ide1/hdc/
Дополнительный Подчиненное /proc/ide/ide1/hdd/

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

■ model. Содержит строку идентификации устройства.

■ media. Описывает тип носителя. Возможные значения: disk, cdrom, tape, floppy и UNKNOWN.

■ capacity. Определяет емкость устройства (в 512-байтовых блоках). Для дисководов CD-ROM значением будет 2³¹-1, а не емкость компакт-диска, вставленного в дисковод. Находящееся в данном файле значение представляет емкость всего физического диска. Емкость файловых систем, содержащихся в разделах диска, будет меньше.

Ниже показано, как определить тип носителя и идентификатор главного устройства, подключенного к дополнительному IDE-контроллеру:

% cat /proc/ide/ide1/hdc/media

cdrom

% cat /proc/ide/ide1/hdc/model

TOSHIBA CD-ROM XM-6702B

В данном случае это дисковод CDROM компании Toshiba.

Если в системе есть SCSI-устройства, в файле /proc/scsi/scsi будет находиться сводка их идентификаторов. Содержимое этого файла выглядит примерно так

% cat /proc/scsi/scsi

Attached devices:

Host: scsi0 Channel: 00 Id: 00 Lun: 00

 Vendor: QUANTUM Model: ATLAS_V__9_WLS Rev: 0230

 Type:   Direct-Access ANSI SCSI revision: 03

Host: scsi0 Channel: 00 Id: 04 Lun: 00

 Vendor: QUANTUM Model: QM39100TD-SW Rev: N491

 Type:   Direct-Access ANSI SCSI revision: 02

В системе присутствует один одноканальный SCSI-контроллер (обозначен как scsi0), к которому подключены два дисковых накопителя Quantum со SCSI-номерами 0 и 4.

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

Файл /proc/sys/dev/cdrom/info хранит различные данные о возможностях дисководов CD ROM. Записи этого файла не требуют особых пояснений:

% cat /proc/sys/dev/cdrom/info

CD-ROM information, Id: cdrom.с 2.56 1999/09/09

drive name: hdc

drive speed: 48

drive # of slots: 0

Can close tray: 1

Can open tray: 1

Can lock tray: 1

Can change speed: 1

Can select disk: 0

Can read multisession: 1

Can read MCN: 1

Reports media changed: 1

Can play audio: 1

 

7.5.3. Точки монтирования

В файле /proc/mounts находится перечень смонтированных файловых систем. Каждая строка соответствует одному дескриптору монтирования и содержит имя устройства, имя точки монтирования и прочие сведения. Та же самая информация хранится в обычном файле /etc/mtab, который автоматически обновляется командой mount.

Ниже перечислены элементы дескриптора монтирования.

■ Первый элемент строки — это имя смонтированного устройства. Для специальных файловых систем, например /proc, здесь стоит значение none.

■ Второй элемент — это имя точки монтирования, т.е. места в корневой файловой системе, где появится содержимое монтируемой файловой системы. Для самой корневой системы точка монтирования обозначается символом /. Разделам подкачки соответствует точка монтирования swap.

■ Третий элемент — это тип файловой системы. В настоящее время на жестких дисках Linux в основном устанавливаются файловые системы типа ext2, но диски DOS и Windows могут монтироваться с файловыми системами других типов, например fat или vfat. Тип файловых систем большинства компакт-дисков — iso9660. Список типов файловых систем приведен на man-странице команды mount.

■ Четвертый элемент — это флаги монтирования. Они указываются при добавлении точки монтирования. Пояснение этих флагов также дано на man-странице команды mount.

В файле /proc/mounts последние два элемента всегда равны нулю и никак не интерпретируются.

Подробнее о формате дескрипторов монтирования можно узнать на man-странице fstab. В Linux есть функции, позволяющие анализировать содержимое дескрипторов монтирования. За дополнительной информацией обратитесь к man-странице функции getmntent().

 

7.5.4. Блокировки

В файле /proc/locks перечислены все блокировки файлов, установленные в настоящий момент в системе. Каждая строка соответствует одной блокировке.

Для блокировок, созданных функцией fcntl() (описана в разделе 8.3. "Функция fcntl(): блокировки и другие операции над файлами"), первыми двумя элементами строки будут слова POSIX и ADVISORY. Третьим элементом будет WRITE или READ, в зависимости от типа блокировки. Следующее число — это идентификатор процесса, установившего блокировку. За ним идут три числа, разделенные двоеточиями. Это старший и младший номера устройства, на котором расположен файл, а также номер индексного дескриптора, оказывающий на местоположение файла в файловой системе. Оставшиеся числа используются внутри ядра и не представляют интереса.

Чтобы понять, как работает файл /proc/locks, запустите программу, приведенную в листинге 8.2. и поставьте блокировку записи на файл /tmp/test-file.

% touch /trap/test-file

% ./lock-file /tmp/test-file

file /tmp/test-file

opening /tmp/test-file

locking

locked; hit enter to unlock...

В другом окне просмотрите содержимое файла /proc/ locks:

% cat /proc/locks

ls POSIX ADVISORY WRITE 5467 08:05:181288 0 2147483647 d1b5f740

00000000 dfea7d40 00000000 00000000

В файле могут присутствовать дополнительные строки, если какие-то программы устанавливали свои блокировки. В данном случае идентификатор процесса программы lock-file — 5467. Убедимся в этом с помощью команды ps:

% ps 5467

 PID TTY    STAT TIME COMMAND

5467 pts/28 S    0:00 ./lock-file /tmp/test-file

Заблокированный файл /tmp/test-file находится на устройстве со старшим и младшим номерами 8 и 5 соответственно. Это номера устройства /dev/sda5:

% df /trap

Filesystem 1k-blocks    Used Available Use% Mounted on

/dev/sda5    8459764 5094292   2935736  63% /

% ls -l /dev/sda5

brw-rw---- 1 root disk 8, 5 May 5 1998 /dev/sda5

На этом устройстве с файлом /tmp/test-file связав индексный дескриптор 181288:

% ls --inode /trap/test-file

181288 /tmp/test-file

 

7.6. Системная статистика

Два элемента файловой системы /proc содержат полезную статистическую информацию. В файле /proc/loadavg находятся данные о загруженности системы. Первые три показателя — это число активных задач (выполняющихся процессов) за последние 1, 5 и 15 минут. Следующая строка отображает число выполняемых задач (процессов, запланированных к выполнению, а не заблокированных в каком-нибудь системном вызове) в данный момент времени и общее число процессов в системе. Последняя строка содержит идентификатор самого недавнего процесса.

В файле /proc/uptime отражено, сколько времени прошло с момента загрузки системы и сколько времени с тех пор система пребывала в неактивном состоянии. Оба показателя выражены в секундах и представлены числами с плавающей запятой:

% cat /proc/uptime

3248936.18 3072330.49

Программа, показанная в листинге 7 7. определяет общее время работы и время простоя системы и отображает эти значения в понятном формате.

Листинг 7.7. ( print-uptime.c ) Отображение времени работы и времени простоя системы

#include

/* Запись результата в стандартный выходной поток.

   Параметр TIME это количество времени, а параметр LABEL --

   короткая описательная строка. */

void print_time(char* label, long time) {

 /* Константы преобразования. */

 const long minute = 60;

 const long hour = minute * 60;

 const long day = hour * 24; /* Вывод результата. */

 printf("%s: %ld days, %ld:%02ld:%02ld\n", label, time / day,

  (time % day) / hour, (time % hour) / minute, time % minute);

}

int main() {

 FILE* fp;

 double uptime, idle_time;

 /* Чтение показателей времени из файла /proc/uptime. */

 fp = fopen("/proc/uptime", "r");

 fscanf(fp, "%lf %lf\n", &uptime, &idle_time);

 fclose(fp);

 /* Форматирование и вывод. */

 print_time("uptime ", (long)uptime);

 print_time("idle time", (long)idle_time);

 return 0;

}

Общее время работы системы отображают также команда uptime и функция sysinfo() (описана в разделе 8.14, "Функция sysinfo(): получение системной статистики"). Команда uptime дополнительно выдает показатели средней загруженности, извлекаемые из файла /proc/loadavg.

 

Глава 8

Системные вызовы Linux

 

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

■ Библиотечная функция — это обычная функция, которая находится во внешней библиотеке, подключаемой к программе. Большинство рассмотренных нами функций содержится в стандартной библиотеке языка С, libc. Вызов библиотечной функции реализуется традиционно: ее аргументы помещаются в регистры процессора или в стек и управление передается в начало кода функции (этот код находится в библиотеке, загруженной в память).

■ Системный вызов реализован в ядре Linux. Аргументы вызова упаковываются и передаются ядру, которое берет на себя управление программой, пока вызов не завершится. Системный вызов — это не обычная функция, и для передачи управления ядру требуется специальная подпрограмма. В GNU-библнотеке языка С (реализация стандартной библиотеки, имеющаяся в Linux) для системных вызовов созданы функции-оболочки, упрощающие обращение к ним. В качестве примеров системных вызовов можно привести низкоуровневые функции ввода-вывода, такие как open() и read().

Совокупность системных вызовов Linux формирует основной интерфейс между программами и ядром. Каждому вызову соответствует некая элементарная операция или функция.

Некоторые системные вызовы оказывают очень большое влияние на систему. Например. есть вызовы, позволяющие завершить работу Linux, выделить системные ресурсы или запретить другим пользователям доступ к ресурсам. С такими вызовами связано ограничение: только процессы, выполняющиеся с привилегиями суперпользователя (учетная запись root), имеют право обращаться к ним. В противном случае вызовы завершатся ошибкой.

Внутри себя библиотечная функция может обращаться к другим функциям или системным вызовам.

В настоящее время в Linux есть около 200 системных вызовов. Их список находится в файле /usr/include/asm/unistd.h. Некоторые из них используются только внутри системы, а некоторые предназначены лишь для реализации специализированных библиотечных функций. В этой главе будут рассмотрены те системные вызовы, которые чаще всего используются системными программистами.

 

8.1. Команда strace

Прежде чем изучать системные вызовы, полезно познакомиться с командой strace, которая отслеживает выполнение заданной программы, выводя список всех запрашиваемых системных вызовов и получаемых сигналов. Эта команда ставится в начале строки вызова программы, например:

% strace hostname

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

В случае команды strace hostname первая строка сообщает о системном вызове execve(), загружающем программу hostname:

execve("/bin/hostname", ["hostname"], [/* 49 vars */]) = 0

Первый аргумент — это имя запускаемой программы. За ним идет список аргументов, состоящий из одного элемента. Дальше указан список переменных среды, который команда strace опустила для краткости.

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

uname({sys="Linux", node="myhostname", ...}) = 0

Заметьте, что команда strace показала метки полей структуры, в которой хранятся аргументы. Эта структура заполняется в системном вызове: Linux помещает в поле sys имя операционной системы, а в поле node — имя компьютера. Функция uname() будет описана ниже, в разделе 8.15. "Функция uname()".

Системный вызов write() выводит полученные результаты на экран. Вспомните, что дескриптор 1 соответствует стандартному выходному потоку. Третий аргумент — это количество отображаемых символов. Функция возвращает число действительно записанных символов.

write(1, "myhostname\n", 11) = 11

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

 

8.2. Функция access(): проверка прав доступа к файлу

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

Функция access() принимает два аргумента: путь к проверяемому файлу и битовое объединение флагов R_OK, W_OK и X_OK, соответствующих правам чтения, записи и выполнения. При наличии у процесса всех необходимых привилегий функция возвращает 0. Если файл существует, а нужные привилегии на доступ к нему у процесса отсутствуют, возвращается -1 и в переменную errno записывается код ошибки EACCES (или EROFS, если проверяется право записи в файл, который расположен в файловой системе, смонтированной только для чтения).

Если второй аргумент равен F_OK, функция access() проверяет лишь факт существования файла. В случае обнаружения файла возвращается 0, иначе — -1 (в переменную errno помещается также код ошибки ENOENT). Когда один из каталогов на пути к файлу недоступен, в переменную errno будет помещён код EACCES.

Программа, показанная в листинге 8.1, с помощью функции access() проверяет существование файла и определяет, разрешен ли к нему доступ на чтение/запись. Имя файла задается в командной строке.

Листинг 8.1. ( check-access.c ) Проверка прав доступа к файлу

#include

#include

#include

int main(int argc, char* argv[]) {

 char* path = argv[1];

 int rval;

 /* Проверка существования файла. */

 rval = access(path, F_OK);

 if (rval == 0)

  printf("%s exists\n", path);

 else {

  if (errno == ENOENT)

   printf("%s does not exist\n", path);

  else if (errno == EACCES)

   printf("%s is not accessible\n", path);

  return 0;

 }

 /* Проверка права доступа. */

 rval = access(path, R_OK);

 if (rval == 0)

  printf("%s is readable\n", path);

 else

  printf("%s is not readable (access denied)\n", path);

 /* проверка права записи. */

 rval = access(path, W_OK);

 if (rval == 0)

  printf("%s is writable\n", path);

 else if (errno == EACCES)

  printf("%s is not writable (access denied)\n", path);

 else if (errno == EROFS)

  printf("%s is not writable (read-only filesystem)\n",

   path);

 return 0;

}

Вот как, к примеру, проверить права доступа к файлу README, расположенному на компакт-диске:

% ./check-access /mnt/cdrom/README

/mnt/cdrom/README exists

/mnt/cdrom/README is readable

/mnt/cdrom/README is not writable (read-only filesystem)

 

8.3. Функция fcntl(): блокировки и другие операции над файлами

Функция fcntl() — это точка доступа к нескольким особым операциям над файлами. Первым аргументом функции является дескриптор файла, вторым указывается код операции. Для некоторых операций требуется также дополнительный, третий аргумент. В этом разделе описана наиболее распространенная операция, выполняемая с помощью функции fcntl(): блокирование файлов.

Функция fcntl() позволяет программе поставить на файл блокировку чтения иди записи. Это напоминает применение исключающих семафоров, которые описывались в главе 5, "Взаимодействие процессов". Блокировка чтения ставится на файл, доступный для чтения. Соответственно блокировка записи ставится на файл, доступный для записи. Несколько процессов могут удерживать блокировку чтения одного и того же файла, но только одному процессу разрешено ставить блокировку записи. Файл не может быть одновременно заблокирован и для чтения, и для записи. Учтите, что наличие блокировки не мешает другим процессам открывать файл и осуществлять чтение/запись его данных, если только они сами не попытаются вызвать функцию fcntl().

Прежде чем ставить блокировку на файл, необходимо создать и обнулить структуру типа flock. В поле l_type должна быть записана константа F_RDLCK в случае блокировки чтения и константа F_WRLCK — в случае блокировки записи. Далее следует вызвать функцию fcntl(), передав ей дескриптор файла, код операции F_SETLCKW и указатель на структуру типа flock. Если аналогичная блокировка уже была поставлена другим процессом, функция fcntl() перейдет в режим ожидания, пока "мешающая" ей блокировка не будет снята.

В листинге 8.2 показана программа, которая открывает для записи указанный файл, а затем ставит на него блокировку записи. Программа ждет нажатия клавиши , после чего снимает блокировку и закрывает файл.

Листинг 8.2. ( lock-file.c ) Установка блокировки записи с помощью функции fcntl()

#include

#include

#include

#include

int main(int argc, char* argv[]) {

 char* file = argv[1];

 int fd;

 struct flock lock;

 printf("opening %s\n", file);

 /* Открытие файла. */

 fd = open(file, O_WRONLY);

 printf("locking\n");

 /* инициализация структуры flock. */

 memset(&lock, 0, sizeof(lock));

 lock.l_type = F_WRLCK;

 /* Установка блокировки записи. */

 fcntl(fd, F_SETLKW, &lock);

 printf("locked; hit Enter to unlock... ");

 /* Ожидание нажатия клавиши . */

 getchar();

 printf("unlocking\n");

 /* Снятие блокировки. */

 lock.l_type = F_UNLCK;

 fcntl(fd, F_SETLKW, &lock);

 close(fd);

 return 0;

}

Скомпилируйте программу и запустите ее с каким-нибудь тестовым файлом, скажем, /tmp/test-file:

% cc -o lock-file lock-file.с

% touch /tmp/test-file

% ./lock-file /tmp/test-file

opening /tmp/test-file

locking

locked; hit Enter to unlock...

Теперь откройте другое окно и вызовите программу еще раз с тем же файлом:

% ./lock-file /tmp/test-file

opening /tmp/test-file

locking

Пытаясь поставить блокировку на файл, программа сама окажется заблокированной. Вернитесь в первое окно и нажмите :

unlocking

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

В Linux имеется системный вызов flock(), также реализующий операцию блокирования файла. Но у функции fcntl() есть большое преимущество: она работает с файловыми системами NFS (при условии, что сервер NFS имеет относительно недавнюю версию и сконфигурирован правильно). Так что. имея доступ к двум компьютерам, которые монтируют одну и ту же файловую систему через NFS, можно повторить показанный выше пример на двух разных машинах.

 

8.4. Функции fsync() и fdatasync(): очистка дисковых буферов

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

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

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

Для реализации такого поведения ОС Linux предоставляет системный вызов fsync(). Эта функция принимает один аргумент — дескриптор записываемого файла — и принудительно переносит на диск все данные этого файла, находящиеся в кэш-буфере. Функция не завершается до тех пор, пока данные не окажутся на диске.

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

Листинг 8.3. ( write_journal_entry.c ) Запись строки в журнальный файл с последующей синхронизацией

#include

#include

#include

#include

#include

const char* journal_filename = "journal.log";

void write_journal_entry(char* entry) {

 int fd =

  open(journal_filename,

   O_WRONLY | O_CREAT | O_APPEND, 0660);

 write(fd, entry, strlen(entry));

 write(fd, "\n", 1);

 fsync(fd);

 close(fd);

}

Аналогичное действие выполняет другой системный вызов: fdatasync(). Но если функция fsync() гарантирует, что дата модификации файла будет обновлена, то функция fdatasync() этого не делает, а лишь гарантирует запись данных. В принципе это означает, что функция fdatasync() способна выполняться быстрее, чем fsync(), так как ей требуется выполнить одну операцию записи на диск, а не две. Но в настоящее время в Linux обе функции работают одинаково, обновляя дату модификации.

Файл можно также открыть в режиме синхронного ввода-вывода, при котором все операции записи будут немедленно фиксироваться на диске. Для этого в функции open() следует указать флаг O_SYNC.

 

8.5. Функции getrlimit() и setrlimit(): лимиты ресурсов

Функции getrlimit() и setrlimit() позволяют процессу определять и задавать лимиты использования системных ресурсов. Аналогичные действия выполняет команда ulimit, которая ограничивает доступ запускаемых пользователем программ к ресурсам.

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

Обе функции принимают два аргумента: код, задающий тип ограничения, и указатель на структуру типа rlimit. Функция getrlimit() заполняет поля этой структуры, тогда как функция setrlimit() проверяет их и соответствующим образом меняет лимит. У структуры rlimit два поля: в поле rlim_cur содержится значение нежесткого лимита, а в поле rlim_max — значение жесткого лимита.

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

■ RLIMIT_CPU. Это максимальный интервал времени центрального процессора (в секундах), занимаемый программой. Именно столько времени отводится программе на доступ к процессору. В случае превышения данного ограничения программа будет завершена по сигналу SIGXCPU.

■ RLIMIT_DATA. Это максимальный объем памяти, который программа может запросить для своих данных. Запросы на дополнительную память будут отвергнуты системой.

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

■ RLIMIT_NOFILE. Это максимальное число файлов, которые могут быть одновременно открыты процессом.

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

Листинг 8.4. ( limit-cpu.c ) Задание ограничения на использование нейтрального процессора

#include

#include

#include

int main() {

 struct rlimit rl;

 /* Определяем текущие лимиты. */

 getrlimit(RLIMIT_CPU, &rl);

 /* Ограничиваем время доступа к процессору

    одной секундой. */

 rl.rlim_cur = 1;

 setrlimit(RLIMIT_CPU, &rl);

 /* Переходим в бесконечный цикл. */

 while(1);

 return 0;

}

Когда программа завершается по сигналу SIGXCPU, интерпретатор команд выдает поясняющее сообщение:

% ./limit_cpu

CPU time limit exceeded

 

8.6. Функция getrusage(): статистика процессов

Функция getrusage() запрашивает у ядра статистику работы процессов. Если первый аргумент функции равен RUSAGE_SELF, процесс получит информацию о самом себе. Если же первым аргументом является константа RUSAGE_CHILDREN, будет выдана информация обо всех его завершившихся дочерних процессах. Второй аргумент — это указатель на структуру типа rusage, в которую заносятся статистические данные.

Перечислим наиболее интересные поля этой структуры.

■ ru_utime. Здесь находится структура типа timeval, в которой указано, сколько пользовательского времени (в секундах) ушло на выполнение процесса. Это время, затраченное центральным процессором на выполнение программного кода, а не системных вызовов.

■ ru_stime. Здесь находится структура типа timeval, в которой указано, сколько системного времени (в секундах) ушло на выполнение процесса. Это время, затраченное центральным процессором на выполнение системных вызовов от имени данного процесса.

■ ru_maxrss. Это максимальный объем физической памяти, которую процесс занимал в какой-то момент своего выполнения.

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

Листинг 8.5. ( prinf-cpu-times.c ) Определение пользовательского и системного времени, затраченного на выполнение текущего процесса

#include

#include

#include

#include

void print_cpu_time() {

 struct rusage usage;

 getrusage(RUSAGE_SELF, &usage);

 printf("CPU time: %ld.%061d sec user, %ld.%061d sec system\n",

  usage.ru_utime.tv_sec, usage.ru_utime.tv_usec,

  usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);

}

 

8.7, Функция gettimeofday(): системные часы

Функция gettimeofday() определяет текущее системное время. В качестве аргумента она принимает структуру типа timeval, в которую записывается значение времени (в секундах), прошедшее с начала эпохи UNIX (1-е января 1970 г., полночь по Гринвичу). Это значение разделяется на два поля. В поле tv_sec хранится целое число секунд, а в поле tv_usec — дополнительное число микросекунд. У функции есть также второй аргумент, который должен быть равен NULL. Функция объявлена в файле .

Результат, возвращаемый функцией gettimeofday(), мало подходит для отображения на экране, поэтому существуют библиотечные функции localtime() и strftime(), преобразующие это значение в нужный формат. Функция localtime() принимает указатель на число секунд (поле tv_sec структуры timeval) и возвращает указатель на структуру типа tm. Эта структура содержит поля, заполняемые параметрами времени в соответствии с локальным часовым поясом:

■ tm_hour, tm_min, tm_sec — текущее время (часы, минуты, секунды);

■ tm_year, tm_mon, tm_day — год, месяц, день;

■ tm_wday — день недели (значение 0 соответствует воскресенью);

■ tm_yday — день года;

■ tm_isdst — флаг, указывающий, учтено ли летнее время.

Функция strftime() на основании структуры tm создает строку, отформатированную по заданному правилу. Формат напоминает тот, что используется в функции printf(): указывается строка с кодами, определяющими включаемые поля структуры. Например, форматная строка вида

"%Y-%m-%d %Н:%М:%S"

соответствует такому результату:

2001-01-14 13:09:42

Функции strftime() необходимо задать указатель на текстовый буфер, куда будет помещена полученная строка, длину буфера, строку формата и указатель на структуру типа tm. Следует учесть, что ни функция localtime(), ни функция strftime() не учитывают дробную часть текущего времени (поле tv_usec структуры timeval). Об этом должен позаботиться программист.

Объявления функций localtime() и strftime() находятся в файле .

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

Листинг 8.6. ( print-time.c ) Отображение даты и времени

#include

#include

#include

#include

void print_time() {

 struct timeval tv;

 struct tm* ptm;

 char time_string[40];

 long milliseconds;

 /* Определение текущего времени и преобразование полученного

    значения в структуру типа tm. */

 gettimeofday(&tv, NULL);

 ptm = localtime(&tv.tv_sec);

 /* Форматирование значения даты и времени с точностью

    до секунды. */

 strftime(time_string, sizeof(time_string),

  "%Y-%m-%d %H:%M:%S", ptm);

 /* Вычисление количества миллисекунд. */

 milliseconds = tv.tv_usec / 1000;

 /* Отображение даты и времени с указанием

    числа миллисекунд. */

 printf("%s.%03ld\n", time_string, milliseconds);

}

 

8.8. Семейство функций mlock(): блокирование физической памяти

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

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

Чтобы заблокировать область памяти, достаточно вызвать функцию mlock(), передав ей указатель на начало области и значение длины области. ОС Linux разбивает память на страницы и соответственно блокирует ее постранично: любая страница, которую захватывает (хотя бы частично) заданная в функции mlock() область памяти, окажется заблокированной. Определить размер системной страницы позволяет функция getpagesize(). В Linux-системах, работающих на платформе x86, эта величина составляет 4 Кбайт.

Вот как можно выделить и заблокировать 32 Мбайт оперативной памяти:

const int alloc_size = 32 * 1024 * 1024;

char* memory = malloc(alloc_size);

mlock(memory, alloc_size);

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

size_t i;

size_t page_size = getpagesize();

for (i = 0; i < alloc_size; i += page_size)

 memory[i] = 0;

Процессу, осуществляющему запись на страницу, операционная система предоставит в монопольное использование ее уникальную копию.

Для разблокирования области памяти следует вызвать функцию munlock(), передав ей те же аргументы, что и функции mlock().

Функция mlockall() блокирует все адресное пространство программы и принимает единственный флаговый аргумент. Флаг MCL_CURRENT означает блокирование всей выделенной на данный момент памяти, но не той, что будет выделяться позднее. Флаг MCL_FUTURE задает блокирование всех страниц, выделенных после вызова функции mlockall(). Сочетание флагов MCL_CURRENT | MCL_FUTURE позволяет блокировать всю память программы, как текущую, так и будущую.

Блокирование больших объемов памяти, особенно с помощью функции mlockall(), несет потенциальную угрозу всей системе. Несправедливое распределение оперативной памяти приведет к катастрофическому снижению производительности системы, так как остальным процессам придется сражаться друг с другом за небольшой "клочок" памяти, вследствие чего они будут постоянно выгружаться на диск и загружаться обратно. Может даже возникнуть ситуация, когда оперативная память закончится и система начнет уничтожать процессы. По этой причине функции mlock() и mlockall() доступны лишь суперпользователю. Если какой-нибудь другой пользователь попытается вызвать одну из этих функций, она вернёт значение -1, а в переменную errno будет записан код EPERM.

Функция munlосkall() разблокирует всю память текущего процесса.

Контролировать использование памяти удобнее всего с помощью команды top. В колонке SIZE ее выходных данных показывается размер виртуального адресного пространства каждой программы (общий размер сегментов кода, данных и стека с учетом выгруженных страниц). В колонке RSS приводится объем резидентной части программы. Сумма значений в столбце RSS не может превышать имеющийся объем ОЗУ, а суммарный показатель по столбцу SIZE не может быть больше 2 Гбайт (в 32-разрядных версиях Linux).

Функции семейства mlock() объявлены в файле .

 

8.9. Функция mprotect(): задание прав доступа к памяти

В разделе 5.3, "Отображение файлов в памяти", рассказывалось о том, как осуществляется отображение файла в памяти. Вспомните, что третьим аргументом функции mmap() является битовое объединение флагов доступа: флаги PROT_READ, PROT_WRITE и PROT_EXEC задают права чтения, записи и выполнения файла, а флаг PROT_NONE означает запрет доступа. Если программа пытается выполнить над отображаемым файлом недопустимую операцию, ей посылается сигнал SIGSEGV (нарушение сегментации), который приводит к завершению программы.

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

Корректное выделение памяти

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

Кроме того, с помощью функции mmap() можно обойти функцию malloc() и запрашивать память непосредственно у ядра Linux.

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

int fd = open("/dev/zero", O_RDONLY);

char* memory =

 mmap(NULL, page_size, PROT_READ | PROT_WRITE,

  MAP_PRIVATE, fd, 0);

close(fd);

Далее программа запрещает запись в эту область памяти, вызывая функцию mprotect():

mprotect(memory, page_size, PROT_READ);

Существует оригинальная методика контроля памяти: можно защитить область памяти с помощью функций mmap() и mprotect(), а затем обрабатывать сигнал SIGSEGV, посылаемый при попытке доступа к этой памяти. Эта методика иллюстрируется в листинге 8.7.

Листинг 8.7. ( mprotect.c ) Обнаружение попыток доступа к памяти благодаря функции mprotect()

#include

#include

#include

#include

#include

#include

#include

#include

static int alloc_size;

static char* memory;

void segv_handler(int signal_number) {

 printf("memory accessed!\n");

 mprotect(memory, alloc_size, PROT_READ | PROT_WRITE);

}

int main() {

 int fd;

 struct sigaction sa;

 /* Назначение функции segv_handler() обработчиком сигнала

    SIGSEGV. */

 memset(&sa, 0, sizeof(sa));

 sa.sa_handler = &segv_handler;

 sigaction(SIGSEGV, &sa, NULL);

 /* Выделение одной страницы путем отображения в памяти файла

    /dev/zero. Сначала память доступна только для записи. */

 alloc_size = getpagesize();

 fd = open("/dev/zero", O_RDONLY);

 memory =

  mmap(NULL, alloc_size, PROT_WRITE, MAP_PRIVATE, fd, 0);

 close(fd);

 /* Запись на страницу для получения ее копии в частное

    использование. */

 memory[0] = 0;

 /* Запрет на запись в память. */

 mprotect(memory, alloc_size, PROT_NONE);

 /* Попытка записи в память. */

 memory[0] = 1;

 /* Удаление памяти. */

 printf("all done\n");

 munmap(memory, alloc_size);

 return 0;

}

Программа работает по следующей схеме.

1. Задается обработчик сигнала SIGSEGV.

2. Файл /dev/zero отображается в памяти, из которой выделяется одна страница. В эту страницу записывается инициализирующее значение, благодаря чему программе предоставляется частная копия страницы.

3. Программа защищает память, вызывая функцию mprotect() с флагом PROT_NONE.

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

5. Программа удаляет область память с помощью функции munmap().

 

8.10. Функция nanosleep(): высокоточная пауза

Функция nanosleep() является более точной версией стандартной функции sleep(), принимая указатель на структуру типа timespec, где время задается с точностью до наносекунды, а не секунды. Правда, особенности работы ОС Linux таковы, что реальная точность оказывается равной 10 мс, но это все равно выше, чем в функции sleep(). Функцию nanosleep() можно использовать в приложениях, где требуется запускать различные операции с короткими интервалами между ними.

В структуре timespec имеются два поля:

■ tv_sес — целое число секунд;

■ tv_nsec — дополнительное число миллисекунд (должно быть меньше, чем 109).

Работа функции nanosleep(), как и функции sleep(), прерывается при получении сигнала. При этом функция возвращает значение -1, а в переменную errno записывается код EINTR. Но у функции nanosleep() есть важное преимущество. Она принимает дополнительный аргумент — еще один указатель на структуру timespec, в которую (если указатель не равен NULL) заносится величина оставшегося интервала времени (т.е. разница между запрашиваемым и прошедшим промежутками времени). Благодаря этому можно легко возобновлять прерванные операции ожидания.

В листинге 8.8 показана альтернативная реализация функции sleep(). В отличие от стандартного системного вызова эта функция может принимать дробное число секунд и возобновлять операцию ожидания в случае прерывания по сигналу.

Листинг 8.8. ( better_sleep.c ) Высокоточная реализация функции sleep()

#include

#include

int better_sleep(double sleep_time) {

 struct timespec tv;

 /* Заполнение структуры timespec на основании указанного числа

    секунд. */

 tv.tv_sec = (time_t)sleep_time;

 /* добавление неучтенных выше наносекунд. */

 tv.tv_nsec = (long)((sleep_time - tv.tv_sec) * 1e+9);

 while (1) {

  /* Пауза, длительность которой указана в переменной tv.

     В случае прерывания по сигналу величина оставшегося

     промежутка времени заносится обратно в переменную tv. */

  int rval = nanosleep(&tv, &tv);

  if (rval == 0)

   /* пауза успешно окончена. */

   return 0;

  else if (errno == EINTR)

   /* Прерывание по сигналу. Повторная попытка. */

   continue;

  else

   /* Какая-то другая ошибка. */

   return rval;

 }

 return 0;

}

 

8.11. Функция readlink(): чтение символических ссылок

Функция readlink() определяет адресата символической ссылки. Она принимает три аргумента: путь к символической ссылке, буфер для записи адресата и длина буфера. Как ни странно, путевое имя, помещаемое в буфер, не завершается нулевым символом. Но поскольку в третьем аргументе возвращается длина буфера, добавить этот символ несложно.

Если первый аргумент не является символической ссылкой, функция readlink() возвращает -1, а в переменную errno записывается константа EINVAL.

Программа, представленная в листинге 8.9, показывает адресата символической ссылки, заданной в командной строке.

Листинг 8.9. ( print-symlink.с ) Отображение адресата символической ссылки

#include «errno.h>

#include

#include

int main(int argc, char* argv[]) {

 char target_path[256];

 char* link_path = argv[1];

 /* Попытка чтения адресата символической ссылки. */

 int len =

  readlink(link_path, target_path, sizeof(target_path));

 if (len == -1) {

  /* Функция завершилась ошибкой. */

  if (errno == EINVAL)

   /* Это не символическая ссылка. */

   fprintf(stderr, "%s is not a symbolic link\n", link_path);

  else

   /* Произошла какая-то другая ошибка. */

   perror("readlink");

  return 1;

 } else {

  /* Завершаем путевое имя нулевым символом. */

  target_path[len] = '\0';

  /* Выводим результат. */

  printf("%s\n", target_path);

  return 0;

 }

}

Ниже показано, как создать символическую ссылку и проверить ее с помощью программы print-symlink:

% ln -s /usr/bin/wc my_link

% ./print-symlink my_link

/usr/bin/wc

 

8.12. Функция sendfile(): быстрая передача данных

Функция sendfile() — это эффективный механизм копирования данных из одного файлового дескриптора в другой. Дескрипторам могут соответствовать дисковые файлы, сокеты или устройства.

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

Функция sendfile() устраняет потребность в создании промежуточного буфера. Ей передаются дескриптор для записи, дескриптор для чтения, указатель на переменную смещения и число копируемых данных. Переменная смещения определяет позицию входного файла, с которой начинается копирование (0 — это начало файла). После окончания копирования переменная будет содержать смещение конца блока. Функция sendfile() объявлена в файле .

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

Листинг 8.10. ( сору.с ) Копирование файла с помощью функции sendfile()

#include

#include

#include

#include

#include

#include

#include

int main(int argc, char* argv[]) {

 int read_fd;

 int write_fd;

 struct stat stat_buf;

 off_t offset = 0;

 /* Открытие входного файла. */

 read_fd = open(argv[1], O_RDONLY);

 /* Определение размера входного файла. */

 fstat(read_fd, &stat_buf);

 /* Открытие выходного файла для записи. */

 write_fd =

  open(argv[2], O_WRONLY | O_CREAT, stat_buf.st_mode);

 /* Передача данных из одного файла в другой. */

 sendfile(write_fd, read_fd, &offset, stat_buf.st_size);

 /* Закрытие файлов. */

 close(read_fd);

 close(write_fd);

 return 0;

}

Функция sendfile() часто используется для повышения эффективности копирования. Она широко применяется Web-серверами и сетевыми демонами, предоставляющими файлы по сети клиентским программам. Запрос обычно поступает через сокет. Серверная программа открывает локальный дисковый файл, извлекает из него данные и записывает их в сокет. Благодаря функции sendfile() эта операция существенно ускоряется.

 

8.13. Функция setitimer(): задание интервальных таймеров

Функция setitimer() является обобщением системного вызова alarm(). Она планирует доставку сигнала по истечении заданного промежутка времени.

С помощью функции setitimer() можно создавать таймеры трех типов.

■ ITIMER_REAL. По истечении указанного времени процессу посылается сигнал SIGALRM.

■ ITIMER_VIRTUAL. После того как процесс отработал требуемое время, ему посылается сигнал SIGVTALRM. Время, когда процесс не выполнялся (работало ядро или другой процесс), не учитывается.

■ ITIMER_PROF. По истечении указанного времени процессу посылается сигнал SIGPROF. Учитывается время выполнения самого процесса, а также запускаемых в нем системных вызовов.

Код таймера задается в первом аргументе функции setitimer(). Второй аргумент — это указатель на структуру типа itimerval, содержащую параметры таймера. Третий аргумент либо равен NULL, либо является указателем на другую структуру itimerval, куда будут записаны прежние параметры таймера.

В структуре itimerval два поля.

■ it_value. Здесь находится структура типа timeval, где записано время отправки сигнала. Если это поле равно нулю, таймер отменяется.

■ it_interval. Это еще одна структура timeval, определяющая, что произойдет после отправки первого сигнала. Если она равна нулю, таймер будет отменен. В противном случае здесь записан интервал генерирования сигналов.

Структура timeval была описана в разделе 8.7. "Функция gettimeofday(): системные часы"

В листинге 8.11 показано, как с помощью функции setitimer() отслеживать выполнение программы. Таймер настроен на интервал 250 мс, по истечении которого генерируется сигнал SIGVTALRM.

Листинг 8.11. ( itimer.c ) Пример создания таймера

#include

#include

#include

#include

void timer_handler(int signum) {

 static int count = 0;

 printf("timer expired %d times\n", ++count);

}

int main() {

 struct sigaction sa;

 struct itimerval timer;

 /* Назначение функции timer_handler обработчиком сигнала

    SIGVTALRM. */

 memset(&sa, 0, sizeof(sa));

 sa.sa_handler = &timer_handler;

 sigaction(SIGVTALRM, &sa, NULL);

 /* Таймер сработает через 250 миллисекунд... */

 timer.it_value.tv_sec = 0;

 timer.it_value.tv_usec = 250000;

 /* ... и будет продолжать активизироваться каждые 250

    миллисекунд. */

 timer.it_interval.tv_sec = 0;

 timer.it_interval.tv_usec = 250000;

 /* Запуск виртуального таймера. Он подсчитывает фактическое

    время работы процесса. */

 setitimer(ITIMER_VIRTUAL, &timer, NULL);

 /* Переход в бесконечный цикл. */

 while (1);

}

 

8.14. Функция sysinfo(): получение системной статистики

Функция sysinfo() возвращает системную статистике. Ее единственным аргументом является указатель на структуру типа sysinfo. Перечислим наиболее интересные поля этой структуры.

■ uptime — время в секундах, прошедшее с момента загрузки системы;

■ totalram — общий объем оперативной памяти;

■ freeram — свободный объем ОЗУ;

■ procs — число процессов, работающих в системе.

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

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

Листинг 8.12. ( sysinfo.c ) Вывод системной статистики

#include

#include

#include

#include

int main() {

 /* Константы преобразования. */

 const long minute = 60;

 const long hour = minute * 60;

 const long day = hour * 24;

 const double megabyte = 1024 * 1024;

 /* Получение системной статистики. */

 struct sysinfo si;

 sysinfo(&si);

 /* Представление информации в понятном виде. */

 printf("system uptime : %ld days, %ld:%02ld:%021d\n",

  si.uptime / day, (si.uptime % day) / hour,

  (si.uptime % hour) / minute, si.uptime % minute);

 printf("total RAM : %5.1f MB\n", si.totalram / megabyte);

 printf("free RAM : %5.1f MB\n",

 si.freeram / megabyte);

 printf("process count : %d\n", si.procs);

 return 0;

}

 

8.15. Функция uname()

Функция uname() возвращает информацию о системе, в частности сетевое и доменное имена компьютера, а также версию операционной системы. Единственным аргументом функции является указатель на структуру типа utsname. Функция заполняет следующие поля этой структуры (все эти поля содержат текстовые строки).

■ sysname. Здесь содержится имя операционной системы (например, Linux).

■ release, version. В этих полях указываются номера версии и модификации ядра.

■ machine. Здесь приводится информация о платформе, на которой работает система. В случае Intel-совместимых компьютеров это будет либо i386, либо i686, в зависимости от процессора.

■ node. Это имя компьютера.

■ __domain. Это имя домена.

Функция uname() объявлена в файле .

В листинге 8.13 показана небольшая программа, которая отображает номера версии и модификации ядра Linux, а также сообщает тип платформы.

Листинг 8.15. ( print-uname.c ) Вывод информации о ядре и платформе

#include

#include

int main() {

 struct utsname u;

 uname(&u);

 printf("%s release %s (version %s) on %s\n", u.sysname,

  u.release, u.version, u.machine);

 return 0;

}

 

Глава 9

Встроенный ассемблерный код

 

Сегодня лишь немногие программисты используют в своей практике язык ассемблера. Языки высокого уровня, такие как С и C++, поддерживаются практически на всех архитектурах и обеспечивают достаточно высокую производительность программ. Для тех редких случаев, когда требуется встроить в программу ассемблерные инструкции, в коллекции GNU-компиляторов (GCC) предусмотрены специальные средства, учитывающие особенности конкретной архитектуры.

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

В программы, написанные на языках С и C++, ассемблерные инструкции встраиваются с помощью функции asm(). Например, на платформе x86 команда

asm("fsin" : "=t" (answer) : "0" (angle));

является эквивалентом следующей инструкции языка C:

answer = sin(angle);

Обратите внимание на то, что, в отличие от обычных ассемблерных инструкций, функция asm() позволяет указывать входные и выходные операнды, используя синтаксис языка С.

Подробнее узнать об инструкциях архитектуры x86, используемых в настоящей главе, можно по следующим адресам: http://developer.intel.com/design/pentiumii/manuals и http://www.x86-64.org/documentation.

 

9.1. Когда необходим ассемблерный код

Инструкции, указываемые в функции asm(), позволяют программам напрямую обращаться к аппаратным устройствам, поэтому полученные программы выполняются быстрее. Ассемблерные инструкции используются при написании кода операционных систем. Например, файл /usr/include/asm/io.h содержит объявления команд, осуществляющих прямой доступ к портам ввода-вывода. Можно также назвать один из исходных файлов ОС Linux — /usr/src/linux/arch/i386/kernel/process.s; в нем с помощью инструкции hlt реализуется пустой цикл ожидания.

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

Иногда одна или две ассемблерные команды способны заменить целую группу высокоуровневых инструкций. Например, чтобы определить позицию самого старшего значащего бита целого числа в языке С, требуется написать цикл, тогда как во многих ассемблерных языках для этой цели существует операция bsrl. Ее использование будет продемонстрировано в разделе 9.4, "Пример".

 

9.2. Простая ассемблерная вставка

 

Вот как с помощью функции asm() осуществляется сдвиг числа на 8 битов вправо:

asm("shrl $8, %0" : "=r" (answer) : "r" (operand) : "cc");

Выражение в скобках состоит из секций, разделенных двоеточиями. В первой секции указана ассемблерная инструкция и ее операнды. Команда shrl осуществляет сдвиг первого операнда на указанное число битов вправо. Первый операнд представлен выражением %0. Второй операнд — это константа $8.

Во второй секции задаются выходные операнды. Единственный такой операнд будет помещен в C-переменную answer, которая должна быть адресуемым (левосторонним) значением. В выражении "=r" знак равенства обозначает выходной операнд, а буква r указывает на то, что значение переменной answer заносится в регистр.

В третьей секции перечислены входные операнды. Переменная operand содержит значение, подвергаемое битовому сдвигу. Выражение "r" означает, что значение переменной записывается в регистр.

Выражение "cc" в четвертой секции говорит о том. что инструкция меняет значение регистра cc (содержит код завершения).

 

9.2.1. Преобразование функции asm() в ассемблерные инструкции

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

Например, следующий фрагмент программы:

double foo, bar;

asm("mycool_asm %1, %0" : "=r" (bar) : "r" (foo));

будет преобразован в такую последовательность команд x86:

 movl -8(%ebp),%edx

 movl -4(%ebp),%ecx

#APP

 mycool_asm %edx, %edx

#NO_APP

 movl %edx,-16(%ebp)

 movl %ecx,-12(%ebp)

Переменные foo и bar занимают по два слова в стеке в 32-разрядной архитектуре x86. Регистр ebp ссылается на данные, находящиеся в стеке.

Первые две команды копируют переменную foo в регистры edx и ecx, с которыми работает инструкция mycool_asm. Компилятор решил поместить результат в те же самые регистры. Последние две команды копируют результат в переменную bar. Выбор нужных регистров и копирование операндов осуществляются автоматически.

 

9.3. Расширенный синтаксис ассемблерных вставок

 

В следующих подразделах будет описан синтаксис правил, по которым строятся выражения в функции asm(). Секции выражения отделяются друг от друга двоеточиями. Мы будем ссылаться на следующую инструкцию, которая вычисляет результат булевого выражения x > y:

asm("fucomip %%st(1), %%st; seta %%al" :

 "=a" (result) : "u" (y), "t" (x) : "cc", "st");

Сначала инструкция fucomip сравнивает два операнда, x и y, и помещает значение, обозначающее результат, в регистр cc, после чего инструкция seta преобразует это значение в 0 или 1.

 

9.3.1. Ассемблерные инструкции

Первая секция содержит ассемблерные инструкции, заключенные в кавычки. В рассматриваемом примере таких инструкций две: fucomip и seta. Они разделены точкой с запятой. Если текущий вариант языка ассемблера не допускает такого способа разделения инструкций, воспользуйтесь символом новой строки (\n).

Компилятор игнорирует содержимое первого раздела, разве что один уровень символов процента удаляется, т.е. вместо %% будет %. Смысл выражения %%st(1) и ему подобных зависит от архитектуры компьютера.

Если при компиляции программы, содержащей функцию asm(), указать опцию -traditional или -ansi, компилятор gcc выдаст предупреждение. Чтобы этого избежать, используйте альтернативное имя __asm__.

 

9.3.2. Выходные операнды

Во второй секции указаны выходные операнды инструкции. Каждому операнду соответствует строка адресации и выражение языка С, записанное в скобках. В случае выходных операндов (все они должны быть левосторонними значениями) строка адресации должна начинаться со знака равенства. Компилятор проверяет, действительно ли каждый выходной операнд является левосторонним значением (т.е может стоять в левой части оператора присваивания).

Список обозначений регистров для конкретной архитектуры можно найти в исходных текстах компилятора gcc (конкретнее — в определении макроса REG_CLASS_FROM_LETTER). Например, в файле gcc/config/i386/i386.h содержатся обозначения, соответствующие архитектуре x86 (табл. 9.1).

Таблица 9.1. Обозначения регистров в архитектуре Intel x86

Символ регистра Регистры, которые могут использоваться компилятором gcc
R Регистры общего назначения (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)
q Общие регистры хранения данных (EAX, ЕВХ, ECX, EDX)
f Регистр для чисел с плавающей запятой
t Верхний стековый регистр для чисел с плавающей запятой
u Второй после верхнего стековый регистр для чисел с плавающей запятой
a Регистр EAX
b Регистр EBX
с Регистр ECX
d Регистр EDX
x Регистр SSE (регистр потокового расширения SIMD)
y Мультимедийные регистры MMX
A Восьмибайтовое значение, формируемое из регистров EAX и EDX
D Указатель приемной строки в строковых операциях (EDI)
S Указатель исходной строки в строковых операциях (ESI)

Если есть несколько однотипных операндов, то они разделяются запятыми, как показано в секции входных операндов. Всего можно задавать до десяти операндов, адресуемых как %0, %1, … %9. Если выходные операнды отсутствуют, но есть входные операнды или модифицируемые регистры, то вторую секцию следует оставить пустой или пометить ее комментарием наподобие /* нет выходных данных */.

 

9.3.3. Входные операнды

В третьей секции задаются входные операнды. Строка адресации такого операнда не должна содержать знака равенства, в остальном синтаксис совпадает с синтаксисом выходных операндов.

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

Данную секцию можно пропустить, если входные операнды отсутствуют и следующая секция модифицируемых регистров пуста.

 

9.3.4. Модифицируемые регистры

Если в качестве побочного эффекта инструкция модифицирует значение одного или нескольких регистров, в функции asm() должна присутствовать четвертая секция. Например, инструкция fucomip меняет регистр кода завершения, обозначаемый как cc. Строки, представляющие затираемые регистры, разделяются запятыми. Если инструкция способна изменить произвольную ячейку памяти, в этой секции должно стоять ключевое слово memory. На основании этой информации компилятор определяет, какие значения должны быть загружены повторно после завершения функции asm(). При отсутствии данной секции компилятор может сделать неверное предположение о том, что регистры содержат прежние значения, и это скажется на работе программы.

 

9.4. Пример

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

Инструкция bsrl вычисляет местоположение старшего значащего бита в первом операнде и записывает результат (номер позиции начиная с нуля) во второй операнд. Например, следующая команда анализирует переменную number и помещает результат в переменную position:

asm("bsrl %1, %0" : "=r" (position) : "r" (number)};

Ей соответствует такой фрагмент на языке С:

long i;

for (i = (number >> 1), position = 0; i != 0; ++position)

 i >>= 1;

Чтобы сравнить скорость выполнения двух фрагментов, мы поместили их в цикл, где перебирается большое количество чисел. В листинге 9.1 приведена реализация на языке С. Программа перебирает значения от единицы до числа, указанного в командной строке. Для каждого значения переменной number вычисляется позиция старшего значащего бита. В листинге 9.2 показано, как сделать то же самое с помощью ассемблерной вставки. Обратите внимание на то, что в обоих случаях результат вычислений заносится в переменную result, объявленную со спецификатором volatile. Это необходимо для подавления оптимизации со стороны компилятора, который удалит весь блок вычислений, если их результаты не используются или не заносятся в память.

Листинг 9.1. ( bit-pos-loop.c ) Нахождение позиции старшего значащего бита в цикле

#include

#include

int main(int argc, char* argv[]) {

 long max = atoi(argv[1]);

 long number;

 long i;

 unsigned position;

 volatile unsigned result;

 /* Повторяем вычисления для большого количества чисел. */

 for (number = 1; number <= max; ++number) {

  /* Сдвигаем число вправо, пока результат не станет

     равным нулю.

     Запоминаем количество операций сдвига. */

 for (i = (number >> 1), position = 0; i != 0; ++position)

  i >>= 1;

  /* Позиция старшего значащего бита — это общее число

     операций сдвига, кроме первой. */

  result = position;

 }

 return 0;

}

Листинг 9.2. ( bit-pos-asm.c ) Нахождение позиции старшего значащего бита с помощью инструкции bsrl

#include

#include

int main(int argc, char* argv[]) {

 long max = atoi(argv[1]);

 long number;

 unsigned position;

 volatile unsigned result;

 /* Повторяем вычисления для большого количества чисел. */

 for (number = 1; number <= max; ++number) {

  /* Вычисляем позицию старшего значащего бита с помощью

     ассемблерной инструкции bsrl. */

  asm("bsrl %1, %0" : "=r" (position) : "r" (number));

  result = position;

 }

 return 0;

}

Скомпилируем обе версии программы в режиме полной оптимизации:

% cc -O2 -о bit-pos-loop bit-pos-loop.c

% cc -O2 -о bit-pos-asm bit-pos-asm.c

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

% time ./bit-pos-loop 250000000

19.51user 0.00system 0:20.40elapsed 95%CPU (0avgtext+0avgdata

0maxresident)k0inputs+0outputs (73major+11minor)pagefaults 0swaps

% time ./bit-pos-asm 250000000

3.19user 0.00system 0:03.32elapsed 95%CPU (0avgtext+0avgdata

0maxresident)k0inputs+0outputs (73major+11minor)pagefaults 0swaps

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

 

9.5. Вопросы оптимизации

Даже при наличии в программе ассемблерных вставок модуль оптимизации компилятора пытается переупорядочить и переписать код программы, чтобы минимизировать время ее выполнения. Когда оптимизатор обнаруживает, что выходные данные функции asm() не используются, он удаляет ее, если только ему не встречается ключевое слово volatile. Любой вызов функции asm() может быть перемещен самым непредсказуемым образом. Единственный способ гарантировать конкретный порядок ассемблерных инструкций — включить все нужные инструкции в одну функцию asm().

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

 

9.6. Вопросы сопровождения и переносимости

Если вы решили включить в программу архитектурно-зависимые ассемблерные вставки. поместите их в отдельные макросы или функции, что облегчит сопровождение программы. Когда все макросы находятся в одном файле и задокументированы, программу легче будет перенести в другую систему, так как придется переписать один-единственный файл. Например, большинство вызовов asm() в исходных текстах Linux сгруппировано в файлах /usr/src/linux/include/asm и /usr/src/linux/include/asm-i386.

 

Глава 10

Безопасность

 

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

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

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

 

10.1. Пользователи и группы

 

В Linux каждому пользователю назначается уникальный номер, называемый его идентификатором (UID, user identifier). При регистрации в системе, естественно, вводится имя пользователя, а не идентификатор. Система преобразовывает введенное имя в соответствующий идентификатор и дальше работает только с ним.

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

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

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

Но выход все же есть — это создание группы. Ей также назначается уникальный номер, называемый идентификатором группы (GID, group identifier). В каждую группу входит один или несколько идентификаторов пользователей. Один и тот же пользователь может быть членом множества групп, но членами групп не могут быть другие группы. У групп, как и у пользователей, есть имена, но они не играют практически никакой роли, так как система работает с идентификаторами групп.

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

Команда id позволяет узнать идентификатор текущего пользователя и группы, в которые он входит:

% id

uid=501(mitchell) gid=501(mitchell) groups=501(mitchell), 503(csl)

В первой части выходных данных указано, что идентификатор пользователя равен 501. В скобках приведено соответствующее этому идентификатору имя пользователя. Как следует из результатов работы команды, пользователь mitchell входит в две группы: с номером 501 (mitchell) и с номером 503 (csl). Читатели, возможно, удивлены тем, что группа 501 появляется дважды: в поле gid и в поле groups. Объяснение этому факту будет дано позже.

 

10.1.1. Суперпользователь

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

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

 

10.2. Идентификаторы пользователей и групп, закрепленные за процессами

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

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

Теперь становится понятным смысл поля gid в выводе команды id. В нем показан идентификатор группы текущего процесса. Пользователь 501 может входить в несколько групп, но текущему процессу соответствует только один идентификатор группы. В рассматривавшемся примере это 501.

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

Узнать идентификаторы пользователя и группы текущего процесса позволяют функции geteuid() и getegid(), объявленные в файле . Они не принимают никаких аргументов и всегда работают, так что проверять ошибки не обязательно. В листинге 10.1 показана программа, которая частично дублирует работу команды id.

Листинг 10.1. ( simpleid.c ) Отображение идентификаторов пользователя и группы

#include

#include

#include

int main() {

 uid_t uid = geteuid();

 gid_t gid = getegid();

 printf("uid=%d gid=%d\n", (int) uid, (int)gid);

 return 0;

}

Если программу запустит тот же пользователь, который ранее запустил команду id, результат будет таким:

% ./simpleid

uid=501 gid=501

 

10.3. Права доступа к файлам

 

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

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

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

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

Совокупность прав доступа к файлу называется кодом режима. Он состоит из трех триад битов, соответствующих владельцу, группе и остальным пользователям. В каждой триаде первый бит означает право чтения, второй — право записи, третий — право выполнения. Символьное представление этих битов называется строкой режима. Просмотреть ее можно с помощью команды ls -l или системного вызова stat(). Задание прав доступа к файлу осуществляется с помощью команды chmod или одноименного системного вызова. Допустим, имеется файл hello и требуется узнать права доступа к нему. Вот как это делается:

% ls -l hello

-rwxr-x--- 1 samuel csl 11734 Jan 22 16:29 hello

Третье и четвертое поля выводных данных сообщают о том, что файл принадлежит пользователю samuel и группе csl. В первом поле отображается строка режима. Начальный дефис указывает на то, что это обычный файл. В случае каталога здесь будет стоять буква d. Специальные файлы, например файлы устройств (см. главу 6, "Устройства") или каналы (см. раздел 5.4, "Каналы"), обозначаются другими буквами. Следующие три символа соответствуют правам владельца файла. В данном случае пользователь samuel имеет право чтения, записи и выполнения файла. Далее указаны права группы, которой принадлежит файл. Пользователям группы разрешено читать и выполнять файл. Последние три символа в строке режима обозначают права остальных пользователей, которым запрещен доступ к файлу.

Давайте проверим, действительно ли все вышесказанное — правда. Для начала попробуем обратиться к файлу от имени пользователя nobody, не входящего в группу csl:

% id

uid=99(nobody) gid=99(nobody) groups=99(nobody)

% cat hello

cat: hello: Permission denied

% echo hi > hello

sh: ./hello: Permission denied

% ./hello

sh: ./hello: Permission denied

Команда cat не смогла выполниться, потому что у нас нет права чтения файла. Запись в файл тоже не разрешена, поэтому потерпела неудачу команда echo. А поскольку право выполнения также отсутствует, запустить программу hello не удалось.

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

% id

uid=501 (mitchell) gid=501 {mitchell) groups=501 (mitchell), 503 (csl)

% cat hello

#!/bin/bash

echo "Hello, world."

% ./hello

Hello, world.

% echo hi > hello

bash: ./hello: Permission denied

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

% id

uid=502(samuel) gid=502(samuel) groups=502(samuel),503(csl)

% echo hi > hello

% cat hello

hi

Менять режим доступа к файлу может только его владелец, а также суперпользователь. Если требуется разрешить всем пользователям запускать файл на выполнение, то это делается так:

% chmod o+k hello

% ls -l hello

-rwxr-x--x 1 samuel csl 3 Jan 22 16:38 hello

Обратите внимание на появление буквы x в конце строки режима. Флаг о+x команды chmod означает добавление (+) права выполнения (x) для остальных пользователей (о). Если требуется, к примеру, отнять право записи у группы, следует задать такой флаг: g-w.

Функция stat() позволяет определить режим доступа к файлу программным путем. Она принимает два аргумента: имя файла и адрес структуры, заполняемой информацией о файле. Подробнее функция stat() описана в приложении Б, "Низкоуровневый ввод-вывод". Пример ее использования показан в листинге 10.2.

Листинг 10.2. ( stat-perm.c ) Проверка того, имеет ли владелец право записи в файл

#include

#include

int main(int argc, char* argv[]) {

 const char* const filename = argv[1];

 struct stat buf;

 /* Получение информации о файле. */

 stat(filename, &buf);

 /* Если владельцу разрешена запись в файл,

    отображаем сообщение. */

 if (buf.st_mode & S_IWUSR)

  printf("Owning user can write '%s'.\n", filename);

 return 0;

}

Если запустить программу с файлом hello, будет выдано следующее:

% ./stat-perm hello

Owning user can write 'hello'.

Константа S_IWUSR соответствует праву записи для владельца. Для каждого бита в строке режима существует своя константа. Например, константа S_IRGRP обозначает право чтения для группы, а константа S_IXOTH — право выполнения для остальных пользователей. Если невозможно получить информацию о файле, функция stat() возвращает -1 и помещает код ошибки в переменную errno.

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

chmod("hello", S_IRUSR | S_IXUSR);

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

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

Есть, правда, одно важное исключение: процессы, запускаемые пользователем root (его идентификатор равен нулю), всегда получают доступ к требуемым файлам независимо от их атрибутов.

 

10.3.1. Проблема безопасности: программы без права выполнения

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

 

10.3.2. Sticky-бит

Помимо обычных битов режима есть один особый бит, называемый sticky-битом ("липучкой"). Он применим только в отношении каталогов.

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

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

О наличии sticky-бита говорит буква t в конце строки режима:

% ls -ld /trap

drwxrwxrwt 12 root root 2048 Jan 24 17:51 /tmp

Соответствующий флаг функций stat() и chmod() называется S_ISVTX.

Если требуется установить для каталога sticky-бит. следует воспользоваться такой командой:

% chmod o+t каталог

А вот как можно назначить каталогу те же права доступа, что и к каталогу /tmp:

chmod(dir_path, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);

 

10.4. Реальные и эффективные идентификаторы

 

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

Упомянутые выше функции geteuid() и getegid() возвращают эффективные идентификаторы пользователя и группы. Для определения реальных идентификаторов предназначены функции getuid() и getgid().

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

Прежде чем выяснять, как менять эффективный идентификатор процесса, рассмотрим, зачем это необходимо. Предположим, имеется серверный процесс, который может просматривать любой файл независимо от того, кто является его владельцем. Такой процесс должен быть запущен пользователем root, так как только у него есть подобные привилегии. А теперь допустим, что запрос к файлу поступает от конкретного пользователя (mitchell, к примеру). Серверный процесс может проверить, есть ли у данного пользователя соответствующие разрешения, но это означает дублирование того кода, который уже реализован в ядре.

Гораздо лучший подход — временно поменять эффективный идентификатор пользователя root на mitchell и попытаться выполнить требуемую операцию. Если пользователь mitchell не имеет нужных прав доступа, ядро само даст об этом знать. После завершения (или отмены) операции серверный процесс восстановит первоначальный эффективный идентификатор.

Механизм временной замены идентификаторов используется программами, которые выполняют аутентификацию пользователей, пытающихся зарегистрироваться в системе. Такие программы работают с правами пользователя root. Когда пользователь вводит свое имя и пароль, программа аутентификации сверяет их с записями в системной базе паролей. Если проверка прошла успешно, программа меняет свои эффективные и реальные идентификаторы на пользовательские, после чего выполняет функцию exec(), которая запускает интерпретатор команд. В результате пользователь оказывается в среде интерпретатора, идентификаторы которого соответствуют пользовательским.

Функция, меняющая пользовательский идентификатор процесса, называется setreuid() (имеется, конечно же, и функция setregid()). Она принимает два аргумента: устанавливаемый реальный идентификатор и требуемый эффективный идентификатор. Вот как, к примеру, можно поменять эффективный и реальный идентификаторы:

setreuid(geteuid(), getuid());

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

■ заменять эффективный идентификатор реальным;

■ заменять реальный идентификатор эффективным;

■ переставлять местами значения реального и эффективного идентификаторов.

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

В качестве любого из аргументов функции setreuid() можно указать значение -1. Это означает, что соответствующий идентификатор нужно оставить без изменений. Есть также вспомогательная функция seteuid(), которая меняет эффективный идентификатор, но не трогает реальный. Например, следующие две строки эквивалентны:

seteuid(id);

setreuid(-1, id);

 

10.4.1. Программы с установленным битом SUID

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

% whoami

mitchell

% su

Password: ...

% whoami

root

Команда whoami аналогична команде id, но отображает только эффективный идентификатор пользователя. Команда su позволяет вызвавшему ее пользователю стать суперпользователем, если введен правильный пароль.

Как же работает команда su? Ведь мы знаем, что интерпретатор команд был запущен с реальным и эффективным идентификаторами, равными mitchell. Функция setreuid() не позволит ему поменять ни один из них.

Дело в том, что у программы su установлен бит смены идентификатора пользователя (SUID, set user identifier). Это значит, что при запуске ее эффективным идентификатором станет идентификатор владельца (реальный идентификатор останется тем же, что у пользователя, запустившего программу). Для установки бита SUID предназначены команда chmod +s и флаг S_SUID функции chmod().

В качестве примера рассмотрим программу, показанную в листинге 10.3.

Листинг 10.3. ( setuid-test.c ) Проверка идентификаторов

#include

#include

int main() {

 printf("uid=%d euid=%d\n", (int)getuid(), (int)geteuid());

 return 0;

}

Теперь предположим, что у программы установлен бит SUID и она принадлежит пользователю root. В этом случае вывод команды ls будет примерно таким:

-rwsrws--x 1 root root 11931 Jan 24 18:25 setuid-test

Буквы s в строке режима означают, что этот файл не только является исполняемым, но для него установлены также биты SUID и SGID. Результат работы программы будет таким:

% whoami

mitchell

% ./setuid-test

uid=501 euid=0

Обратите внимание на то, что эффективный идентификатор стал равным нулю. Устанавливать биты SUID и SGID позволяют команда chmod u+s и chmod g+s соответственно. Приведем пример:

% ls -l program

-rwxr-xr-x 1 samuel csl 0 Jan 30 23:38 program

% chmod g+s program

% ls -l program

-rwxr-sr-x 1 samuel csl 0 Jan 30 23:38 program

% chmod u+s program

% ls -l program

-rwsr-sr-x 1 samuel csl 0 Jan 30 23:38 program

Аналогичным целям служат флаги S_ISUID и S_ISGID функции chmod().

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

Рассмотрим атрибуты программы su:

% ls -l /bin/su

-rwsr-xr-x 1 root root 14188 Mar 7 2000 /bin/su

Как видите, она принадлежит пользователю root и для нее установлен бит SUID. Обратите внимание на то, что команда su не меняет идентификатор интерпретатора команд, в котором она была вызвана, а запускает новый интерпретатор с измененным идентификатором. Первоначальный интерпретатор будет заблокирован до тех пор, пока пользователь не введет exit.

 

10.5. Аутентификация пользователей

Программы, у которых установлен бит SUID, не должны запускаться кем попало. Например, программа su, прежде чем менять идентификатор пользователя, заставляет его ввести пароль. Это называется аутентификацией — программа проверяет, получил ли пользователь права суперпользователя.

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

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

При написании аутентификационной программы важно позволить системному администратору использовать тот механизм аутентификации, который он считает приемлемым. В Linux этой цели служат подключаемые модули аутентификации (РАМ, pluggable authentication modules). Рассмотрим простейшее приложение (листинг 10.4).

Листинг 10.4. ( pam.c ) Пример использования модулей РАМ

#include

#include

#include

int main() {

 pam_handle_t* pamh;

 struct pam_conv pamc;

 /* Указание диалоговой функции. */

 pamc.conv = &misc_conv;

 pamc.eppdata_ptr = NULL;

 /* Начало сеанса аутентификации. */

 pam_start("su", getenv("USER"), &pamc, &pamh);

 /* Аутентификация пользователя. */

 if (pam_authenticate(pamh, 0) != PAM_SUCCESS)

  fprintf(stderr, "Authentication failed!\n");

 else

  fprintf(stderr, "Authentication OK.\n");

 /* Конец сеанса. */

 pam_end(pamh, 0);

 return 0;

}

Чтобы скомпилировать эту программу, необходимо подключить к ней две библиотеки: libpam и libpam_misс:

% gcc -о para pam.c -lpam -lpam_misc

Сначала программа создает объект диалога, который используется библиотекой РАМ, когда ей требуется запросить у пользователя данные. Функция misc_conv(), адрес которой записывается в объект, — это стандартная диалоговая функция, осуществляющая терминальный ввод-вывод. Можно написать собственную функцию, отображающую всплывающее окно, использующую голосовой ввод-вывод или реализующую другие способы общения с пользователем.

Затем вызывается функция pam_start(), которая инициализирует библиотеку РАМ. Первый аргумент функции — это имя сервиса. Оно должно уникальным образом идентифицировать приложение. Программа не будет работать, пока системный администратор не настроит систему на использование указанного сервиса. В данном случае задействуется сервис su, при котором программа аутентифицирует пользователей так же, как это делает команда su. В реальных программах так поступать не следует. Выберите реальное имя сервиса и создайте сценарий инсталляции, который позволит системному администратору правильно настраивать механизм аутентификации.

Второй аргумент функции — это имя пользователя, которого требуется аутентифицировать. В данном примере имя пользователя берется из переменной среды USER (обычно это имя соответствует эффективному идентификатору текущего процесса, но так бывает не всегда). В большинстве реальных программ в данном месте выдается запрос на ввод имени. Третьим аргументом является ссылка на объект диалога. В четвертом аргументе указывается переменная, в которую функция pam_start() запишет дескриптор сеанса. Этот дескриптор должен передаваться всем последующим функциям библиотеки РАМ.

Далее в программе вызывается функция pam_authenticate(). Во втором ее аргументе указываются различные флаги. Значение 0 означает стандартные установки. Возвращаемое значение функции говорит о том. как прошла аутентификация. В конце программы вызывается функция pam_end(), которая удаляет выделенные ранее структуры данных.

Предположим, что пользователь должен ввести пароль "password". Если это будет сделано, получим следующее:

% ./pam

Password: password

Authentication OK.

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

Вот что произойдет, если в систему попробует вломиться хакер:

% ./pam

Password: badguess

Authentication failed!

Полное описание работы модулей аутентификации приведено в каталоге /usr/doc/pam.

 

10.6. Дополнительные проблемы безопасности

 

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

 

10.6.1. Переполнение буфера

Почти псе основные Internet-демоны, включая демоны таких программ, как sendmail, finger, talk и др., подвержены атакам типа переполнение буфера. О них следует обязательно помнить при написании программ, которые должны выполняться с правами пользователя root, а также программ, осуществляющих межзадачное взаимодействие или читающих файлы, которые не принадлежат пользователю, запустившему программу.

Суть атаки заключается в том, чтобы заставить программу выполнить код, который она не собиралась выполнять. Типичный механизм достижения этой цели — перезапись части стека программы. В стеке, помимо всего прочего, сохраняется адрес памяти, по которому программа передает управление после завершения текущей функции. Следовательно, если поместить код взлома в памяти, а затем изменить адрес возврата так. чтобы он указывал на этот код, то по завершении текущей функции программа начнет выполнять код хакера с правами текущего процесса. Если процесс принадлежит пользователю root, последствия будут катастрофическими. Если атаке подвергся процесс другого пользователя, катастрофа наступит "только" для него (а также для любого пользователя, который работает с файлами пострадавшего).

Хуже всего обстоит дело с программами, которые работают в режиме демона и ожидают поступление запросов на подключение. Демоны обычно принадлежат пользователю root. Если в программе есть описываемая "дыра", любой, кто сможет подключиться к этой программе, способен захватить контроль над компьютером, послав по сети "смертельную" последовательность данных. Программы, не работающие с сетью, гораздо безопаснее, так как их могут атаковать только пользователи, уже зарегистрировавшиеся в системе.

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

#include

int main() {

 /* Никто, будучи в здравом уме, не выбирает имя пользователя

    длиной более 32 символов. Кроме того, я думаю, что в UNIX

    допускаются только 8-символьные имена. Поэтому выделенного

    буфера должно быть достаточно. */

 char username[32];

 /* Предлагаем пользователю ввести свое имя. */

 printf("Enter your username: ");

 /* Читаем введенную строку. */

 gets(username);

 /* Выполняем другие действия... */

return 0;

}

Комбинация 32-символьного буфера и функции gets() делает возможным переполнение буфера. Функция gets() читает вводимые данные до тех пор, пока не встретится символ новой строки, после чего помещает весь результат в массив username. В комментариях к программе предполагается, что пользователи выбирают себе короткие имена, не превышающие в длину 32 символа. Но при написании защищенных программ необходимо помнить о существовании хакеров. В данном случае хакер может выбрать сколь угодно длинное имя. Локальные переменные, в частности username, сохраняются в стеке, поэтому выход за пределы массива оборачивается тем, что в стек помещаются произвольные данные.

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

char* username = getline(NULL, 0, stdin);

Функция getline() автоматически вызывает функцию malloc(), которая выделяет буфер для введенной строки и возвращает указатель на него. Естественно, следует не забыть вызвать функцию free(), чтобы по окончании работы с буфером вернуть память системе.

Ситуация еще проще, если используется язык C++, где есть готовые строковые примитивы. В C++ ввод строки осуществляется так:

string username;

getline(cin, username);

Буфер строки username удаляется автоматически, поэтому даже не придется вызывать функцию free().

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

 

10.6.2. Конкуренция доступа к каталогу /tmp

Другая распространенная проблема безопасности связана с созданием файлов с предсказуемыми именами, в основном в каталоге /tmp. Предположим, что программа prog, выполняющаяся от имени пользователя root, всегда создает временный файл /tmp/prog и помещает в него важную информацию. Тогда злоумышленник может заранее создать символическую ссылку /tmp/prog на любой другой файл в системе. Когда программа попытается создать временный файл, функция open() завершится успешно, но в действительности вернет дескриптор символической ссылки. Любые данные, записываемые во временный файл, окажутся перенаправленными в файл злоумышленника.

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

Посредством этой атаки часто уничтожаются системные файлы. Создав нужную символическую ссылку, хакер может заставить программу, выполняющуюся с правами суперпользователя, затереть важный системный файл, например /etc/passwd.

Один из способов избежать такой атаки — создавать временные файлы со случайными именами. Например, можно прочитать из устройства /dev/random случайные данные и включить их в имя файла. Это усложнит задачу хакеру, но не остановит его полностью. Он может попытаться создать большое число символических ссылок с потенциально верными именами. Даже если их будет 10000, одна верная догадка приведет к непоправимому.

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

В разделе 2.1.7, "Временные файлы", рассказывалось о применении функции mkstemp() для создания временных файлов. К сожалению, в Linux эта функция открывает файл с флагом O_EXCL после того, как было выбрано трудно угадываемое имя. Другими словами, применять функцию небезопасно, если каталог /tmp смонтирован через NFS.

Прием, который всегда работает, заключается в вызове функции lstat() (рассматривается в приложении Б, "Низкоуровневый ввод-вывод") для созданного файла. Она отличается от функции stat() тем, что возвращает информацию о самой символической ссылке, а не о файле, на который она ссылается. Если функция сообщает, что новый файл является обычным файлом, а не символической ссылкой, и принадлежит владельцу программы, то все в порядке.

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

Листинг 10.5. ( temp-file.c ) Безопасное создание временного файла

#include

#include

#include

#include

/* Функция возвращает дескриптор созданного временного файла.

   Файл будет доступен для чтения и записи только тому

   пользователю, чей идентификатор равен эффективному

   идентификатору текущего процесса. Если файл не удалось создать,

   возвращается -1. */

int secure_temp_file() {

 /* Этот дескриптор ссылается на устройство /dev/random, из

    которого будут получены случайные данные. */

 static int random_fd = -1;

 /* Случайное целое число. */

 unsigned int random;

 /* Буфер для преобразования числа в строку. */

 char filename[128];

 /* дескриптор создаваемого временного файла. */

 int fd;

 /* информация о созданном файле. */

 struct stat stat_buf;

 /* Если устройство /dev/random еще не было открыто,

    открываем его. */

 if (random_fd == -1) {

  /* Открытие устройства /dev/random. Предполагается, что

     это устройство является источником случайных данных,

     а не файлом, созданным хакером. */

  random_fd = open("/dev/random", O_RDONLY);

  /* Если устройство /dev/random не удалось открыть,

     завершаем работу. */

  if (random_fd == -1)

   return -1;

 }

 /* чтение целого числа из устройства /dev/random. */

 if (read(random_fd, &random, sizeof(random)) != sizeof(random))

  return -1;

 /* Формирование имени файла из случайного числа. */

 sprintf(filename, "/tmp/%u", random);

 /* Попытка открытия файла. */

 fd = open(filename,

  /* Используем флаг O_EXCL. */

  O_RDWR | O_CREAT | O_EXCL,

  /* Разрешаем доступ только владельцу файла. "/

  S_IRUSR | S_IWUSR);

 if (fd == -1)

  return -1;

 /* Вызываем функцию lstat(), чтобы проверить, не является ли

    файл символической ссылкой */

 if (lstat(filename, &stat_buf) == -1)

  return -1;

 /* Если файл не является обычным файлом, кто-то пытается

    обмануть нас. */

 if (!S_ISREG(stat_buf.st_mode))

  return -1;

 /* Если файл нам не принадлежит, то, возможно, кто-то успел

    подменить его. */

 if (stat_buf.st_uid != geteuid() ||

  stat_buf.st_gid != getegid())

  return -1;

 /* Если у файла установлены дополнительные биты доступа,

    что-то не так. */

 if ((stat_buf.st_mode & ~(S_IRUSR | S_IWUSR)) != 0)

  return -1;

 return fd;

}

Рассмотренная функция вызывает функцию open() для создания файла, а затем функцию lstat() для проверки того, что файл не является символической ссылкой. Внимательный читатель обнаружит здесь то, что называется состоянием гонки. Между вызовами функций open() и lstat() злоумышленник может успеть удалить файл и заменить его символической ссылкой. Это не вызовет разрушающих последствий, но приведет к тому, что функция завершится ошибкой и не сможет выполнить свою задачу. Такой тип атаки называется отказом от обслуживания.

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

Грамотный системный администратор не допустит, чтобы каталог /tmp был смонтирован через NFS, поэтому на практике можно пользоваться функцией mkstemp(). Если же речь идет о другом каталоге, то нельзя ни доверять флагу O_EXCL, ни рассчитывать на установку sticky-бита.

 

10.6.3. Функции system() и popen()

Третья распространенная проблема безопасности, о которой должен помнить каждый программист, заключается в несанкционированном запуске программ через интерпретатор команд. В качестве наглядной демонстрации рассмотрим сервер словарей. Серверная программа ожидает поступления запросов через Internet. Клиент посылает слово, а сервер сообщает, является ли оно корректным словом английского языка. В любой Linux-системе имеется файл /usr/dict/words, в котором содержится список 45000 слов, поэтому серверу достаточно выполнить такую команду:

% grep -х слово /usr/dict/words

Код завершения команды grep сообщит о том, обнаружено ли указанное слово в файле /usr/dict/words.

В листинге 10.6 показан пример реализации поискового модуля сервера.

Листинг 10.6. ( grep-dictionary.c ) Поиск слова в словаре

#include

#include

/* Функция возвращает ненулевое значение, если аргумент WORD

   встречается в файле /usr/dict/words. */

int grep_for_word(const char* word) {

 size_t length;

 char* buffer;

 int exit_code;

 /* Формирование строки 'grep -x WORD /usr/dict/words'.

    Строка выделяется динамически во избежание

    переполнения буфера. */

 length =

  strlen("grep -х ") + strlen(word) +

 strlen(" /usr/dict/words") + 1;

 buffer = (char*)malloc(length);

 sprintf(buffer, "grep -x %s /usr/dict/words", word);

 /* Запуск команды. */

 exit_code = system(buffer);

 /* Очистка буфера. */

 free(buffer);

 /* Если команда grep вернула значение 0, значит, слово найдено

    в словаре. */

 return exit_code == 0;

}

Обратите внимание на подсчет числа символов в строке и динамическое выделение буфера, что позволяет обезопасить программу от переполнения буфера. К сожалению, небезопасна сама функция system() (описана в разделе 3.2.1, "Функция system()"). Функция вызывает стандартный интерпретатор команд и принимает от него код завершения. Но что произойдет, если злоумышленник вместо слова введет показанную ниже строку?

foo /dev/null; rm -rf /

В этом случае сервер выполнит такую команду:

grep -х foo /dev/null; rm -rf / /usr/dict/words

Теперь проблема стала очевидной. Пользователь запустил одну команду, якобы grep, а на самом деле их оказалось две, так как интерпретатор считает точку с запятой разделителем команд. Первая команда — это по-прежнему безобидный вызов утилиты grep, зато вторая команда пытается удалить все файлы в системе. Даже если серверная программа не имеет привилегий суперпользователя, она удалит все файлы, доступные запустившему ее пользователю. Похожая проблема возникает и при использовании функции popen() (описана в разделе 3.4.4, "Функции popen() и pclose()"), которая создает канал между родительским и дочерним процессами, но тоже вызывает интерпретатор для запуска команды.

Существуют два способа устранения подобных проблем. Первый заключается в использовании функции семейства exec() вместо функции system() или popen(). Специальные символы интерпретатора команд (например, точка с запятой) не подвергаются обработке, если они присутствуют в списке аргументов функции exec(). Естественно, при этом пропадают преимущества таких функций, как system() и popen().

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

 

Глава 11

Демонстрационное Linux-приложение

 

В этой главе кусочки мозаики сложатся в единую композицию. Мы опишем и реализуем законченную Linux-программа, в которой объединятся многие рассмотренные в данной книге методики. Программа через протокол HTTP выдает информацию о системе, в которой она работает.

 

11.1. Обзор

 

Демонстрационная программа является частью пакета мониторинга Linux-системы и предоставляет следующие возможности.

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

■ Программа не работает со статическими HTML-страницами. Все страницы динамически генерируются модулями, каждый из которых вычисляет итоговую информацию о какой-либо характеристике системы.

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

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

■ Серверу не требуются привилегии суперпользователя (он не работает с привилегированным портом). Это ограничивает его в доступе к системной информации.

Программу сопровождают четыре модуля, в которых иллюстрируются методики сбора системной информации. В модуле time используется системный вызов gettimeofday(). В модуле issue применяются функции низкоуровневого ввода-вывода и системный вызов sendfile(). В модуле diskfree показано, как с помощью функций fork(), exec() и dup2() выполнять команды в дочерних процессах. В модуле processes продемонстрирована работа с файловой системой /proc.

 

11.1.1. Существующие ограничения

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

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

■ Программа не претендует на полную совместимость со спецификациями HTML (http://www.w3.org/MarkUp/). Она генерирует простые HTML-страницы, которые могут обрабатываться популярными Web-броузерами.

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

■ Мы не пытаемся регулировать объем ресурсов (число процессов, объем используемой памяти), потребляемых сервером или его модулями. Многие многозадачные Web-серверы обслуживают запросы посредством фиксированного пула процессов, а не создают новый дочерний процесс для каждого соединения.

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

Протокол HTTP

Протокол HTTP (Hypertext Transport Protocol) используется для организации взаимодействия Web-клиентов и серверов. Клиент подключается к серверу, устанавливая соединение с заранее известным портом (обычно его номер — 80). Запросы и заголовки HTTP представляются в виде обычного текста.

Подключившись к серверу, клиент посылает запрос. Типичный запрос выглядит так: GET /page HTTP/1.0 . Метод GET означает запрос на получение Web-страницы. Второй элемент — это путь к странице. В третьем элементе указан протокол и его версия. В последующих строках содержатся поля заголовка отформатированные наподобие заголовков почтовых сообщений. В них приведена дополнительная информация о клиенте. Заголовок оканчивается пустой строкой.

В ответ сервер сообщает результат обработки запроса. Типичный ответ таков: HTTP/1.0 200 OK . Первый элемент — это версия протокола. В следующих двух элементах описан результат. В данном случае код 200 означает успешное выполнение запроса. Далее идут поля заголовка, который, оканчивается пустой строкой. После заголовка сервер может передать произвольные данные.

Обычно сервер возвращает HTML-код Web-страницы. В рассматриваемом примера в заголовке ответа будет указано следующее: Content-type: text/html .

Спецификацию протокола HTTP можно получить по адресу http://www.w3.org/Protocols .

 

11.2. Реализация

 

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

В каждом исходном файле экспортируются функции и переменные, используемые в других частях программы. Для простоты все они объявлены в одном файле заголовков: server.h (листинг 11.1). Функции, применяемые в рамках только одного модуля, объявлены со спецификатором static и не включены в файл server.h.

Листинг 11.1. ( server.h ) Объявления функций и переменных

#ifndef SERVER_H

#define SERVER_H

#include

#include

/*** Символические константы файла common.c. ********************/

/* Имя программы. */

extern const char* program_name;

/* Если не равна нулю, отображаются развернутые сообщения. */

extern int verbose;

/* Напоминает функцию malloc(), не прерывает работу программы,

   если выделить память не удалось. */

extern void* xmalloc(size_t size);

/* Напоминает функцию realloc(), но прерывает работу программы,

   если выделить память не удалось */

extern void* xrealloc(void* ptr, size_t size);

/* Напоминает функцию strdup(), но прерывает работу программы,

   если выделить память не удалось. */

extern char* xstrdup(const char* s);

/* Выводит сообщение об ошибке заданного системного вызова

   и завершает работу программы. */

extern void system_error(const char* operation);

/* Выводит сообщение об ошибке и завершает работу программы. */

extern void error(const char* cause, const char* message);

/* Возвращает имя каталога, содержащего исполняемый файл

   программы. Поскольку возвращается указатель на область памяти,

   вызывающая подпрограмма должна удалить ее с помощью

   функции free(). В случае неудачи выполнение программы

   завершается. */

extern char* get_self_executable_directory();

/*** Символические константы файла module.с *********************/

/* Экземпляр загруженного серверного модуля. */

struct server_module {

 /* Дескриптор библиотеки, в которой находится модуль. */

 void* handle;

 /* Описательное имя модуля. */

 const char* name;

 /* Функция, генерирующая HTML-код для модуля. */

 void (*generatе_function)(int);

};

/* Каталог, из которого загружаются модули. */

extern char* module_dir;

/* Функция, пытающаяся загрузить указанный серверный модуль.

   Если модуль существует, возвращается структура

   с его описанием, в противном случае возвращается NULL. */

extern struct server_module* module_open(const char* module_path);

/* Закрытие модуля и удаление объекта MODULE. */

extern void module_close(struct server_module* module);

/*** Символические константы файла server.c. ********************/

/* Запуск сервера по адресу LOCAL_ADDRESS и порту PORT. */

extern void server_run(struct in_addr local_address, uint16_t port);

#endif /* SERVER_H */

 

11.2.1. Общие функции

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

Листинг 11.2. ( common.c ) Функции общего назначения

#include

#include

#include

#include

#include

#include "server.h"

const char* program_name;

int verbose;

void* xmalloc(size_t size) {

 void* ptr = malloc(size);

 /* Аварийное завершение, если выделить память не удалось. */

 if (ptr == NULL)

  abort();

 else

  return ptr;

}

void* xrealloc(void* ptr, size_t size) {

 ptr = realloc(ptr, size);

 /* Аварийное завершение, если выделить память не удалось. */

 if (ptr == NULL)

  abort();

 else

  return ptr;

}

char* xstrdup(const char* s) {

 char* copy = strdup(s);

 /* Аварийное завершение, если выделить память не удалось. */

 if (сору == NULL)

  abort();

 else

  return copy;

}

void system_error(const char* operation) {

 /* Вывод сообщения об ошибке на основании значения

    переменной errno. */

 error(operation, strerror(errno));

}

void error(const char* cause, const char* message) {

 /* Запись сообщения об ошибке в поток stderr. */

 fprintf(stderr, "%s: error: (%s) %s\n", program_name,

  cause, message);

 /* Завершение программы */

 exit(1);

}

char* get_self_executable_directory() {

 int rval;

 char link_target[1024];

 char* last_slash;

 size_t result_length;

 char* result;

/* Чтение содержимого символической ссылки /proc/self/exe. */

 rval =

  readlink("/proc/self/exe", link_target,

 sizeof(link_target));

 if (rval == -1)

 /* Функция readlink() завершилась неудачей, поэтому выходим

    из программы. */

 abort();

 else

  /* Запись нулевого символа в конец строки. */

  link_target[rval] = '\0';

 /* Удаление имени файла,

    чтобы осталось только имя каталога. */

 last_slash = strrchr(link_target, '/');

 if (last_slash == NULL || last_slash == link_target)

 /* Формат имени некорректен. */

 abort();

 /* Выделение буфера для результирующей строки. */

 result_length = last_slash - link_target;

 result = (char*)xmalloc(result_length + 1);

 /* Копирование результата. */

 strncpy(result, link_target, result_length);

 result[result_length] = '\0';

 return result;

}

Приведенные здесь функции можно использовать в самых разных программах.

■ Функции xmalloc(), xrealloc() и xstrdup() являются расширенными версиями стандартных функций malloc(), realloc() и strdup(), в которые дополнительно включен код проверки ошибок. В отличие от стандартных функций, которые возвращают пустой указатель в случае ошибки, наши функции немедленно завершают работу программы, если в системе недостаточно памяти.

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

■ Функция error() сообщает о фатальной ошибке, произошедшей в программе. При этом в поток stderr записывается сообщение об ошибке, и работа программы завершается. Для ошибок, произошедших в системных вызовах или библиотечных функциях, предназначена функция system_error(), которая генерирует сообщение об ошибке на основании значения переменной errno (см. раздел 2.2.3, "Коды ошибок системных вызовов").

■ Функция get_self_executable_directory() определяет каталог, в котором содержится исполняемый файл текущего процесса. Это позволяет программе находить свои внешние компоненты. Функция проверяет содержимое символической ссылки /proc/self/exe (см. раздет 7.2.1, "Файл /proc/self).

В файле common.c определены также две полезные глобальные переменные.

■ Переменная program_name содержит имя выполняемой программы, указанное в списке аргументов командной строки (см. раздел 2.1.1, "Список аргументов").

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

 

11.2.2. Загрузка серверных модулей

В файле module.c (листинг 11.3) содержится реализация динамически загружаемых серверных модулей. Загруженному модулю соответствует структура типа server_module, который определен в файле server.h.

Листинг 11.3. ( module.c ) Загрузка и выгрузка серверных модулей

#include

#include

#include

#include

#include "server.h"

char* module_dir;

struct server_module* module_open(const char* module_name) {

 char* module_path;

 void* handle;

 void (*module_generate)(int);

 struct server_module* module;

 /* Формирование путевого имени библиотеки, в которой содержится

    загружаемый модуль. */

 module_path =

  (char*)xmalloc(strlen(module_dir) +

  strlen(module_name) + 2);

 sprintf(module_path, "%s/%s", module_dir, module_name);

 /* Попытка открыть файл MODULE_PATH как совместно используемую

    библиотеку. */

 handle = dlopen(module_path, RTLD_NOW);

 free (module_path);

 if (handle == NULL) {

  /* Ошибка: либо путь не существует, либо файл не является

     совместно используемой библиотекой. */

  return NULL;

 }

 /* Чтение константы module_generate из библиотеки. */

 module_generatе =

  (void(*)int))dlsym(handle,

  "module_generate");

 /* Проверяем, найдена ли константа. */

 if (module_generate == NULL) {

  /* Константа отсутствует в библиотеке. Очевидно, файл не

     является серверным модулем. */

  dlclose(handle);

  return NULL;

 }

 /* Выделение и инициализация объекта server_module. */

 module =

  (struct server_module*)xmalloc

  (sizeof (struct server_module));

 module->handle = handle;

 module->name = xstrdup(module_name);

 module->generate_function = module_generate;

 /* Успешное завершение функции. */

 return module;

}

void module_close(struct server_module* module) {

 /* Закрытие библиотеки. */

 dlclose(module->handle);

 /* Удаление строки с именем модуля. */

 free((char*)module->name);

 /* Удаление объекта module. */

 free(module);

}

Каждый модуль содержится в файле совместно используемой библиотеки (см. раздел 2.3.2, "Совместно используемые библиотеки") и должен экспортировать функцию module_generate(). Эта функция генерирует HTML-код Web-страницы и записывает его в сокет, дескриптор которого передан ей в качестве аргумента.

В файле module.c определены две функции.

■ Функция module_open() пытается загрузить серверный модуль с указанным именем. Файл модуля имеет расширение .so, так как это совместно используемая библиотека. Функция открывает библиотеку с помощью функции dlopen() и ищет в библиотеке константу module_generate посредством функции dlsym() (описаны в разделе 2.3.6, "Динамическая загрузка и выгрузка"). Если библиотеку не удалось открыть или в ней не обнаружена экспортируемая константа module_generate, возвращается значение NULL. В противном случае выделяется и возвращается объект module.

■ Функция module_close() закрывает совместно используемую библиотеку, соответствующую указанному модулю, и удаляет объект module.

В файле module.c определена также глобальная переменная module_dir. В ней записано имя каталога, в котором функция module_open() будет искать совместно используемые библиотеки.

 

11.2.3. Сервер

Файл server.c (листинг 11.4) представляет собой реализацию простейшего HTTP-сервера.

Листинг 11.4. ( server.c ) Реализация HTTP-сервера

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include "server.h"

/* HTTP-ответ и заголовок, возвращаемые в случае

   успешной обработки запроса. */

static char* ok_response =

 "HTTP/1.0 100 OK\n"

 "Content-type: text/html\n"

 "\n";

/* HTTP-ответ, заголовок и тело страницы на случай

   непонятного запроса. */

static char* bad_request_response =

 "HTTP/1.0 400 Bad Reguest\n"

 "Content-type: text/html\n"

 "\n"

 "\n"

 " \n"

 " 

Bad Request

\n"

 " 

This server did not understand your request.

\n"

 " \n"

 "\n";

/* HTTP-ответ, заголовок и шаблон страницы на случай,

   когда запрашиваемый документ не найден. */

static char* not_found_response_template =

 "HTTP/1.0 404 Not Found\n"

 "Content-type: text/html\n"

 "\n"

 "\n"

 " \n"

 " 

Not Found

\n"

 " 

The requested URL %s was not found on this server.

\n"

 " \n"

 "\n";

/* HTTP-ответ, заголовок к шаблон страницы на случай,

   когда запрашивается непонятный метод */

static char* bad_method_response_template =

 "HTTP/1.0 501 Method Not Implemented\n"

 "Content-type: text/html\n"

 "\n"

 "\n"

 " \n"

 " 

Method Not Implemented

\n"

 " 

The method %s is not implemented by this server.

\n"

 " \n"

 "\n";

/* Обработчик сигнала SIGCHLD, удаляющий завершившиеся

   дочерние процессы. */

static void clean_up_child_process(int signal_number) {

 int status;

 wait(&status);

}

/* Обработка HTTP-запроса "GET" к странице PAGE и

   запись результата в файл с дескриптором CONNECTION_FD. */

static void handle_get(int connection_fd, const char* page) {

 struct server_module* module = NULL;

 /* Убеждаемся, что имя страницы начинается с косой черты и

    не содержит других символов косой черты, так как

    подкаталоги не поддерживаются. */

 if (*page == '/' && strchr(page + 1, '/') == NULL) {

  char module_file_name[64];

  /* Имя страницы правильно. Формируем имя модуля, добавляя

     расширение ".so" к имени страницы. */

  snprintf(module_file_name, sizeof(module_file_name),

   "%s.so", page + 1);

  /* Попытка открытия модуля. */

  module = module_open(module_file_name);

 }

 if (module == NULL) {

  /* Имя страницы неправильно сформировано или не удалось

     открыть модуль с указанным именем. В любом случае

     возвращается HTTP-ответ "404. Not Found". */

  char response[1024];

  /* Формирование ответного сообщения. */

  snprintf(response, sizeof(response),

   not_found_response_template, page);

  /* Отправка его клиенту. */

  write(connection_fd, response, strlen(response));

 } else {

  /* Запрашиваемый модуль успешно загружен. */

  /* Выдача HTTP-ответа, обозначающего успешную обработку

     запроса, и HTTP-заголовка для HTML-страницы. */

  write(connection_fd, ok_response, strlen(ok_response));

  /* Вызов модуля, генерирующего HTML-код страницы и

     записывающего этот код в указанный файл. */

  (*module->generate_function)(connection_fd);

  /* Работа с модулем окончена. */

  module_close(module);

 }

}

/* Обработка клиентского запроса на подключение. */

static void handle_connection(int connection_fd) {

 char buffer[256];

 ssize_t bytes_read;

 /* Получение данных от клиента. */

 bytes_read =

  read(connection_fd, buffer, sizeof(buffer) — 1);

 if (bytes_read > 0) {

  char method[sizeof(buffer)];

  char url[sizeof(buffer)];

  char protocol[sizeof(buffer)];

  /* Часть данных успешно прочитана. Завершаем буфер

     нулевым символом, чтобы его можно было использовать

     в строковых операциях. */

  buffer[bytes_read] = '\0';

  /* Первая строка, посылаемая клиентом, -- это HTTP-запрос.

     В запросе указаны метод, запрашиваемая страница и

     версия протокола. */

  sscanf(buffer, "%s %s %s", method, url, protocol);

  /* В заголовке, стоящем после запроса, может находиться

     любая информация. В данной реализации HTTP-сервера

     эта информация не учитывается. Тем не менее необходимо

     прочитать все данные, посылаемые клиентом. Данные читаются

     до тех пор, пока не встретится конец заголовка,

     обозначаемый пустой строкой. В HTTP пустой строке

     соответствуют символы CR/LF. */

  while (strstr(buffer, " \r\n\r\n") == NULL)

   bytes_read = read(connection_fd, buffer, sizeof(buffer));

  /* Проверка правильности последней операции чтения.

     Если она не завершилась успешно, произошел разрыв

     соединения, поэтому завершаем работу. */

  if (bytes_read == -1) {

   close(connection_fd);

   return;

  }

  /* Проверка поля версии. Сервер понимает протокол HTTP

     версий 1.0 и 1.1. */

  if (strcmp(protocol, "HTTP/1.0") &&

   strcmp(protocol, "HTTP/1.1")) {

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

   write(connection_fd, bad_request_response,

    sizeof(bad_request_response));

  } else if (strcmp (method, "GET")) {

   /* Сервер реализует только метод GET, а клиент указал

      другой метод. */

   char response[1024];

   snprintf(response, sizeof(response),

    bad_method_response_template, method);

   write(connection_fd, response, strlen(response));

  } else

   /* Корректный запрос. Обрабатываем его. */

   handle_get(connection_fd, url);

 } else if (bytes_read == 0)

  /* Клиент разорвал соединение, не успев отправить данные.

     Ничего не предпринимаем */

  ;

 else

  /* Операция чтения завершилась ошибкой. */

  system_error("read");

}

void server_run(struct in_addr local_address, uint16_t port) {

 struct sockaddr_in socket_address;

 int rval;

 struct sigaction sigchld_action;

 int server_socket;

 /* Устанавливаем обработчик сигнала SIGCHLD, который будет

    удалять завершившееся дочерние процессы. */

 memset(&sigchld_action, 0, sizeof(sigchld_action));

 sigchld_action.sa_handler = &clean_up_child_process;

 sigaction(SIGCHLD, &sigchld_action, NULL);

 /* Создание TCP-сокета */

 server_socket = socket(PF_INET, SOCK_STREAM, 0);

 if (server_socket == -1) system_error("socket");

 /* Создание адресной структуры, определяющей адрес

    для приема запросов. */

 memset(&socket_address, 0, sizeof(socket_address));

 socket_address.sin_family = AF_INET;

 socket_address.sin_port = port;

 socket_address.sin_addr = local_address;

 /* Привязка сокета к этому адресу. */

 rval =

  bind(server_socket, &socket_address,

  sizeof(socket_address));

 if (rval != 0)

  system_error("bind");

 /* Перевод сокета в режим приема запросов. */

 rval = listen(server_socket, 10);

 if (rval != 0)

  system_error("listen");

 if (verbose) {

  /* В режиме развернутых сообщений отображаем адрес и порт,

     с которыми работает сервер. */

  socklen_t address_length;

  /* Нахождение адреса сокета. */

  address_length = sizeof(socket_address);

  rval =

   getsockname(server_socket, &socket_address, &address_length);

  assert(rval == 0);

  /* Вывод сообщения. Номер порта должен быть преобразован

     из сетевого (обратного) порядка следования байтов

     в серверный (прямой). */

  printf("server listening on %s:%d\n",

   inet_ntoa(socket_address.sin_addr),

   (int)ntohs(socket_address.sin_port));

  }

  /* Бесконечный цикл обработки запросов. */

  while (1) {

   struct sockaddr_in remote_address;

   socklen_t address_length;

   int connection;

   pid_t child_pid;

  /* Прием запроса. Эта функция блокируется до тех пор, пока

     не поступит запрос. */

  address_length = sizeof(remote_address);

  connection = accept(server_socket, &remote_address,

   &address_length);

  if (connection == -1) {

   /* Функция завершилась неудачно. */

   if (errno == EINTR)

    /* Функция была прервана сигналом. Повторная попытка. */

    continue;

   else

    /* Что-то случилось. */

    system_error("accept");

  }

  /* Соединение установлено. Вывод сообщения, если сервер

     работает в режиме развернутых сообщений. */

  if (verbose) {

   socklen_t address_length;

   /* Получение адреса клиента. */

   address_length = sizeof(socket_address);

   rval =

    getpeername(connection, &socket_address, &address_length);

   assert(rval == 0);

   /* Вывод сообщения. */

   printf("connection accepted from %s\n",

   inet_ntoa(socket_address.sin_addr));

  }

  /* Создание дочернего процесса для обработки запроса. */

  child_pid = fork();

  if (child_pid == 0) {

   /* Это дочерний процесс. Потоки stdin и stdout ему не нужны,

      поэтому закрываем их. */

   close(STDIN_FILENO);

   close(STDOUT_FILENO);

   /* Дочерний процесс не должен работать с серверным сокетом,

      поэтому закрываем его дескриптор. */

   close(server_socket);

   /* Обработка запроса. */

   handle_connection(connection);

   /* Обработка завершена. Закрываем соединение и завершаем

      дочерний процесс. */

   close(connection);

   exit(0);

  } else if (child_pid > 0) {

   /* Это родительский процесс. Дескриптор клиентского сокета

      ему не нужен. Переход к приему следующего запроса. */

   close(connection);

  } else

   /* Вызов функции fork() завершился неудачей. */

   system_error("fork");

 }

}

В файле server.c определены следующие функции.

■ Функция server_run() является телом сервера. Она запускает сервер и начинает принимать запросы на подключение, не завершаясь до тех пор, пока не произойдет серьезная ошибка. Сервер создает потоковый TCP-сокет (см. раздел 5.5.3, "Серверы").

Первый аргумент функции server_run определяет локальный адрес, по которому принимаются запросы. У компьютера может быть несколько адресов, каждый из которых соответствует определённому сетевому интерфейсу. Данный аргумент ограничивает работу сервера конкретным интерфейсом или разрешает принимать запросы отовсюду, если равен INADDR_ANY.

Второй аргумент функции server_run() — это номер порта сервера. Если порт уже используется или является привилегированным, работа сервера завершится. Когда номер порта задан равным нулю. ОС Linux автоматически выберет неиспользуемый порт.

Для обработки каждого клиентского запроса сервер создает дочерний процесс с помощью функции fork() (см. раздел 3.2.2. "Функции fork() и exec()"), в то время как родительский процесс продолжает принимать новые запросы. Дочерний процесс вызывает функцию handle_connection(), после чего закрывает соединение и завершается.

■ Функция handle_connection() обрабатывает отдельный клиентский запрос, принимая в качестве аргумента дескриптор сокета. Функция читает данные из сокета и пытается интерпретировать их как HTTP-запрос на получение страницы.

Сервер обрабатывает только запросы протокола HTTP версий 1.0 и 1.1. Столкнувшись с иными протоколом или версией сервер возвращает HTTP-код 400 и сообщение bad_request_response. Сервер понимает только HTTP-метод GET. Если клиент запрашивает какой-то другой метод, сервер возвращает HTTP-код 501 и сообщение bad_method_response_template.

■ Если клиент послал правильно сформированный запрос GET, функция handle_connection() вызывает функцию handle_get(), которая обрабатывает запрос. Эта функция пытается загрузить серверный модуль, имя которого генерируется на основании имени запрашиваемой страницы. Например, когда клиент запрашивает страницу с именем "information", делается попытка загрузить модуль information.so. Если модуль не может быть загружен, функция handle_get() возвращает HTTP-код 404 и сообщение not_found_response_template.

В случае обращения к верной странице функция handle_get() возвращает клиенту HTTP-код 200, указывающий на успешную обработку запроса, и вызывает функцию module_generate(), содержащуюся в модуле. Последняя генерирует HTML-код Web-страницы и посылает его клиенту.

■ Функция server_run() регистрирует функцию clean_up_child_process() в качестве обработчика сигнала SIGCHLD. Обработчик просто очищает ресурсы завершившегося дочернего процесса (см. раздел 3.4.4. "Асинхронное удаление дочерних процессов").

 

11.2.4. Основная программа

В файле main.c (листинг 11.5) содержится функция main() сервера. Она отвечает за анализ аргументов командной строки и обнаружение ошибок в них, а также за конфигурирование и запуск сервера.

Листинг 11.5. ( main.c ) Главная серверная функция, выполняющая анализ аргументов командной строки

#include

#include

#include

#include

#include

#include

#include

#include

#include "server.h"

/* Описание длинных опций для функции getopt_long(). */

static const struct option long_options[] = {

 { "address",    1, NULL, 'a' },

 { "help",       0, NULL, 'h' },

 { "module-dir", 1, NULL, 'm' },

 { "port",       1, NULL, 'p' },

 { "verbose",    0, NULL, 'v' },

};

/* Описание коротких опций для функции getopt_long(). */

static const char* const short_options = "a:hm:p:v";

/* Сообщение о том, как правильно использовать программу. */

static const char* const usage_template =

 "Usage: %s { options }\n"

 " -a, --address ADDR   Bind to local address (by default, bind\n"

 "                      to all local addresses).\n"

 " -h, --help           Print this information.\n"

 " -m, --module-dir DIR Load modules from specified directory\n"

 "                      (by default, use executable directory).\n"

 " -p, --port PORT      Bind to specified port.\n"

 " -v, --verbose        Print verbose messages.\n";

/* Вывод сообщения о правильном использовании программы

   и завершение работы. Если аргумент IS_ERROR не равен нулю,

   сообщение записывается в поток stderr и возвращается

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

   поток stdout и возвращается обычный нулевой код. */

static void print_usage(int is_error) {

 fprintf(is_error ? stderr : stdout, usage_template,

  program_name);

 exit(is_error ? 1 : 0);

}

int main(int argc, char* const argv[]) {

 struct in_addr local_address;

 uint16_t port;

 int next_option;

 /* Сохранение имени программы для отображения в сообщениях

    об ошибке. */

 program_name = argv[0];

 /* Назначение стандартных установок. По умолчанию сервер

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

    назначается неиспользуемый порт. */

 local_address.s_addr = INADDR_ANY;

 port = 0;

 /* He отображать развернутые сообщения. */

 verbose = 0;

 /* Загружать модули из каталога, в котором содержится

    исполняемый файл. */

 module_dir = get_self_executable_directory();

 assert(module_dir != NULL);

 /* Анализ опций. */

 do {

  next_option =

   getopt_long(argc, argv, short_options,

  long_options, NULL);

  switch (next_option) {

  case 'a':

   /* Пользователь ввел -a или --address. */

   {

    struct hostent* local_host_name;

    /* Поиск заданного адреса. */

    local_host_name = gethostbyname(optarg);

    if (local_host_name == NULL ||

     local_host_name->h_length == 0)

     /* He удалось распознать имя. */

     error(optarg, "invalid host name");

    else

     /* Введено правильное имя */

     local_address.s_addr =

      *((int*)(local_host_name->h_addr_list[0]));

   }

   break;

  case 'h':

   /* Пользователь ввёл -h или --help. */

   print_usage(0);

  case 'm':

   /* Пользователь ввел -m или --module-dir. */

   {

    struct stat dir_info;

    /* Проверка существования каталога */

    if (access(optarg, F_OK) != 0)

     error(optarg, "module directory does not exist");

    /* Проверка доступности каталога. */

    if (access(optarg, R_OK | X_OK) != 0)

     error(optarg, "module directory is not accessible");

    /* Проверка того, что это каталог. */

    if (stat(optarg, &dir_info) != 0 || !S_ISDIR(dir_info.st_mode))

     error(optarg, "not a directory");

    /* Все правильно. */

    module_dir = strdup(optarg);

   }

   break;

  case 'p':

   /* Пользователь ввел -p или --port. */

   {

    long value;

    char* end;

    value = strtol(optarg, &end, 10);

    if (*end != '\0')

     /* В номере порта указаны не только цифры. */

     print_usage(1);

    /* Преобразуем номер порта в число с сетевым (обратным)

       порядком следования байтов. */

    port = (uint16_t)htons(value);

   }

   break;

  case 'v':

   /* Пользователь ввел -v или --verbose. */

   verbose = 1;

   break;

  case '?':

   /* Пользователь ввел непонятную опцию. */

   print_usage(1);

  case -1:

   /* Обработка опций завершена. */

   break;

  default:

   abort();

  }

 } while (next_option != -1);

 /* Программа не принимает никаких дополнительных аргументов.

    Если они есть, выдается сообщение об ошибке. */

 if (optind != argc)

  print_usage(1);

 /* Отображение имени каталога, если программа работает в режиме

    развернутых сообщений. */

 if (verbose)

  printf("modules will be loaded from %s\n", module_dir);

 /* Запуск сервера. */

 server_run(local_address, port);

 return 0;

}

Файл main.c содержит следующие функции.

■ Функция getopt_long() (см. раздел 21.3, "Функция getopt_long()") вызывается для анализа опций командной строки. Опции могут задаваться в двух форматах: длинном и коротком. Описание длинных опций приведено в массиве long_options, а коротких — в массиве short_options.

По умолчанию серверный порт имеет номер 0, а локальный адрес задан в виде константы INADDR_ANY. Эти установки можно переопределить с помощью опций --port (-p) и --address (-a) соответственно. Если пользователь ввел адрес, вызывается библиотечная функция gethostbyname(), преобразующая его в числовой Internet-адрес.

По умолчанию серверные модули загружаются из каталога, где находится исполняемый файл. Этот каталог определяется с помощью функции get_self_executable_directory(). Данную установку можно переопределить с помощью опции --module (-m). В таком случае проверяется, является ли указанный каталог доступным.

По умолчанию развернутые сообщения не отображаются, если не указать опцию --verbose (-v).

■ Если пользователь ввел опцию --help (-h) или указал неправильную опцию, вызывается функция print_usage(), которая отображает сообщение о правильном использовании программы и завершает работу.

 

11.3. Модули

 

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

 

11.3.1. Отображение текущего времени

Модуль time.so (исходный текст приведен в листинге 11.6) генерирует простую страницу, где отображается текущее время на сервере. В функции module_generate() вызывается функция gettimeofday(), возвращающая значение текущего времени (см. раздел 8.7, "Функция gettimeofday(): системные часы"), после чего функции localtime() и strftime() преобразуют это значение в текстовый формат. Полученная строка встраивается в шаблон HTML-страницы page_template.

Листинг 11.6. ( time.c ) серверный модуль, отображающий текущее время

#include

#include

#include

#include

#include "server.h"

/* шаблон HTML-страницы, генерируемой данным модулем. */

static char* page_template =

 "\n"

 " \n"

 "  \n"

 " \n"

 " \n"

 " 

The current time is %s

\n"

 " \n"

 "\n";

void module_generate(int fd) {

 struct timeval tv;

 struct tm* ptm;

 char time_string[40];

 FILE* fp;

 /* Определение времени суток и заполнение структуры типа tm. */

 gettimeofday(&tv, NULL);

 ptm = localtime(&tv.tv_sec);

 /* Получение строкового представления времени с точностью

    до секунды. */

 strftime(time_string, sizeof(time_string), "%H:%M:%S", ptm);

 /* Создание файлового потока, соответствующего дескриптору

    клиентского сокета. */

 fp = fdopen(fd, "w");

 assert(fp != NULL);

 /* Запись HTML-страницы. */

 fprintf(fp, page_template, time_string);

 /* Очистка буфера потока */

 fflush(fp);

}

Для удобства в этом модуле используются стандартные библиотечные функции ввода-вывода. Функция fdopen() возвращает указатель потока (FILE*), соответствующий дескриптору клиентского сокета (подробнее об этом рассказывается в приложении Б, "Низкоуровневый ввод-вывод"). Для отправки страницы клиенту вызывается обычная функция fprintf(), а функция fflush() предотвращает потерю данных в случае закрытия сокета.

HTML-страница, возвращаемая модулем time.so, содержит в заголовке тэг , который служит клиенту указанием перезагружать страницу каждые 5 секунд. Благодаря этому клиент всегда будет знать точное время.

 

11.3.2. Отображение версии Linux

Модуль issue.so (исходный текст приведен в листинге 11.7) выводит информацию о дистрибутиве Linux, с которым работает сервер. Традиционно эта информация хранится в файле /etc/issue. Модель посылает клиенту Web-страницу с содержимым файла, заключенным в тэге

.                             

Листинг 11.7. ( issue.c ) Серверный модуль, отображающий информацию о дистрибутиве Linux

#include

#include

#include

#include

#include

#include

#include "server.h"

/* HTML-код начала генерируемой страницы. */

static char* page_start =

 "\n"

 " \n"

 " 

\n";                             

/* HTML-код конца генерируемой страницы. */

static char* page_end =

 " 

\n"

 " \n"

 "\n";

/* HTML-код страницы, сообщающей о том, что

   при открытии файла /etc/issue произошла ошибка. */

static char* error_page =

 "\n"

 " \n"

 " 

Error: Could not open /etc/issue.

\n"

 " \n"

 "\n";

/* Сообщение об ошибке. */

static char* error_message =

 "Error reading /etc/issue.";

void module_generate(int fd) {

 int input_fd;

 struct stat file_info;

 int rval;

 /* Открытие файла /etc/issue */

 input_fd = open("/etc/issue", O_RDONLY);

 if (input_fd == -1)

  system_error("open");

 /* Получение информации о файле. */

 rval = fstat(input_fd, &file_info);

 if (rval == -1)

  /* не удалось открыть файл или прочитать данные из него. */

  write(fd, error_page, strlen(error_page));

 else {

  int rval;

  off_t offset = 0;

  /* Запись начала страницы */

  write(fd, page_start, strlen(page_start));

  /* Копирование данных из файла /etc/issue

     в клиентский сокет. */

  rval = sendfile(fd, input_fd, &offset, file_info.st_size);

  if (rval == -1)

   /* При отправке файла /etc/issue произошла ошибка.

      Выводим соответствующее сообщение. */

   write(fd, error_message, strlen(error_message));

  /* Конец страницы. */

  write(fd, page_end, strlen(page_end));

 }

 close(input_fd);

}

Сначала модуль пытается открыть файл /etc/issue. Если это не удалось, клиенту возвращается сообщение об ошибке. В противном случае посылается начальный код HTML-страницы, содержащийся в переменной page_start, затем — содержимое файла /etc/issue (это делается с помощью функции sendfile(), о которой рассказывалось в разделе 8.12. "Функция sendfile(): быстрая передача данных") и, наконец конечный код HTML-страницы, содержащийся в переменной page_end.

Этот модуль можно легко настроить на отправку любого другого файла. Если файл содержит HTML-страницу, переменные page_start и page_end будут не нужны.

 

11.3.3. Отображение объема свободного дискового пространства

Модуль diskfree.so (исходный текст приведен в листинге 11.8) генерирует страницу с информацией о свободном дисковом пространстве в файловых системах, смонтированных на серверном компьютере. Эта информация берется из выходных данных команды df -h. Как и в модуле issue.so, выходные данные заключаются в тэги

.                             

Листинг 11.8. ( diskfree.c ) Серверный модуль, отображающий информацию о свободном дисковом пространстве

#include

#include

#include

#include

#include "server.h"

/* HTML-код начала генерируемой страницы. */

static char* page_start =

 "\n"

 " \n"

 " 

\n";                             

/* HTML-код конца генерируемой страницы. */

static char* page_end =

 "  

\n"

 " \n"

 "\n";

void module_generate(int fd) {

 pid_t child_pid;

 int rval;

 /* Запись начала страницы. */

 write(fd, page_start, strlen(page_start));

 /* Создание дочернего процесса. */

 child_pid = fork();

 if (child_pid == 0) {

  /* Это дочерний процесс. */

  /* Подготовка списка аргументов команды df. */

  char* argv[] = { "/bin/df, "-h", NULL };

  /* Дублирование потоков stdout и stderr для записи данных

     в клиентский сокет. */

  rval = dup2(fd, STDOUT_FILENO);

  if (rval == -1)

   system_error("dup2");

  rval = dup2(fd, STDERR_FILENO);

  if (rval == -1)

   system_error("dup2");

  /* Запуск команды df, отображающей объем свободного

     пространства в смонтированных файловых системах. */

  execv(argv[0], argv);

  /* Функция execv() возвращает управление в программу только

     при возникновении ошибки. */

  system_error("execv");

 } else if (child_pid > 0) {

  /* Это родительский процесс, дожидаемся завершения дочернего

     процесса. */

  rval = waitpid(child_pid, NULL, 0);

  if (rval == -1)

   system_error("waitpid");

 } else

  /* Вызов функции fork() завершился неудачей. */

  system_error("fork");

 /* запись конца страницы. */

 write(fd, page_end, strlen(page_end));

}

В то время как модуль issue.so посылает содержимое файла с помощью функции sendfile(), данный модуль должен вызвать внешнюю команду и перенаправить результаты ее работы клиенту. Для этого модуль придерживается такой последовательности действий.

1. Сначала с помощью функции fork() создается дочерний процесс (см. раздел 3.2.2. "Функции fork() и exec()").

2. Дочерний процесс копирует дескриптор сокета в дескрипторы STDOUT_FILENO и STDERR_FILENO, соответствующие стандартным потокам вывода и ошибок (см. раздел 2.1.4, "Стандартный ввод-вывод"). Это копирование осуществляется с помощью системного вызова dup2() (см. раздел 5.4 3. "Перенаправление стандартных потоков ввода, вывода и ошибок"). Все последующие данные, записываемые в эти потоки в рамках дочернего процесса, будут направляться в сокет.

3. Дочерний процесс с помощью функции execv() вызывает команду df -h.

4. Родительский процесс дожидается завершения дочернего процесса, вызывая функцию waitpid() (см. раздел 5.4 2. "Системные вызовы wait()").

Этот модуль можно легко настроить на вызов другой системной команды.

 

11.3.4. Статистика выполняющихся процессов

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

Листинг 11.9. ( processes.c ) Серверный модуль, отображающий таблицу процессов

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include "server.h"

/* Эта функция записывает в аргументы UID и GID

   идентификаторы пользователя и группы, которым

   принадлежит процесс с указанным идентификатором,

   в случае успешного завершения возвращается нуль,

   иначе -- ненулевое значение. */

static int get_uid_gid(pid_t pid, uid_t* uid, gid_t* gid) {

 char dir_name[64];

 struct stat dir_info;

 int rval;

 /* Формирование имени каталога процесса

    в файловой системе /proc. */

 snprintf(dir_name, sizeof(dir_name), "/proc/%d", (int)pid);

 /* Получение информации о каталоге. */

 rval = stat(dir_name, &dir_info);

 if (rval != 0)

  /* Каталог не найден. Возможно, процесс больше

     не существует. */

  return 1;

 /* Убеждаемся в том, что это действительно каталог. */

 assert(S_ISDIR(dir_info.st_mode));

 /* Определяем интересующие нас идентификаторы. */

 *uid = dir_info.st_uid;

 *gid = dir_info.st_gid;

 return 0;

}

/* Эта функция находит имя пользователя,

   соответствующее заданному идентификатору.

   Возвращаемый буфер должен быть удален

   в вызывающей функции. */

static char* get_user_name(uid_t uid) {

 struct passwd* entry;

 entry = getpwuid(uid);

 if (entry == NULL)

  system_error("getpwuid");

 return xstrdup(entry->pw_name);

}

/* Эта функция находит имя группы, соответствующее

   заданному идентификатору, возвращаемый буфер

   должен быть удален в вызывающей функции. */

static char* get_group_name(gid_t gid) {

 struct group* entry;

 entry = getgrgid(gid);

 if (entry == NULL)

  system_error("getgrgid");

 return xstrdup(entry->gr_name);

}

/* Эта функция находит имя программы, которую выполняет

   процесс с заданным идентификатором. Возвращаемый буфер

   должен быть удален в вызывающей функции. */

static char* get_program_name(pid_t pid) {

 char file_name[64];

 char status_info[256];

 int fd;

 int rval;

 char* open_paren;

 char* close_paren;

 char* result;

 /* Генерируем имя файла "stat", находящегося в каталоге

    данного процесса в файловой системе /proc,

    и открываем этот файл. */

 snprintf(file_name, sizeof(file_name), "/proc/%d/stat",

  (int)pid);

 fd = open(file_name, O_RDONLY);

 if (fd == 1)

  /* Файл не удалось открыть. Возможно, процесс

     больше не существует. */

  return NULL;

 /* Чтение содержимого файла

 rval = read(fd, status_info, sizeof(status_info) — 1);

 close(fd);

 if (rval <= 0)

  /* По какой-то причине файл не удалось прочитать, завершаем

     работу. */

  return NULL;

 /* Завершаем прочитанный текст нулевым символом. */

 status_info[rval] = '\0';

 /* Имя программы -- это второй элемент файла, заключенный в

    круглые скобки. Находим местоположение скобок. */

 open_paren = strchr(status_info, '(');

 close_paren = strchr(status_info, ')');

 if (open_paren == NULL ||

  close_paren == NULL || close_paren < open_paren)

  /* He удалось найти скобки, завершаем работу. */

  return NULL;

 /* Выделение памяти для результирующей строки */

 result = (char*)xmalloc(close_paren — open_paren);

 /* Копирование имени программы в буфер. */

 strncpy(result, open_paren + 1, close_paren - open_paren — 1);

 /* Функция strncpy() не завершает строку нулевым символом,

    приходится это делать самостоятельно. */

 result[close_paren - open_paren - 1] = '\0';

 /* Конец работы. */

 return result;

}

/* Эта функция определяет размер (в килобайтах) резидентной

   части процесса с заданным идентификатором.

   В случае ошибки возвращается -1. */

static int get_rss(pid_t pid) {

 char file_name[64];

 int fd;

 char mem_info[128];

 int rval;

 int rss;

 /* Генерируем имя файла "statm", находящегося в каталоге

    данного процесса в файловой системе proc. */

 snprintf(file_name, sizeof(file_name), "/proc/%d/statm",

  (int)pid);

 /* Открытие файла. */

 fd = open(file_name, O_RDONLY);

 if (fd == -1)

  /* Файл не удалось открыть. Возможно, процесс больше не

     существует. */

  return -1;

 /* Чтение содержимого файла. */

 rval = read(fd, mem_info, sizeof(mem_info) — 1);

 close(fd);

 if (rval <= 0)

  /* Файл не удалось прочитать, завершаем работу. */

  return -1;

 /* Завершаем прочитанный текст нулевым символом. */

 mem_infо[rval] = '\0';

 /* Определяем размер резидентной части процесса. Это второй

    элемент файла. */

 rval = sscanf(mem_info, "%*d %d", &rss);

 if (rval != 1)

  /* Содержимое файла statm отформатировано непонятным

     образом. */

  return -1;

 /* Значения в файле statm приведены в единицах, кратных размеру

    системной страницы. Преобразуем в килобайты. */

 return rss * getpagesize() / 1024;

}

/* Эта функция генерирует строку таблицы для процесса

   с заданным идентификатором. Возвращаемый буфер должен

   удаляться в вызывающей функции, в случае ошибки

   возвращается NULL. */

static char* format_process_info(pid_t pid) {

 int rval;

 uid_t uid;

 gid_t gid;

 char* user_name;

 char* group_name;

 int rss;

 char* program_name;

 size_t result_length;

 char* result;

 /* Определяем идентификаторы пользователя и группы, которым

    принадлежит процесс. */

 rval = get_uid_gid(pid, &uid, &gid);

 if (rval != 0)

  return NULL;

 /* Определяем размер резидентной части процесса. */

 rss = get_rss(pid);

 if (rss == -1)

  return NULL;

 /* Определяем имя исполняемого файла процесса. */

 program_name = get_program_name(pid);

 if (program_name == NULL)

  return NULL;

 /* Преобразуем идентификаторы пользователя и группы в имена. */

 user_name = get_user_name(uid);

 group_name = get_group_name(gid);

 /* Вычисляем длину строки, в которую будет помещен результат,

    и выделяем для нее буфер. */

 result_length =

  strlen(program_name) + strlen(user_name) +

  strlen(group_name) + 128;

 result = (char*)xmalloc(result_length);

 /* Форматирование результата. */

 snprintf(result, result_length,

  "%d%s%s"

  "%s%d\n",

  (int)pid, program_name, user_name, group_name, rss);

 /* Очистка памяти. */

 free(program_name);

 free(user_name);

 free(group_name);

 /* Конец работы. */

 return result;

}

/* HTML-код начала страницы, содержащей таблицу процессов. */

static char* page_start =

 "\n"

 " \n"

 " 

\n"

 "  

\n"

 "   

\n"

 "    

\n"

 "    

\n"

 "    

\n"

 "    

\n"

 "    

\n"

 "   

\n"

 "  

\n"

 "  

\n";

/* HTML-код конца страницы, содержащей таблицу процессов. */

static char* page_end =

 "  

\n"

 " 

PID Program User Group RSS (KB)
\n"

 " \n"

 "\n";

void module_generate(int fd) {

 size_t i;

 DIR* proc_listing;

 /* Создание массива iovec. В этот массив помещается выходная

    информации, причем массив может увеличиваться динамически. */

 /* Число используемых элементов массива */

 size_t vec_length = 0;

 /* выделенный размер массива */

 size_t vec_size = 16;

 /* Массив элементов iovec. */

 struct iovec* vec =

  (struct iovec*)xmalloc(vec_size *

  sizeof(struct iovec));

 /* Сначала в массив записывается HTML-код начала страницы. */

 vec[vec_length].iov_base = page_start;

 vec[vec_length].iov_len = strlen(page_start);

 ++vec_length;

 /* Получаем список каталогов в файловой системе /proc. */

 proc_listing = opendir("/proc");

 if (proc_listing == NULL)

  system_error("opendir");

 /* Просматриваем список каталогов. */

 while (1) {

  struct dirent* proc_entry;

  const char* name;

  pid_t pid;

  char* process_info;

  /* Переходим к очередному элементу списка. */

  proc_entry = readdir(proc_listing);

  if (proc_entry == NULL)

   /* Достигнут конец списка. */

   break;

  /* Если имя каталога не состоит из одних цифр, то это не

     каталог процесса; пропускаем его. */

  name = proc_entry->d_name;

  if (strspn(name, "0123456789") != strlen(name))

   continue;

  /* Именем каталога является идентификатор процесса. */

  pid = (pid_t)atoi(name);

  /* генерируем HTML-код для строки таблицы, содержащей

     описание данного процесса. */

  process_info = format_process_info(pid);

  if (process_info == NULL)

   /* Произошла какая-то ошибка. Возможно, процесс уже

      завершился. Создаем строку-заглушку. */

   process_info =

    "ERROR";

  /* Убеждаемся в том, что в массиве iovec достаточно места

     для записи буфера (один элемент будет добавлен в массив

     по окончании обработки списка процессов). Если места

     не хватает, удваиваем размер массива. */

  if (vec_length == vec_size - 1) {

   vec_size *= 2;

   vec = xrealloc(vec, vec_size - sizeof(struct iovec));

  }

  /* Сохраняем в массиве информацию о процессе. */

  vec[vec_length].iov_base = process_info;

  vec[vec_length].iov_len = strlen(process_info);

  ++vec_length;

 }

 /* Конец обработки списка каталогов */

 closedir(proc_listing);

 /* Добавляем HTML-код конца страницы. */

 vec[vec_length].iov_base = page_end;

 vec[vec_length].iov_len = strlen(page_end);

 ++vec_length;

 /* Передаем всю страницу клиенту. */

 writev(fd, vec, vec_length);

 /* Удаляем выделенные буферы. Первый и последний буферы

    являются статическими, поэтому не должны удаляться. */

 for (i = 1; i < vec_length - 1; ++i)

  free(vec[i].iov_base);

 /* Удаляем массив iovec. */

 free(vec);

}

Задача сбора информации о процессах и представления ее в виде HTML-таблицы разбивается на ряд более простых операций.

■ Функция get_uid_gid() возвращает идентификатор пользователя и группы, которым принадлежит процесс. Для этого вызывается функция stat() (описана в приложении Б, "Низкоуровневый ввод-вывод"), берущая информацию из каталога процесса в файловой системе /proc.

■ Функция get_user_name() возвращает имя пользователя, соответствующее заданному идентификатору. Она просто вызывает библиотечную функцию getpwuid(), которая обращается к файлу /etc/passwd и возвращает копию строки из него. Функция get_group_name() находит имя группы по заданному идентификатору. Она вызывает функцию getgrgid().

■ Функция gеt_program_name() возвращает имя программы, соответствующей заданному процессу. Эта информация извлекается из файла stat, находящегося в каталоге процесса в файловой системе /proc (см. раздел 7.2, "Каталоги процессов"). Мы поступаем так, а не проверяем символические ссылки exe или cmdline, поскольку последние недоступны, если серверный процесс не принадлежит тому же пользователю, что и проверяемый процесс.

■ Функция get_rss() определяет объем резидентной части процесса. Эта информация содержится во втором элементе файла statm (см. раздел 7.2.6, "Статистика использования процессом памяти"), находящегося в каталоге процесса в файловой системе /proc.

■ Функция format_process_info() генерирует набор HTML-тэгов для строки таблицы, представляющей заданный процесс. Здесь вызываются все вышеперечисленные функции.

■ Функция module_generate() генерирует HTML-страницу с таблицей процессов. Выводная информация включает начальный HTML-блок (переменная page_start), строки с информацией о процессах (создаются функцией format_process_info()) и конечный HTML-блок (переменная page_end).

Функция module_generate() определяет идентификаторы процессов, проверяя содержимое файловой системы /proc. Для получения и анализа списка каталогов вызываются функции opendir() и readdir() (описаны в приложении Б, "Низкоуровневый ввод-вывод''). Из данного списка отбираются элементы, имена которых состоят из одних цифр: это каталоги процессов.

Поскольку в таблице может содержаться достаточно большое число строк, последовательная запись их в сокет с помощью функции write() приведет к ненужному повышению трафика. Для оптимизации числа передаваемых пакетов используется функция writev() (описана в приложении Б, "Низкоуровневый ввод-вывод"). Для нее создается массив vec, состоящий из элементов типа iovec. Так как число процессов не известно заранее приходится начинать с маленького массива и увеличивать его по мере необходимости. В переменной vec_length содержится число используемых элементов массива vec, а в переменной vec_size — число выделенных элементов. Когда эти переменные становятся почти равными друг другу, размер массива удваивается с помощью функции xrealloc(). По окончании работы с массивом удаляются все адресуемые в нем строки, а также сам массив.

 

11.4. Работа с сервером

 

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

 

11.4.1. Файл Makefile

Вместо утилиты Autoconf мы воспользуемся простым файлом Makefile, совместимым с GNU-утилитой Make. Этот файл упростит компиляцию и компоновку сервера и его модулей. Содержимое файла показано в листинге 11.10.

Листинг 11.10. ( Makefile ) Файл конфигурации сервера

### Конфигурация. ##############################################

# Стандартные параметры компилятора языка С.

CFLAGS = -Wall -g

# Исходные файлы сервера.

SOURCES = server.c module.c common.c main.c

# Соответствующие объектные файлы.

OBJECTS = $(SOURCES:.c=.o)

# Совместно используемые библиотеки серверных модулей.

MODULES = diskfree.so issue.so processes.so time.so

### Правила. ####################################################

# Служебный целевой модуль.

.PHONY: all clean

# Стандартный целевой модуль: компиляция всех файлов.

all: server $(MODULES)

# Удаление всех компонентов.

clean:

    rm -f $(OBJECTS) $(MODULES) server

# Главная серверная программа, должна компоноваться с флагами

# -Wl,-export-dynamic, чтобы динамически загружаемые модули могли

# находить в программе символические константы. Подключается также

# библиотека libdl, в которой находятся функции динамической

# загрузки.

server: $(OBJECTS)

    $(CC) $(CFLAGS) Wl,-export-dynamic -о $@ $^ -ldl

# Все объектные файлы сервера зависят от файла server.h.

# Используем стандартное правило создания объектных файлов из

# исходных файлов.

$(OBJECTS): server.h

# Правило создания совместно используемых библиотек из

# соответствующих исходных файлов, компилируем с флагом -fPIC и

# генерируем совместно используемый объектный файл.

$(MODULES): \

%.so: %.c server.h

    $(CC) $(CFLAGS) -fPIC -shared -o $@ $<

В файле Makefile есть следующие целевые модули.

■ Модуль all (используется по умолчанию при вызове файла Makefile без аргументов, так как стоит первым) содержит исполняемый файл server и все серверные модули. Последние перечислены в переменной MODULES.

■ Модуль clean предназначен для удаления всех скомпилированных компонентов.

■ Модуль server подключает к проекту исполняемый файл сервера. Компилируются и компонуются исходные файлы, перечисленные в переменной SOURCES.

■ Последнее правило представляет собой шаблон компиляции совместно используемых файлов серверных модулей.

Обратите внимание на то, что исходные файлы серверных модулей компилируются с флагом -fPIC, так как они включаются в совместно используемые библиотеки (см. раздел 2.3.2, "Совместно используемые библиотеки").

Исполняемый файл server компонуется с флагом -Wl,-export-dynamic. Благодаря этому файл будет экспортировать свои символические константы, что позволит динамически загружаемым модулям ссылаться на функции, находящиеся в файле common.c.

 

11.4.2. Создание сервера

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

% make

cc -Wall -g -с -o server.о server.с

cc -Wall -g -с -o module.о module.с

cc -Wall -g -с -o common.о common.с

cc -Wall -g -с -o main.o main.c

cc -Wall -g -Wl,-export-dynamic -o server server.о module.о

 common.о main.o -ldl

cc -Wall -g -fPIC -shared -o diskfree.so diskfree.c

cc -Wall -g -fPIC -shared -o issue.so issue.с

cc -Wall -g -fPIC -shared -o processes.so processes.с

cc -Wall -g -fPIC -shared -o time.so time.с

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

% ls -l server *.so

-rwxr-xr-x 1 samuel samuel 25769 Mar 11 01:15 diskfree.so

-rwxr-xr-x 1 samuel samuel 31184 Mar 11 01:15 issue.so

-rwxr-xr-x 1 samuel samuel 41579 Mar 11 01:15 processes.so

-rwxr-xr-x 1 samuel samuel 71758 Mar 11 01:15 server

-rwxr-xr-x 1 samuel samuel 13980 Mar 11 01:15 time.so

 

11.4.3. Запуск сервера

Для запуска сервера достаточно ввести в командной строке имя server. Если не задать номер порта с помощью опции --port (-p). ОС Linux самостоятельно выберет порт. При указании опции --verbose (-v) сервер покажет, какой порт ему назначен.

Если не назначить серверу адрес с помощью опции --address (-а), сервер будет принимать запросы по всем имеющимся адресам. Для подключенного к сети компьютера это означает, что любой пользователь сети, зная номер порта сервера и имя страницы, сможет обратиться к серверу. Из соображений безопасности рекомендуем указывать адрес localhost, пока вы не убедитесь в правильной работе сервера. В этом случае сервер будет связан с локальным сетевым устройством (обозначается как lo) и к нему смогут обращаться только программы, работающие на том же самом компьютере.

% ./server --address localhost --port 4000

Теперь сервер работает. Откройте окно броузера и попытайтесь обратиться к серверу по номеру порта. Запросите страницу, имя которой совпадает с именем модуля. Вот как, например, вызывается модуль diskfree.so:

http://localhost:4000/diskfree

Вместо 4000 можно указать любой другой номер порта, который был выбран. Чтобы завершить работу сервера, нажмите .

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

httр://host.domain.com:4000/diskfree

Если задать опцию --verbose (-v), сервер при запуске отобразит свою конфигурационную информацию, а затем будет показывать IP-адрес каждого подключающегося к нему клиента. Если подключаться через интерфейс localhost, клиентский адрес всегда будет равен 127.0.0.1.

С помощью опции --module-dir (-m) можно указать другой каталог размещения серверных модулей. По умолчанию они находятся там же, где и программа server.

Те, кто забыли или не знают синтаксис опций командной строки, могут вызвать программу server с опцией --help (-h):

% ./server --help

Usage: ./server [ options ]

 -a, --address ADDR   Bind to local address (by default, bind

                      to all local addresses).

 -h, --help           Print this information.

 -m, --module-dir DIR Load modules from specified directory

                      (by default, use executable directory).

 -p, --port PORT      Bind to specified port.

 -v, --verbose        Print verbose messages.

 

11.5. Вместо эпилога

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

Вероятнее всего, для программы потребуется создать man-страницу. Это первое место, куда пользователи обращаются за информацией о программе. Страницы интерактивной документации форматируются с помощью классической программы troff. Чтобы узнать формат troff-файлов, введите такую команду:

% man troff

Чтобы узнать, как ОС Linux ищет man-страницы, просмотрите справочную информацию о самой команде man:

% man man

Можно также подготовить документацию в формате GNU-системы Info. Для получения информацию об этой системе выполните команду

% info info

Для многих Linux-лрограмм имеется также документация в формате простого текста и HTML.

Удачного программирования!