Глава 7
Системные вызовы в UNIX
В настоящей главе мы рассмотрим самый низкий уровень взаимодействия с операционной системой UNIX системные вызовы. Они являются входами в ядро. Эти средства предоставляются операционной системой; все остальные средства построены на их основе.
Материал главы охватывает несколько важных областей. К ним относится прежде всего система ввода-вывода, являющаяся основной для функций типа
fopen
и
putс
. Речь пойдет также о файловой системе, в частности о каталогах и индексных дескрипторах. Затем мы обсудим процессы, т.е. как запускать задачи из программы, после чего поговорим о сигналах и прерываниях: что происходит, когда вы нажимаете клавишу
DELETE, и как такую ситуацию должным образом обработать в программе.
Как и в гл. 6, во многих примерах приводятся полезные программы, не вошедшие в седьмую версию. Даже если они не помогут вам непосредственно, знакомство с ними может навести вас на мысль о сходных средствах, которые целесообразно создать и для вашей системы.
Подробное описание системных вызовов вы можете найти во втором разделе справочного руководства по UNIX, так как здесь освещаются лишь основные вопросы.
7.1 Ввод-вывод низкого уровня
Низкий уровень ввода-вывода представляет собой непосредственный вход в операционную систему Ваши программы читают или пишут файлы порциями некоторого подходящего размера. Ядро буферизует данные порциями, которые подходят для периферийных устройств, и планирует операции для устройств так, чтобы их производительность была оптимальной для всех пользователей.
Дескрипторы файлов
Ввод и вывод обычно осуществляются посредством чтения или записи файлов, так как все периферийные устройства, и даже ваш терминал, представлены как файлы в файловой системе. Таким образом, единый интерфейс обеспечивает связь между программой и периферийными устройствами.
В самом общем случае, прежде чем читать или писать файл, необходимо сообщить системе о вашем намерении открыть файл. Если вы собираетесь писать в файл, может быть, его необходимо и создать. Система проверяет ваше право сделать это (существует ли файл и есть ли у вас разрешение на доступ к нему?), и при положительном результате проверки возвращает неотрицательное целое, называемое дескриптором файла. Всякий раз, когда нужно выполнить ввод-вывод через файл, для его идентификации вместо имени используется дескриптор файла. Вся информация об открытом файле поддерживается системой. Ваша программа ссылается на файл только через дескриптор файла. Указатель на
FILE
, как отмечалось в гл. 6, является ссылкой на структуру, которая наряду с прочим содержит дескриптор файла: макрокоманда
fileno(fp)
, определенная в
<stdio.h>
, возвращает дескриптор файла.
Для обеспечения удобства ввода и вывода на терминал предусмотрены специальные меры. Когда программа запускается из
shell
, она получает три открытых файла с дескрипторами 0, 1 и 2 стандартный входной поток, стандартный выходной поток и стандартный файл диагностики. Всем им по умолчанию поставлен в соответствие терминал, поэтому если программа читает только через дескриптор файла 0 и пишет через дескрипторы файлов 1 и 2, она может выполнять ввод-вывод без открывания файлов. Если же программа открывает любые другие файлы, они будут иметь дескрипторы 3, 4 и т.д.
При переключении ввода-вывода на файлы или программные каналы (к ним или от них)
shell
изменяет назначение терминала по умолчанию для дескрипторов файлов 0 и 1. Обычно дескриптор файла 2 закрепляется за терминалом, так что сообщения об ошибках могут поступать на него. Использование символики
shell
, такой, как
2>filename
и
2>&1
, вызовет переназначение файла, присвоенного по умолчанию, но при этом присвоение файлов меняется в
shell
, а не программой. (Программа сама может переназначить их впоследствии, если потребуется, что, правда, бывает редко.)
Файловый ввод-вывод:
read
и
write
Весь ввод и вывод обеспечиваются двумя системными вызовами,
read
и
write
, которые доступны в Си с помощью функций с теми же именами. Для обеих первый аргумент это дескриптор файла, второй массив байтов, который служит источником данных или назначением, а третий число байтов, которые следует передать.
int fd, n, nread, nwritten;
char buf[SIZE];
nread = read(fd, buf, n);
nwritten = write(fd, buf, n);
Каждый вызов возвращает число переданных байтов. При чтении возвращенное число может быть меньше, чем запрошенное, поскольку для чтения оставлено менее
n
байт. (Когда файлу поставлен в соответствие терминал,
read
обычно читает до следующей строки, что составляет меньшую часть запрошенного.) Возвращаемое значение 0 подразумевает конец файла, а значение -1 обозначает некоторую ошибку. При записи возвращаемое значение есть число действительно записанных байтов; если оно не равно числу байтов, которое предполагается записать, возникает ошибка. Несмотря на то, что число байтов, которые следует читать или писать, не ограничено, наиболее часто используются два значения: 1, что соответствует одному символу за одно обращение ("не буферизовано"), и размер блока на диске, как правило,- 512 или 1024 байта (такое значение имеет
BUFSIZ
в
<stdio.h>
). Для иллюстрации изложенного здесь приведена программа копирования входного потока в выходной. Так как входной и выходной потоки могут переключаться на любой файл или устройство, она действительно скопирует что-нибудь куда-либо: это "скелетная" реализация
cat
.
/* cat: minimal version */
#define SIZE 512 /* arbitrary */
main() {
char buf[SIZE];
int n;
while ((n = read(0, buf, sizeof buf)) > 0)
write(1, buf, n);
exit(0);
}
Если размер файла не кратен числу
SIZE
, некоторый вызов
read
вернет меньшее число байтов, которые должны быть записаны с помощью
write
; следующий затем вызов
read
вернет нуль.
Чтение и запись порциями, подходящими для диска, будут наиболее эффективными, но даже ввод-вывод по одному символу за раз осуществим для умеренных объемов буферизуются ядром. Дороже всего обходятся обращения к системе. Программа
ed
, например, использует однобайтовый способ, чтобы читать стандартный входной поток. Мы хронометрировали работу данной версии cat для файла в 54 000 байт при шести значениях
SIZE
: