В настоящей главе мы рассмотрим самый низкий уровень взаимодействия с операционной системой UNIX системные вызовы. Они являются входами в ядро. Эти средства предоставляются операционной системой; все остальные средства построены на их основе.
Материал главы охватывает несколько важных областей. К ним относится прежде всего система ввода-вывода, являющаяся основной для функций типа fopen и putс. Речь пойдет также о файловой системе, в частности о каталогах и индексных дескрипторах. Затем мы обсудим процессы, т.е. как запускать задачи из программы, после чего поговорим о сигналах и прерываниях: что происходит, когда вы нажимаете клавишу DELETE, и как такую ситуацию должным образом обработать в программе.
Как и в гл. 6, во многих примерах приводятся полезные программы, не вошедшие в седьмую версию. Даже если они не помогут вам непосредственно, знакомство с ними может навести вас на мысль о сходных средствах, которые целесообразно создать и для вашей системы.
Подробное описание системных вызовов вы можете найти во втором разделе справочного руководства по UNIX, так как здесь освещаются лишь основные вопросы.
7.1 Ввод-вывод низкого уровня
Низкий уровень ввода-вывода представляет собой непосредственный вход в операционную систему Ваши программы читают или пишут файлы порциями некоторого подходящего размера. Ядро буферизует данные порциями, которые подходят для периферийных устройств, и планирует операции для устройств так, чтобы их производительность была оптимальной для всех пользователей.
Дескрипторы файлов
Ввод и вывод обычно осуществляются посредством чтения или записи файлов, так как все периферийные устройства, и даже ваш терминал, представлены как файлы в файловой системе. Таким образом, единый интерфейс обеспечивает связь между программой и периферийными устройствами.
В самом общем случае, прежде чем читать или писать файл, необходимо сообщить системе о вашем намерении открыть файл. Если вы собираетесь писать в файл, может быть, его необходимо и создать. Система проверяет ваше право сделать это (существует ли файл и есть ли у вас разрешение на доступ к нему?), и при положительном результате проверки возвращает неотрицательное целое, называемое дескриптором файла. Всякий раз, когда нужно выполнить ввод-вывод через файл, для его идентификации вместо имени используется дескриптор файла. Вся информация об открытом файле поддерживается системой. Ваша программа ссылается на файл только через дескриптор файла. Указатель на FILE, как отмечалось в гл. 6, является ссылкой на структуру, которая наряду с прочим содержит дескриптор файла: макрокоманда fileno(fp), определенная в
Для обеспечения удобства ввода и вывода на терминал предусмотрены специальные меры. Когда программа запускается из 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 в
/* 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:
Время (пользователь+система, в сек.)
Размер | PDP-11/40 | VAX-11/750 |
1 | 271.0 | 188.8 |
10 | 29.9 | 19.3 |
100 | 3.8 | 2.6 |
512 | 1.3 | 1.0 |
1024 | 1.2 | 0.6 |
5120 | 1.0 | 0.6 |
Размер блока на диске для системы на PDP-11 составляет 512 байт и 1024 байта — для VAX.
Доступ нескольких процессоров к одному и тому же файлу в одно и то же время является совершенно законным: в самом деле, один процесс может писать, в то время как другой читает. Это обескураживает, если не входит в ваши планы, но иногда оказывается полезным. Даже несмотря на то, что при одном обращении к функции read возвращается 0, который сигнализирует о конце файла, следующий вызов read обнаружит наличие некоторого количества байтов, если еще данные пишутся в файл. Это соображение лежит в основе программы readslow, которая продолжает читать свой входной поток вне зависимости от того, достигла она конца файла или нет. Программа readslow удобна для наблюдения за работой программы.
$ slowprog >temp &
5213 идентификатор процесса
$ readslow
Иными словами, медленная программа выполняет вывод в файл; readslow, возможно, совместно с некоторыми другими программами "наблюдает", как накапливаются данные.
По составу readslow идентична cat, за исключением того, что она зацикливается, а не завершается, когда встречает конец входного потока. Программа readslow должна использовать ввод-вывод низкого уровня, так как стандартная библиотечная функция продолжает выдавать EOF после первого конца файла.
/* readslow: keep reading, waiting for more */
#define SIZE 512 /* arbitrary */
main() {
char buf[SIZE];
int n;
for (;;) {
while ((n = read(0, buf, sizeof buf)) > 0)
write(1, buf, n);
sleep(10);
}
}
Функция sleep заставляет программу остановиться на определенное число секунд (см. справочное руководство по sleep(3)). Мы не хотим, чтобы программа долго занималась поиском дополнительных данных, так как на это расходуется время центрального процессора. Таким образом, наша версия readslow копирует свой входной поток до конца файла, "спит" какое-то время, затем снова возобновляет работу. Если пока она была "в паузе", пришли еще данные, они будут прочитаны следующим read.
Упражнение 7.1
Добавьте readslow аргумент n , так что установленное по умолчанию время паузы может быть изменено на n секунд. Некоторые системы обеспечивают флаг -f ("навсегда") для tail , которая объединяет функции tail и readslow . Прокомментируйте этот вариант.
Упражнение 7.2
Что происходит с readslow , если читаемый файл обрывается? Как бы вы исправили ситуацию? Подсказка: читайте о fstat в разд. 7.3.
Создание файла: open , creat , close , unlink
Все стандартные файлы, кроме установленных по умолчанию, — входной, выходной и файл диагностики вы должны явно открыть для чтения или записи. Это можно сделать с помощью двух системных вызовов — открыть и создать.
Функция open весьма похожа на fopen из предыдущей главы, за исключением того, что вместо указателя файла она возвращает дескриптор файла, имеющий тип int.
char *name;
int fd, rwmode;
fd = open(name, rwmode);
Как и для fopen, аргумент name есть символьная строка, содержащая имя файла. Аргумент вида доступа, однако, другой: rwmode равен 0 для чтения, 1 — для записи, 2 — в том случае, когда нужно открыть файл для чтения и записи. При вызове open возвращается -1, если возникает какая-либо ошибка; иначе возвращается корректный дескриптор файла.
Попытка открыть несуществующий файл является ошибкой. Системный вызов creat позволяет создать новые файлы или переписать старые.
int perms;
fd = creat(name, perms);
Вызов creat возвращает дескриптор файла, если можно создать файл name, и -1 в противном случае. Если файл не существует, creat создает его с правами доступа, определяемыми аргументом perms. Существующий файл creat сокращает до нулевой длины, т.е. применение creat к уже существующему файлу не является ошибкой (права доступа при этом не изменяются). Безотносительно к правам доступа файл, к которому было обращение creat, открыт для записи.
Как известно из второй главы, с файлом связаны девять битов информации о защите, контролирующих разрешение на чтение, запись или выполнение, так что число из трех восьмеричных цифр удобно для спецификации этой информации. Например, 0755 дает разрешение владельцу файла читать, писать и выполнять его, а чтение и выполнение файла доступно любому пользователю. Не забывайте о первом нуле, который определяет восьмеричные числа в языке Си.
Иллюстрацией изложенного может служить упрощенная версия cp. Ее главный недостаток состоит в том, что она копирует только один файл и не разрешает использовать в качестве второго аргумента каталог. Кроме того, наша версия не сохраняет права доступа файла-источника; в дальнейшем мы покажем, как это исправить.
/* cp: minimal version */
#include
#define PERMS 0644 /* RW for owner, R for group, others */
char *progname;
main(argc, argv) /* cp: copy f1 to f2 */
int argc;
char *argv[];
{
int f1, f2, n;
char buf[BUFSIZ];
progname = argv[0];
if (argc != 3)
error("Usage: %s from to", progname);
if ((f1 = open(argv[1], 0)) == -1)
error("can't open %s", argv[1]);
if ((f2 = creat(argv[2], PERMS)) == -1)
error("can't create %s", argv[2]);
while ((n = read(f1, buf, BUFSIZ)) > 0)
if (write(f2, buf, n) != n)
error("write error", (char*)0);
exit(0);
}
error мы обсудим ниже.
Число файлов, которые одновременно могут быть открыты программой, ограничено (обычно порядка 20; см. NOFILE в
Обработка ошибок: errno
Обсуждаемые здесь системные вызовы, а по сути все системные вызовы, могут вызывать ошибки. Обычно они сигнализируют об ошибке, возвращая значение -1. Иногда полезно знать, какая именно ошибка произошла, поэтому системные вызовы, когда это приемлемо, оставляют номер ошибки во внешней целой переменной, называемой errno. (Значение различных номеров ошибок объясняется во введении к разд. 2 справочного руководства по UNIX.) С помощью errno ваша программа может определить, например, чем вызвана неудача при открытии файла — тем, что он не существует, или тем, что у вас нет разрешения на его чтение. Кроме того, есть массив символьных строк sys_errlist, индексируемый errno, который переводит число в строку, передающую смысл ошибки. Наша версия error использует эти структуры данных:
error(s1, s2) /* print error message and die */
char *s1, *s2;
{
extern int errno, sys_nerr;
extern char *sys_errlist[], *progname;
if (progname)
fprintf(stderr, "%s: ", progname);
fprintf(stderr, s1, s2);
if (errno > 0 && errno < sys_nerr)
fprintf (stderr, " (%s)", sys_errlist[errno]);
fprintf(stderr, "\n");
exit(1);
}
Errno первоначально равна нулю и всегда должна быть меньше, чем sys_herr. Она не становится нулевой вновь при нормальной работе, поэтому вы должны обнулять ее после каждой ошибки, если ваша программа будет продолжать выполняться. Сообщения об ошибках в нашей версии cp появляются следующим образом:
$ cp foo bar
cp: can't open foo (Нет такого файла или каталога)
$ date >foo; chmod 0 foo Создать нечитаемый файл
$ cp too bar
cp: can't open foo (В разрешении отказано)
$
Произвольный доступ: lseek
Файл ввода-вывода обычно последовательный: каждый read или write занимает место в файле непосредственно после использованного при предыдущем вызове. Однако при необходимости файл может быть прочитан или записан в произвольном порядке. Системный вызов lseek позволяет перемещаться по файлу, не осуществляя ни чтения, ни записи:
int fd, origin;
long offset, pos, lseek();
pos = lseek(fd, offset, origin);
Текущая позиция в файле с дескриптором fd перемещается к позиции offset, которая отсчитывается относительно места, определяемого origin. Последующие процессы чтения или записи будут начинаться с этой позиции. Origin может иметь значения 0, 1, 2, задавая тем самым начало отсчета значения offset — от начала, от текущей позиции или от конца файла соответственно.
Возвращаемое значение есть новая абсолютная позиция или -1 при ошибке. Например, при добавлении информации в файл нужно дойти до его конца, а затем выполнить запись:
lseek(fd, 0L, 2);
Чтобы вернуться обратно к началу ("перемотать"), необходимо вызвать
lseek(fd, 0L, 0);
Для определения текущей позиции следует выполнить
pos = lseek(fd, 0L, 1);
Обратите внимание на аргумент 0L: смещение есть длинное целое. ('l' в lseek означает 'long' — длинный, чтобы отличить его от системного вызова seek в шестой версии, где используются короткие целые.)
С помощью lseek можно обращаться с файлами как с большими массивами, однако при этом время доступа к ним возрастает. Например, следующая функция читает любое число байтов из любого места в файле:
get(fd, pos, buf, n) /* read n bytes from position pos */
int fd, n;
long pos;
char *buf;
{
if (lseek(fd, pos, 0) == -1) /* get to pos */
return -1;
return read(fd, buf, n);
}
Упражнение 7.3
Модифицируйте readslow так, чтобы обрабатывать имя файла в качестве аргумента, если оно присутствует. Добавьте -е :
$ readslow -е
заставляет readslow искать конец входного потока, прежде чем начать чтение. Каковы функции lseek при работе с программным каналом?
Упражнение 7.4
Перепишите efopen из гл. 6, чтобы вызвать error.
7.2 Файловая система: каталоги
Наша следующая тема — как ориентироваться в иерархии каталогов. При этом мы будем использовать не новые системные вызовы, а лишь несколько старых в новом контексте. В качестве примера приведем функцию spname, которая пытается справиться с неверно написанными именами файлов. Функция
n = spname(name, newname);
ищет файл с именем, "достаточно близким" к name. Если такое имя найдено, оно копируется в newname. Значение n, возвращаемое spname, равно -1, если ничего достаточно близкого не найдено, 0 — при точном совпадении и 1, если была сделана коррекция.
Spname является удобным дополнением к команде p: если вы пытаетесь печатать файл, но неверно написали имя, p спросит вас, не имели ли вы в виду что-либо другое:
$ p /urs/srx/ccmd/p/spnam.с Очень плохое имя
"/usr/src/cmd/p/spname.с"? y Предложенная коррекция принята
/* spname: возвращает верно написанное имя файла */
...
Пока мы пишем имя файла, spname пытается исправить каждую его составную часть, в которой несовпавшая буква была опущена, оказалась лишней, просто неверна или поменялась местами с другой буквой. Это удобное средство рассчитано на того, кто печатает не очень внимательно.
Прежде чем писать программу, уместно сделать короткий обзор структуры файловой системы. Каталог представляет собой файл, содержащий список имен файлов и указание, где они размещены. Место размещения определяется индексом в так называемой индексной таблице файлов. В записи индексной таблицы содержится вся информация о файле, кроме его имени. Строка каталога, таким образом, состоит из двух элементов — индекса файла и его имени. Точное описание можно найти в файле
$ cat /usr/include/sys/dir.h
#define DIRSIZ 14 /* максимальная длина имени файла */
struct direct /* структура строки каталога */
{
ino_t d_ino; /* номер индексного дескриптора */
char d_name[DIRSIZ]; /* имя файла */
};
$
"Тип" ino_t это typedef, описывающий индекс в индексной таблице. Он является коротким целым без знака (unsigned short) в версиях системы для PDP-11 и VAX и не должен включаться в программу, так как может быть иным на другой машине. Поэтому мы воспользуемся определением типа typedef. Полный набор "системных" типов находится в
Действия spname достаточно прямолинейны, хотя и требуют выполнения нескольких граничных условий. Предположим, что имя файла /d1/d2/f. Основная идея состоит в следующем: отделить первую компоненту (/), найти в каталоге имя, близкое к следующей компоненте (d1), затем найти имя, близкое к d2, и т.д. до тех пор, пока не будет достигнуто полное совпадение для каждой составной части. Если на какой-то стадии в каталоге не окажется подходящего кандидата, поиск прекратится.
Мы разбили процесс на три функции. Сама spname выделяет компоненты пути и составляет из них имя файла, наилучшим образом совпадающее с исходным. Функция mindist ищет в данном каталоге файл с именем, ближайшим к составленному функцией spname. Функция spdist вычисляет "расстояние" между двумя именами.
/* spname: return correctly spelled filename */
/*
* spname(oldname, newname) char *oldname, *newname;
* returns -1 if no reasonable match to oldname,
* 0 if exact match,
*1 if corrected.
* stores corrected name in newname.
*/
#include
#include
spname(oldname, newname)
char *oldname, *newname;
{
char *p, guess[DIRSIZ+1], best[DIRSIZ+1];
char *new = newname, *old = oldname;
for (;;) {
while (*old == '/') /* skip slashes */
*new++ = *old++;
*new = '\0';
if (*old == '\0') /* exact or corrected */
return strcmp(oldname, newname) != 0;
p = guess; /* copy next component into guess */
for (; *old != '/' && *old != '\0'; old++)
if (p < guess+DIRSIZ)
*p++ = *old;
*p = '\0';
if (mindist(newname, guess, best) >= 3)
return -1; /* hopeless */
for (p = best; *new = *p++; ) /* add to end */
new++; /* of newname */
}
}
mindist(dir, guess, best) /* search dir for guess */
char *dir, *guess, *best;
{
/* set best, return distance 0..3 */
int d, nd, fd;
struct {
ino_t ino;
char name[DIRSIZ+1]; /* 1 more than in dir.h */
} nbuf;
nbuf.name[DIRSIZ] = '\0'; /* +1 for terminal '\0' */
if (dir[0] == '\0') /* current directory */
dir = ".";
d = 3; /* minimum distance */
if ((fd = open(dir, 0)) == -1)
return d;
while (read(fd,(char *)&nbuf, sizeof(struct direct)) > 0)
if (nbuf.ino) {
nd = spdist(nbuf.name, guess);
if (nd <= d && nd != 3) {
strcpy(best, nbuf.name);
d = nd;
if (d == 0) /* exact match */
break;
}
}
close(fd);
return d;
Если имя каталога, данное mindist, пустое, отыскивается '.'. Функция mindist читает одну строку каталога за один раз. Отметим, что буфер для read представляет собой структуру, а не массив символов. Мы используем sizeof, чтобы вычислить число байтов и привести адрес к символьному указателю.
Если строка каталога в данный момент не используется (поскольку файл удален), то поле индекса в ней равно нулю и она пропускается. Проверка расстояния осуществляется как
if (nd <= d...)
а не как
if (nd < d...)
поэтому любой одиночный символ дает лучшее совпадение, чем имя '.', которое всегда является первой строкой в каталоге.
/* spdist: return distance between two names */ /*
* very rough spelling metric:
* 0 if the strings are identical
* 1 if two chars are transposed
* 2 if one char wrong, added or deleted
* 3 otherwise
*/
#define EQ(s,t) (strcmp(s,t) == 0)
spdist(s, t)
char *s, *t;
{
while (*s++ == *t)
if (*t++ == '\0')
return 0; /* exact match */
if (*--s) {
if (*t) {
if (s[1] && t[1] && *s == t[1] && *t == s[1] && EQ(s+2, t+2))
return 1; /* transposition */
if (EQ(s+1, t+1))
return 2; /* 1 char mismatch */
}
if (EQ(s+1, t))
return 2; /* extra character */
}
if (*t && EQ(s, t+1))
return 2; /* missing character */
return 3;
}
Поскольку у нас есть spname, несложно вставить функции по коррекции написания в p:
/* p: print input in chunks (version 4) */
#include
#define PAGESIZE 22
char *progname; /* program name for error message */
main(argc, argv)
int argc;
char *argv[];
{
FILE *fp, *efopen();
int i, pagesize = PAGESIZE;
char *p, *getenv(), buf[BUFSIZ];
progname = argv[0];
if ((p=getenv("PAGESIZE")) != NULL)
pagesize = atoi(p);
if (argc > 1 && argv[1][0] == '-') {
pagesize = atoi(&argv[1][1]);
argc--;
argv++;
}
if (argc == 1)
print(stdin, pagesize);
else
for (i = 1; i < argc; i++)
switch (spname(argv[i], buf)) {
case -1: /* no match possible */
fp = efopen(argv[i], "r");
break;
case 1: /* corrected */
fprintf(stderr, "\"%s\"? ", buf);
if (ttyin() == 'n')
break;
argv[i] = buf; /* fall through... */
case 0: /* exact match */
fp = efopen(argv[i], "r");
print(fp, pagesize);
fclose(fp);
}
exit(0);
}
Функции по коррекции написания не следует слепо применять к каждой программе, которая использует имена файлов. Они хорошо сочетаются с p, так как p — диалоговая программа, но подходят и для недиалоговых программ.
Упражнение 7.5
Насколько вы можете улучшить эвристику для выявления наилучшего совпадения в spname ? Например, неразумно рассматривать регулярный файл так, как если бы он был каталогом; текущая версия это допускает.
Упражнение: 7.6
Имя tx совпадает с каким-либо именем tc , которое оказывается последним в каталоге для любого одиночного символа с . Можете ли вы придумать лучшую меру расстояния? Реализуйте ее и посмотрите, насколько хорошо эта конструкция работает с реальными пользователями.
Упражнение 7.7
Работает ли p ощутимо быстрее, если чтение каталога выполняется большими порциями?
Упражнение 7.8
Модифицируйте spname , чтобы возвращать имя, которое является префиксом желаемого имени, если нельзя найти более точного совпадения. Как следует разрешить ситуацию, если несколько имен совпадают с префиксом?
Упражнение 7.9
Какую пользу могли бы извлечь другие программы из spname ? Сконструируйте отдельную программу, которая корректировала бы другие аргументы прежде, чем передать их другой программе, как в
$ fix prog filenames...
Можете написать версию cd , которая использует spname ? Как бы вы ее встроили?
7.3 Файловая система: индексные дескрипторы
Теперь мы обсудим системные вызовы применительно к файловой системе, в частности к такой информации о файлах, как размеры, даты изменений, права доступа и т.д. Эти системные вызовы позволяют получить полностью всю информацию, о которой упоминалось во второй главе.
Для начала разберемся в самом индексном дескрипторе. Часть индексного дескриптора описывается структурой stat, определенной в
struct stat /* структура, возвращаемая stat */
{
dev_t st_dev; /* устройство, содержащее файл */
ino_t st_ino; /* индекс */
short st_mod; /* биты режима */
short st_nlink; /* число связей файла */
short st_uid; /* пользовательский идентификатор
владельца */
short st_gid; /* идентификатор группы владельцев */
dev_t st_rdev; /* для специальных файлов */
off_t st_size; /* размер файла в символах */
time_t st_atime; /* время последнего чтения файла */
time_t st_mtime; /* время последней записи
или создания файла */
time_t st_ctime; /* время последнего изменения
индексного дескриптора или файла */
}
Большинство полей поясняются примечаниями. Типы вроде dev_t и ino_t определены в
#define S_IFMT 0170000 /* тип файла */
#define S_IFDIR 0040000 /* каталог */
#define S_IFCHR 0020000 /* байт-ориентированный */
#define S_IFBLK 0060000 /* блок-ориентированный */
#define S_IFREG 0100000 /* регулярный */
#define S_SUID 0004000 /* установка идентификатора пользователя при
выполнении */
#define S_ISGID 0002000 /* установка идентификатора группы
при выполнении */
#define S_ISVTX 0001000 /* сохранить выгруженный текст даже после
использования */
#define S_IREAD 0000400 /* разрешение читать, владелец */
#define S_IWRITE 0000200 /* разрешение писать, владелец */
#define S_IEXEC 0000100 /* разрешение на выполнение/поиск, владелец */
Индексный дескриптор для файла доступен двум системным вызовам stat и fstat. При вызове stat параметром является имя файла, а результатом — информация из индексного дескриптора для этого файла (или — 1 при наличии ошибки). Fstat выполняет те же функции в отношении дескриптора открытого файла (не в отношении указателя на FILE). Иными словами,
char *name;
int fd;
struct stat stbuf;
stat(name, &stbuf);
fstat(fd, &stbuf);
заполняет структуру stbuf информацией из индексного дескриптора для имени файла или дескриптора файла fd.
Зная все это, мы можем приступить к написанию некоторой полезной программы. Начнем с версии checkmail — программы на Си, которая следит за содержимым вашего почтового ящика. Если файл увеличивается, checkmail выдает сообщение: "У вас есть корреспонденция" и включает звонок. (При уменьшении файла, видимо, из-за того, что вы успели прочитать и сбросить некоторую почтовую корреспонденцию, сообщение не требуется.) Для первого шага вы сделали вполне достаточно, а когда ваша программа заработает, вы станете знатоком.
/* checkmail: watch user's mailbox */
#include
#include
#include
char *progname;
char *maildir = "/usr/spool/mail"; /* system dependent */
main(argc, argv)
int argc;
char *argv[];
{
struct stat buf;
char *name, *getlogin();
int lastsize = 0;
progname = argv[0];
if ((name = getlogin()) == NULL)
error("can't get login name", (char*)0);
if (chdir(maildir) == -1)
error("can't cd to %s", maildir);
for (;;) {
if (stat(name, &buf) == -1) /* no mailbox */
buf.st_size = 0;
if (buf.st_size > lastsize)
fprintf(stderr, "\nYou have mail\007\n");
lastsize = buf.st_size;
sleep(60);
}
}
Функция getlogin(3) возвращает ваше регистрационное имя или NULL, если это невозможно, checkmail переходит к почтовому каталогу с помощью системного вызова chdir, так что последующие вызовы stat не должны будут "добираться" до почтового каталога через все каталоги, начиная от корневого. Возможно, вы должны адаптировать maildir для своей системы. Мы написали checkmail так, чтобы она работала, даже если нет почтового ящика, поскольку большинство версий mail убирают почтовый ящик в том случае, когда он пуст.
Мы приводили эту программу в гл. 5 для иллюстрации циклов shell. Всякий раз при проверке почтового ящика она создает несколько процессов и загружает систему больше, чем хотелось бы. Версия на Си — единственный процесс, который выполняет stat для файла каждую минуту. Сколько времени требуется на то, чтобы checkmail постоянно выполнялась как фоновая задача? Как показали наши измерения, это время составляет меньше секунды в час, так что им вполне можно пренебречь.
sv : иллюстрация обработки ошибок
Следующей мы собираемся написать похожую на cp программу sv, которая будет копировать множество файлов в каталог, заменяя каждый файл лишь в том случае, если его нет в каталоге или он "старше" копируемого с тем же именем (имя sv означает "сохранять"). Суть действия программы состоит в том, что она не переписывает новую версию файла, sv использует больше информации из индексного дескриптора, чем checkmail. Вызов sv будет иметь такую конструкцию:
$ sv file1 file2 ... dir
Она копирует file1 в dir/file1, file2 в dir/file2 и т.д., если только целевой файл не новее, чем файл-источник; в этой ситуации копирование не происходит и печатается соответствующее предупреждение. Во избежание создания большого числа копий или связанных файлов sv не допускает применения символов '/' в любом исходном имени файла.
/* sv: save new files */
#include
#include
#include
#include
char *progname;
main(argc, argv)
int argc;
char *argv[];
{
int i;
struct stat stbuf;
char *dir = argv[argc-1];
progname = argv[0];
if (argc <= 2)
error("Usage: %s files... dir", progname);
if (stat(dir, &stbuf) == -1)
error("can't access directory %s", dir);
if ((stbuf.st_mode & S_IFMT) != S_IFDIR)
error("%s is not a directory", dir);
for (i = 1; i < argc-1; i++)
sv(argv[i], dir);
exit(0);
}
Значения времени, хранящиеся в индексных дескрипторах, исчисляются в секундах (за начало отсчета принято время 0:00 по Гринвичу, 1 января 1970 г.), так что более старые файлы имеют меньшие значения в поле st_mtime.
sv(file, dir) /* save file in dir */
char *file, *dir;
{
struct stat sti, sto;
int fin, fout, n;
char target[BUFSIZ], buf[BUFSIZ], *index();
sprintf(target, "%s/%s", dir, file);
if (index(file, '/') != NULL) /* strchr() in some systems */
error("won't handle /'s in %s", file);
if (stat(file, &sti) == -1)
error("can't stat %s", file);
if (stat(target, &sto) == -1) /* target not present */
sto.st_mtime = 0; /* so make it look old */
if (sti.st_mtime < sto.st_mtime) /* target is newer */
fprintf(stderr, "%s: %s not copied\n", progname, file);
else if ((fin = open(file, 0)) == -1)
error("can't open file %s", file);
else if ((fout = creat(target, sti.st_mode)) == -1)
error("can't create %s", target);
else
while ((n = read(fin, buf, sizeof buf)) > 0)
if (write(fout, buf, n) != n)
error("error writing %s", target);
close(fin);
close(fout);
}
Мы заменили стандартные функции ввода-вывода функцией creat, так что sv может сохранять режим работы входного файла. (Заметьте, что index и strchr — разные имена одной и той же процедуры; посмотрите в справочном руководстве по string(3), какое имя использует ваша система.)
Хотя программа sv довольно специфична, в ней отражены некоторые важные идеи. Многие программы не являются системными, но тем не менее используют информацию, поддерживаемую операционной системой и доступную через системные вызовы. Для таких программ существенно, что представление информации хранится только в стандартных файлах макроопределений типа
Отметим, что по крайней мере две трети кода sv составляет контроль ошибок. На ранних этапах написания программы было искушение сэкономить на обработке ошибок, поскольку это отвлекает от основной задачи. Когда же программа уже работает, трудно решиться на то, чтобы вернуться назад и вставить в нее процедуры проверки, которые превращают специальную программу в унифицированную.
Программа sv не защищена от возможных сбоев. Она, например, не обрабатывает прерывания в неподходящие моменты, но более аккуратна, чем большинство других программ. Хотелось бы обратить ваше внимание на финальный оператор write. Программа редко сбивается на этом операторе, поэтому многие программы игнорируют такую возможность. Однако переполнение дискового пространства, неполадки в линии связи или иные нарушения могут вызвать ошибки в write, и вы гораздо лучше справитесь с ними, если программа выдает вам соответствующее сообщение.
Дело в том, что контроль ошибок весьма утомителен, но тем не менее важен. Из-за ограниченного объема книги и обширности излагаемого в ней материала мы не уделяли должного внимания этому вопросу. Но в настоящих, "производственных" программах не следует позволять себе игнорировать ошибки.
Упражнение 7.10
Модифицируйте checkmail так, чтобы идентифицировать посылающего сообщение: "У вас есть почта". Подсказка : sscanf , lseek .
Упражнение 7.11
Модифицируйте checkmail так, чтобы она не переходила к каталогу почты перед входом в цикл. Окажет ли это ощутимое влияние на ее производительность? Более трудный вопрос: можете ли вы написать версию checkmail , которая обходится только одним процессом для оповещения всех пользователей?
Упражнение 7.12
Напишите программу watchfile , которая управляет файлом и печатает его с начала всякий раз, как он изменится. Когда бы вы ее использовали?
Упражнение 7.13
Программа sv очень "прямолинейна" при обработке ошибок. Модифицируйте ее так, чтобы она продолжала выполняться, даже если не удается обработать некоторый файл.
Упражнение 7.14
Сделайте sv рекурсивной: если один из исходных файлов — каталог, то этот каталог и все его файлы обрабатываются таким же образом. Сделайте рекурсивной cp . Подумайте, следует ли cp и sv объединить в одну программу, чтобы cp -v не создавала копию, если целевой файл новее файла-источника.
Упражнение 7.15
Напишите программу random .
$ random filename
должна выдавать одну строку, произвольно выбранную из файла. Если есть файл people , содержащий имена, random можно использовать в программе scapegoat ("козел отпущения"), полезной при случайном определении виновных:
$ cat scapegoat
echo "В этом виноват `random people`!"
$ scapegoat
В этом виноват Кен!
$
Убедитесь в том, что random хороша независимо от распределения длины строк.
Упражнение 7.16
Помимо прочего в индексном дескрипторе указаны адреса размещения блоков файла на диске. Рассмотрите файл <sys/into.h> , а затем напишите программу icat , которая должна читать файлы, описываемые номером записи каталога и устройством диска. (Она, конечно, будет работать только в том случае, если требуемый диск открыт на чтение.) При каких обстоятельствах icat полезна?
7.4 Процессы
В этом разделе мы покажем вам, как выполнить одну программу, вызвав ее из другой. Самый легкий путь — привлечь стандартную библиотечную программу system, упомянутую, но забракованную в гл. 6. Программа system использует один аргумент — командную строку в том виде, в каком она вводится с терминала (за исключением символа перевода строки), и выполняет ее порожденным shell. Если командная строка должна быть создана из кусочков, можно прибегнуть к форматированию памяти программой sprintf. В конце раздела мы рассмотрим более безопасную версию system для работы с диалоговыми программами, но прежде чем изучать программу в целом, обсудим структуры, из которых она составляется.
Создание процесса низкого уровня: execlp и execvp
Самая важная операция - выполнение другой программы без возврата с помощью системного вызова execlp. Например, чтобы напечатать дату и выполнить тем самым последнее действие запущенной программы, используют
execlp("date", "date", (char*)0);
Первый аргумент execlp есть имя файла команды; execlp выбирает путь поиска (т.е. $PATH) из вашего окружения и выполняет такой же поиск, как shell. Второй и последующие аргументы — это имена и аргументы команд; для новой программы они становятся массивом argv. Конец списка отмечен аргументом 0. (См. справочное руководство по exec(2), и вы поймете конструкцию execlp.)
Вызов execlp перекрывает существующую программу новой, запускает ее и затем завершается. Первоначальная программа получает управление обратно только при возникновении ошибки, например, когда файл не удается найти или он является невыполнимым:
execlp("date", "date", (char*)0);
fprintf(stderr, "Не удалось выполнить 'date'\n");
exit(1);
Если число аргументов вам заранее не известно, полезно применить execvp (вариант execlp). Вызов выглядит так:
execvp(filename, argp);
где argp означает массив указателей к аргументам (таким, как argv). Последним в массиве должен быть указатель NULL, так что execvp может отметить конец списка. Как и для execlp, filename — это файл, в котором находится программа, argp — массив argv для новой программы, a argp[0] — имя программы.
Ни одна из перечисленных выше программ не обеспечивает расширения в списке аргументов метасимволов типа <, >, *, кавычки и т.п. Если они вам нужны, воспользуйтесь execlp и вызовите /bin/sh из shell, которая выполнит эту работу. Сконструируйте строку commandline, содержащую полную команду, как если бы она была напечатана на терминале, например:
execlp("/bin/sh/", "sh", "-с", commandline, (char*)0);
Аргумент -с предписывает трактовать следующий аргумент как целую командную строку.
В качестве иллюстрации exec рассмотрим программу waitfile. Команда
$ waitfile filename [command]
периодически проверяет поименованный файл. Если он не менялся после последней проверки, выполняется command. В том случае, когда команда не указана, файл копируется в стандартный выходной поток. С помощью waitfile мы контролируем работу troff, как в
$ waitfile troff .out echo troff done &
Программа waitfile использует fstat, чтобы выявить время последнего изменения файла.
/* waitfile: wait until file stops changing */
#include
#include
#include
char *progname;
main(argc, argv)
int argc;
char *argv[];
{
int fd;
struct stat stbuf;
time_t old_time = 0;
progname = argv[0];
if (argc < 2)
error("Usage: %s filename [cmd]", progname);
if ((fd = open(argv[1], 0)) == -1)
error("can't open %s", argv[1]);
fstat(fd, &stbuf);
while(stbuf.st_mtime != old_time) {
old_time = stbuf.st_mtime;
sleep(60);
fstat(fd, &stbuf);
}
if (argc == 2) { /* copy file */
execlp("cat", "cat", argv[1], (char*)0);
error("can't execute cat %s", argv[1]);
} else { /* run process */
execvp(argv[2], &argv[2]);
error("can't execute %s", argv[2]);
}
exit(0);
}
Мы рассмотрели пример работы как execlp, так и execvp. Эта программа выбрана в качестве иллюстрации, поскольку она весьма полезна, но возможны и другие варианты. Так, waitfile могла бы просто завершиться по окончании изменения файла.
Упражнение 7.17
Модифицируйте watchfile (упр. 7.12) так, чтобы она имела то же свойство, что и waitfile : в отсутствие command копируется файл, в противном случае выполняется команда. Могли бы watchfile и waitfile разделять исходную программу? Подсказка: argv[0] .
Управление процессами: fork и wait
Следующий шаг — вновь получить управление после запуска программы с помощью execlp и execvp. Так как эти программы просто "перекрывают" старую программу новой, для сохранения старой требуется сначала разбить ее на две копии. Одна из копий может быть перекрыта, в то время как другая ждет новую, перекрывающую ее программу, чтобы завершиться. Разбиение выполняется с помощью системного вызова fork:
proc_id = fork();
Программа разбивается на две копии, каждая из которых продолжает работать. Они отличаются лишь значением, возвращаемым fork, — номером процесса process-id. В первом процессе (потомке) proc_id равен нулю, во втором (родительском) proc_id есть номер процесса-потомка. Итак, вызвать другую программу и вернуться можно следующим образом:
if (fork() == 0)
execlp("/bin/sh", "sh", "-с", commandline, (char*)0);
Фактически этого достаточно, за исключением обработки ошибок. Fork делает две копии программы. В процессе-потомке fork возвращает нуль, так что он вызывает execlp, которая выполняет commandline и затем завершается. В родительском процессе fork возвращает не нуль, поэтому execlp пропускается. (При наличии ошибки fork возвращает -1-)
Чаще родительский процесс ожидает, пока потомок закончит работу, прежде чем продолжить свое выполнение, для чего используется системный вызов wait:
int status;
if (fork() == 0)
execlp(...); /* потомок */
wait(&status); /* родитель */
Однако при этом не контролируются ошибки, такие, как сбои execlp и fork, или возможность одновременной работы нескольких процессов-потомков (wait возвращает номер завершившегося процесса-потомка, если вы хотите сравнить его со значением, возвращенным fork). Тем не менее эти три строки являются сердцевиной стандартной функции system.
Значение status, возвращаемое wait, содержит в своих младших восьми разрядах системное представление кода завершения процесса-потомка; оно равно нулю при нормальном завершении и не равно нулю при разного рода затруднениях. Следующие старшие восемь битов берутся из аргумента вызова exit или возвращаются из main, которая вызывает окончание выполнения процесса-потомка.
Если программа вызывается из shell, три дескриптора файла, 0, 1 и 2, ссылаются на соответствующие файлы, и все остальные дескрипторы доступны для использования. Когда эта программа вызывает другую, в соответствии с профессиональной этикой указанные условия должны быть соблюдены. Ни fork, ни exec не влияют никоим образом на открытые файлы; оба процесса, родитель и потомок, имеют одни и те же открытые файлы. Если процесс-родитель буферизует выходной поток, который необходимо вывести до процесса-потомка, родитель должен очистить свой буфер ранее execlp. И, наоборот, при буферизации родителем входного потока потомок потеряет информацию, которая читалась родителем. Выходной поток может быть выведен, но входной нельзя "положить назад". Обе ситуации являются следствием реализации входного или выходного потока стандартной библиотекой ввода-вывода, обсуждавшейся в гл. 6, поскольку при этом и ввод, и вывод буферизуются обычным образом.
Именно свойство наследования дескрипторов файлов через execlp используется в system: если у вызывающей программы стандартные входной и выходной потоки не связаны с терминалом, то этим же свойством обладает команда, вызванная из system. Возможно, такой вариант нам и нужен. В списке команд редактора ed, например, входной поток команды, начинающейся с символа !, вероятно, должен поступить из того же списка. Даже тогда ed должен считывать из своего входного потока по одному символу во избежание возникновения проблем буферизации ввода.
Для диалоговых программ, подобных p, system должна тем не менее вновь связать стандартный входной и выходной потоки с терминалом, в частности /dev/tty.
Системный вызов dup(fd) дублирует дескриптор файла fd на незанятый дескриптор файла с наименьшим номером и возвращает новый дескриптор, ссылающийся на тот же самый открытый файл. Следующая программа "присоединяет" стандартный входной поток программы к файлу:
int fd;
fd = open("file", 0);
close(0);
dup(fd);
close(fd);
Вызов close(fd) освобождает дескриптор файла 0 (стандартный входной поток), но, как правило, не влияет на процесс-родитель. Здесь приведена наша версия system для диалоговых программ, использующая progname для вывода сообщений об ошибках. Вам следует игнорировать те части функции, которые имеют дело с сигналами (мы вернемся к ним позднее).
/*
* Safer version of system for interactive programs
*/
#include
#include
system(s) /* run command line s */
char *s;
{
int status, pid, w, tty;
int (*istat)(), (*qstat)();
extern char *progname;
fflush(stdout);
tty = open("/dev/tty", 2);
if (tty == -1) {
fprintf(stderr, "%s: can't open /dev/tty\n", progname);
return -1;
}
if ((pid = fork()) == 0) {
close(0);
dup(tty);
close(1);
dup(tty);
close(2);
dup(tty);
close(tty);
execlp("sh", "sh", "-c", s, (char*)0);
exit(127);
}
close(tty);
istat = signal(SIGINT, SIG_IGN);
qstat = signal(SIGQUIT, SIG_IGN);
while ((w = wait(&status)) != pid && w != -1)
;
if (w == -1)
status = -1;
signal(SIGINT, istat);
signal(SIGQUIT, qstat);
return status;
}
Отметим, что /dev/tty открыта с режимом 2 — чтение и запись. С помощью dup формируются стандартный входной и выходной потоки. Здесь можно провести аналогию со сборкой системой стандартных входного и выходного потоков и потока ошибок, когда вы в нее входите. Поэтому в ваш стандартный входной поток можно писать:
$ echo hello 1>&0
hello
$
Это означает, что вам следует применить dup к дескриптору файла 2, чтобы вновь связать стандартные ввод и вывод, но открытие /dev/tty является более естественным и безопасным. Даже system имеет потенциальные проблемы: открытые файлы в вызывающей программе, такие, как tty в подпрограмме ttin программы p, будут передаваться процессу-потомку.
Смысл изложенного выше состоит не в том, что вы должны использовать нашу версию system для своих программ (она могла бы разрушить недиалоговый ed, например), а в том, чтобы понять, как управляют процессами и корректно используют примитивы; значение слова "корректно" меняется в зависимости от приложения и может быть не согласовано со стандартной реализацией system.
7.5 Сигналы и прерывания
Теперь мы рассмотрим работу с сигналами извне (такими, как прерывания) и ошибками программы. Последние возникают главным образом из-за некорректных обращений к памяти, выполнения привилегированных команд или при выполнении операций с плавающей запятой. Наиболее распространенными внешними сигналами являются прерывание, посылаемый при печати символа del, выйти, генерируемый символом FS (ctrl-\), отбой, вызываемый завершением телефонной связи, и закончить, генерируемый командой kill. Когда происходит одно из этих событий, посылается сигнал всем процессам, запущенным с того же терминала, и если не были приняты другие меры, процесс завершается. Для большинства сигналов пишется файл образа памяти, который может потребоваться при поиске ошибок (см. справочное руководство по adb(1), sdb(1)).
Системный вызов signal изменяет действие, заданное по умолчанию. Он имеет два аргумента: номер, определяющий сигнал, и адрес функции или код, предписывающий игнорировать сигнал либо запустить процедуру, принятую по умолчанию. Файл
#include
signal(SIGINT, SIG_IGN);
Специфицирует игнорирование прерываний, тогда как
signal(SIGINT, SIG_DEL);
восстанавливает действие по умолчанию, означающее завершение процесса. В любом случае signal возвращает предыдущее значение сигнала. Если второй аргумент signal представляет собой имя функции, которая уже должна быть описана в том же самом исходном файле, то функция будет вызвана, когда возникнет сигнал. Это практикуется довольно часто, чтобы программа могла "подчищать" неоконченные работы перед своим завершением, например удалять временный файл:
#include
char *tempfile = "temp.xxxxxx";
main() {
extern onintr();
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
mktemp(tempfile);
/* Process ... */
exit(0);
}
onintr() { /* почистить, если прервано */
unlink(tempfile);
exit(1);
}
Почему в main имеют место проверки и двойной вызов signal? Вспомните, что сигналы посылаются всем процессам, запущенным с данного терминала. Соответственно если программа должна быть запущена не в диалоговом режиме (с помощью &), shell делает так, что она будет игнорировать прерывания. Поэтому сигналы прерывания, посланные основным процессам, не остановят ее. Если бы эта программа началась с объявления о том, что все прерывания, которые должны быть посланы подпрограмме onintr, не принимаются во внимание, были бы сведены на нет все усилия shell защитить ее при запуске в фоновом режиме.
Решение, показанное выше, состоит в том, чтобы проверить состояние обработки прерываний, если они игнорировались ранее. Функции программы в том виде, в каком она написана, зависят от возвращаемого signal предыдущего состояния конкретного сигнала. Если сигналы уже игнорировались, процесс должен продолжить это дело; в противном случае их следует перехватывать.
Более сложная программа может перехватить прерывание и интерпретировать его как запрос на прекращение своих действий и возврат к основному циклу обработки команд. Подумаем о текстовом редакторе: прерывание длинного вывода на печать не должно вызывать завершения редактирования и потерю уже отредактированного текста. Программа для такого случая может быть написана следующим образом:
#include
#include
jmp_buf sjbuf;
main() {
int onintr();
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
setjmp(sjbuf);
/* сохранить текущую позицию стека */
for(;;) {
/* главный рабочий цикл */
}
...
}
onintr() { /* установить если прервано */
signal(SIGINT, onintr); /* установить
для следующего прерывания */
printf("\nInterrupt\n");
longjmp(sjbuf, 0); /* вернуться
в сохраненное состояние */
}
Файл
Отметим, что после прерывания сигнал вновь настраивается на onintr. Это обусловлено тем, что когда сигналы возникают, они автоматически настраиваются на реакцию по умолчанию.
Некоторые программы, которые "хотят" обнаружить сигналы, просто не могут быть остановлены в произвольный момент, например в середине обновления сложных составных данных. Решение состоит в том, что подпрограмма обработки прерывания должна установить флаг и вернуться к месту вызова exit или longjmp. Выполнение программы продолжится точно с того места, где оно было прервано, а флаг прерывания будет проверен позднее.
С этим подходом связана одна трудность. Предположим, что, когда посылается сигнал прерывания, программа читается с терминала. Описанная подпрограмма непременно вызывается; она устанавливает свой флаг и возвращается. Если бы, как отмечалось выше, было верно то, что выполнение возобновляется точно с того места, где оно прервалось, программа продолжала бы чтение с терминала до ввода пользователем другой строки. Однако здесь возникает недоразумение, поскольку пользователь может не знать, что программа читает, и предположительно предпочел бы, чтобы сигнал сразу оказал действие. Для разрешения проблемы система должна закончить read, но с сообщением об ошибке, указывающим, что произошло: errno присваивается EINTR, определенное в заголовке
Так, программы, которые "ловят" сигналы и продолжают после этого свою работу, должны быть готовы к появлению ошибок, вызванных прерванными системными вызовами. (Следует остерегаться системных вызовов read — чтение с терминала, wait, pause). Такая программа при чтении стандартного входного потока могла бы использовать фрагмент, подобный следующему:
#include
extern int errno;
...
if (read(0, &c, 1) <= 0) /* EOF или прерывание */
if (errno == EINTR) { /* EOF, вызванный прерыванием */
errno = 0; /* устанавливается для следующего раза */
} else { /* настоящий конец файла */
...
}
Очень сложно постоянно следить за тем, как реакция на сигнал комбинируется с выполнением других программ. Предположим, программа ловит сигналы прерывания и располагает средствами (типа "!"в ed) для выполнения других программ. Тогда программа могла бы выглядеть так:
if (fork() == 0)
execlp(...);
signal(SIGINT, SIG_IGN); /* родитель игнорирует прерывание */
wait(&status); /* пока потомок не завершился */
signal(SIGINT, onintr); /* восстанавливает прерывания */
Почему? Сигналы посылаются всем вашим процессам. Предположим, программа, которую вы вызвали, ловит свои собственные сигналы прерывания, как это делает редактор. Если вы прервете выполнение подпрограммы, она получит сигнал, вернется к своему главному циклу и, возможно, начнет читать с вашего терминала. Но вызывающая программа также перейдет от wait к подпрограмме и будет читать с терминала. Два процесса, читающие с вашего терминала, создадут трудную ситуацию, так как в результате системе придется гадать, к кому попадет та или иная строка входного потока. Решение состоит в том, чтобы родительская программа игнорировала прерывания, пока не завершился процесс-потомок. Это решение нашло свое отражение при обработке сигнала в system:
#include
system(s) /* run command line s */
char *s;
{
int status, pid, w, tty;
int (*istat)(), (*qstat)();
...
if ((pid = fork()) == 0) {
...
execlp("sh", "sh", "-c", s, (char*)0);
exit(127);
}
...
istat = signal(SIGINT, SIG_IGN);
qstat = signal(SIGQUIT, SIG_IGN);
while ((w = wait(&status)) != pid && w != -1);
if (w == -1)
status = -1;
signal(SIGINT, istat);
signal(SIGQUIT, qstat);
return status;
}
Несколько слов по поводу описаний: функция signal, очевидно, имеет довольно странный второй аргумент. Фактически он представляет собой указатель на функцию, поставляющую целое значение, и в то же время это тип самой подпрограммы сигнала. Две величины, SIG_IGN и SIG_DFL, имеют правильный тип, но выбраны так, что не совпадают ни с одной из существующих функции. Для любознательных покажем, как они определены для PDP-11 и VAX: определения, видимо, достаточно "неуклюжи", чтобы стимулировать использование
#define SIG_DFL (int(*)())0
#define SIG_IGM (int(*)())1
Будильники
Системный вызов alarm(n) обеспечивает посылку сигнала SIGALRM вашему процессу через n секунд. Сигнал будильника может быть использован для того, чтобы удостовериться в возникновении каких-то событий за соответствующее время. Если что-нибудь произошло, сигнал будильника может быть выключен; в противном случае процесс может получить управление, перехватив этот сигнал.
Для иллюстрации приведем программу timeout, которая запускает другую команду; если команда не закончила свое выполнение за определенное время, она будет завершена по звонку будильника. Например, вспомните команду watchfor из гл. 5. Вместо того чтобы запускать ее без ограничения времени работы, установите ограничение в часах:
$ timeout -3600 watchfor dmg &
Программа timeout демонстрирует почти все возможности, которые мы обсуждали в последних двух разделах. Создан процесс-потомок, родительский процесс устанавливает будильник и затем ждет, пока потомок завершит работу. Если будильник "зазвенел" первым, потомок уничтожается. Делается попытка вернуть состояние потомка при выходе.
/* timeout: set time limit on a process */
#include
#include
int pid; /* child process id */
char *progname;
main(argc, argv)
int argc;
char *argv[];
{
int sec = 10, status, onalarm();
progname = argv[0];
if (argc > 1 && argv[1][0] == '-') {
sec = atoi(&argv[1][1]);
argc--;
argv++;
}
if (argc < 2)
error("Usage: %s [-10] command", progname);
if ((pid=fork()) == 0) {
execvp(argv[1], &argv[1]);
error("couldn't start %s", argv[1]);
}
signal(SIGALRM, onalarm);
alarm(sec);
if (wait(&status) == -1 || (status & 0177) != 0)
error("%s killed", argv[1]);
exit((status >> 8) & 0377);
}
onalarm() /* kill child when alarm arrives */
{
kill(pid, SIGKILL);
}
Упражнение 7.18
Можете ли вы представить, как реализована sleep ? Подсказка : pause(2) . При каких обстоятельствах (если это вообще возможно) sleep и alarm могли бы помешать друг другу?
Историческая и библиографическая справка
Детального описания реализации системы UNIX не существует отчасти потому, что программа является собственностью фирмы. В статье К. Томпсона "UNIX implementation" (BSTJ, July, 1978) описываются основные идеи. Другие статьи, в которых обсуждаются связанные с UNIX темы, это "The UNIX system — a retrospective" (BSTJ, July, 1978) и "The evolution of the UNIX timesharing system" (Symposium on Language Design and Programming Methodology, Springer — Verlag, Lecture Notes in Computer Science #79, 1979). Обе статьи принадлежат Д. Ритчи.
Программа readslow была разработана П. Вейнбергером как недорогое средство, позволяющее следить за успехами шахматной машины "Белла" К. Томпсона и Дж. Кондона во время шахматных турниров. "Белла" записывала позиции своей игры в файл, а зрители просматривали файл с помощью readslow , не отнимая драгоценного времени у машины. (Новейшая версия "Беллы" лишь небольшую долю вычислений выполняет на основной машине, так что проблема снята.)
На создание spname нас вдохновил Т. Дафф. Статья А. Дархема, Д. Лэмба и Дж. Сакса "Spelling correction in user interfaces" (CACM, October, 1983) представляет иной способ коррекции написания в контексте программы почты.