функции, позволяющей распечатать все поля сразу (однако такая функция может быть написана вами для конкретного типа струк- тур). Также не существует формата для scanf(), который вводил бы структуру целиком. Вводить можно только по частям - каждое поле отдельно. 5.3. Напишите программу, которая по номеру месяца возвращает общее число дней года вплоть до этого месяца. 5.4. Переделайте предыдущую программу таким образом, чтобы она по написанному бук- вами названию месяца возвращала общее число дней года вплоть до этого месяца. В прог- рамме используйте функцию strcmp(). 5.5. Переделайте предыдущую программу таким образом, чтобы она запрашивала у пользо- вателя день, месяц, год и выдавала общее количество дней в году вплоть до данного дня. Месяц может обозначаться номером, названием месяца или его аббревиатурой. 5.6. Составьте структуру для учетной картотеки служащего, которая содержала бы сле- дующие сведения: фамилию, имя, отчество; год рождения; домашний адрес; место работы, должность; зарплату; дату поступления на работу. 5.7. Что печатает программа? struct man { char name[20]; int salary; } workers[] = { { "Иванов", 200 }, { "Петров", 180 }, { "Сидоров", 150 } }, *wptr, chief = { "начальник", 550 }; main(){ struct man *ptr, *cptr, save; ptr = wptr = workers + 1; cptr = &chief; save = workers[2]; workers[2] = *wptr; *wptr = save; wptr++; ptr--; ptr->salary = save.salary; printf( "%c %s %s %s %s\n%d %d %d %d\n%d %d %c\n", *workers[1].name, workers[2].name, cptr->name, ptr[1].name, save.name, wptr->salary, chief.salary, (*ptr).salary, workers->salary, wptr - ptr, wptr - workers, *ptr->name ); } Ответ: С Петров начальник Сидоров Сидоров 180 550 150 150 2 2 И 5.8. Разберите следующий пример: #include <stdio.h> struct man{ А. Богатырев, 1992-95 - 177 - Си в UNIX char *name, town[4]; int salary; int addr[2]; } men[] = { { "Вася", "Msc", 100, { 12, 7 } }, { "Гриша", "Len", 120, { 6, 51 } }, { "Петя", "Rig", 140, { 23, 84 } }, { NULL, "" , -1, { -1, -1 } } }; main(){ struct man *ptr, **ptrptr; int i; ptrptr = &ptr; *ptrptr = &men[1]; /* men+1 */ printf( "%s %d %s %d %c\n", ptr->name, ptr->salary, ptr->town, ptr->addr[1], ptr[1].town[2] ); (*ptrptr)++; /* копируем *ptr в men[0] */ men[0].name = ptr->name; /* (char *) #1 */ strcpy( men[0].town, ptr->town ); /* char [] #2 */ men[0].salary = ptr->salary; /* int #3 */ for( i=0; i < 2; i++ ) men[0].addr[i] = ptr->addr[i]; /* массив #4 */ /* распечатываем массив структур */ for(ptr=men; ptr->name; ptr++ ) printf( "%s %s %d\n", ptr->name, ptr->town, ptr->addr[0]); } Обратите внимание на такие моменты: 1) Как производится работа с указателем на указатель (ptrptr). 2) При копировании структур отдельными полями, поля скалярных типов (int, char, long, ..., указатели) копируются операцией присваивания (см. строки с пометками #1 и #3). Поля векторных типов (массивы) копируются при помощи цикла, поэле- ментно пересылающего массив (строка #4). Строки (массивы букв) пересылаются стандартной функцией strcpy (строка #2). Все это относится не только к полям структур, но и к переменным таких типов. Структуры можно также копировать не по полям, а целиком: men[0]= *ptr; 3) Запись аргументов функции printf() лесенкой позволяет лучше видеть, какому фор- мату соответствует каждый аргумент. 4) При распечатке массива структур мы печатаем не определенное их количество (рав- ное размеру массива), а пользуемся указателем NULL в поле name последней струк- туры как признаком конца массива. 5) В поле town мы храним строки из 3х букв, однако выделяем для хранения массив из 4х байт. Это необходимо потому, что строка "Msc" состоит не из 3х, а из 4х бай- тов: 'M','s','c','\0'. При работе со структурами и указателями большую помощь могут оказать рисунки. Вот как (например) можно нарисовать данные из этого примера (массив men изображен не весь): А. Богатырев, 1992-95 - 178 - Си в UNIX --ptr-- --ptrptr-- ptr | * |<------|---* | ---|--- ---------- | / =========men[0]== / men:|name | *---|-----> "Вася" | |---------------| | |town |M|s|c|\0| | |---------------| | |salary| 100 | | |---------------| | |addr | 12 | 7 | \ ----------------- \ =========men[1]== \-->|name | *---|-----> "Гриша" ............ 5.9. Составьте программу "справочник по таблице Менделеева", которая по названию химического элемента выдавала бы его характеристики. Таблицу инициализируйте массивом структур. 5.10. При записи данных в файл (да и вообще) используйте структуры вместо массивов, если элементы массива имеют разное смысловое назначение. Не воспринимайте структуру просто как средство объединения данных разных типов, она может быть и средством объе- динения данных одного типа, если это добавляет осмысленности нашей программе. Чем плох фрагмент? int data[2]; data[0] = my_key; data[1] = my_value; write(fd, (char *) data, 2 * sizeof(int)); Во-первых, тогда уж лучше указать размер всего массива сразу (хотя бы на тот случай, если мы изменим его размер на 3 и забудем поправить множитель с 2 на 3). write(fd, (char *) data, sizeof data); Кстати, почему мы пишем data, а не &data? (ответ: потому что имя массива и есть его адрес). Во-вторых, элементы массива имеют разный смысл, так не использовать ли тут структуру? struct _data { int key; int value; } data; data.key = my_key; data.value = my_value; write(fd, &data, sizeof data); 5.11. Что напечатает следующая программа? Нарисуйте расположение указателей по окон- чании данной программы. #include <stdio.h> struct lnk{ char c; А. Богатырев, 1992-95 - 179 - Си в UNIX struct lnk *prev, *next; } chain[20], *head = chain; add(c) char c; { head->c = c; head->next = head+1; head->next->prev = head; head++; } main(){ char *s = "012345"; while( *s ) add( *s++ ); head->c = '-'; head->next = (struct lnk *)NULL; chain->prev = chain->next; while( head->prev ){ putchar( head->prev->c ); head = head->prev; if( head->next ) head->next->prev = head->next->next; } } 5.12. Напишите программу, составлящую двунаправленный список букв, вводимых с клави- атуры. Конец ввода - буква '\n'. После третьей буквы вставьте букву '+'. Удалите пятую букву. Распечатайте список в обратном порядке. Оформите операции вставки/удаления как функции. Элемент списка должен иметь вид: struct elem{ char letter; /* буква */ char *word; /* слово */ struct elem *prev; /* ссылка назад */ struct elem *next; /* ссылка вперед */ }; struct elem *head, /* первый элемент списка */ *tail, /* последний элемент */ *ptr, /* рабочая переменная */ *prev; /* предыдущий элемент при просмотре */ int c, cmp; ... while((c = getchar()) != '\n' ) Insert(c, tail); for(ptr=head; ptr != NULL; ptr=ptr->next) printf("буква %c\n", ptr->letter); Память лучше отводить не из массива, а функцией calloc(), которая аналогична функции malloc(), но дополнительно расписывает выделенную память байтом '\0' (0, NULL). Вот функции вставки и удаления: extern char *calloc(); /* создать новое звено списка для буквы c */ struct elem *NewElem(c) char c; { struct elem *p = (struct elem *) calloc(1, sizeof(struct elem)); /* calloc автоматически обнуляет все поля, * в том числе prev и next */ p->letter = c; return p; } А. Богатырев, 1992-95 - 180 - Си в UNIX /* вставка после ptr (обычно - после tail) */ Insert(c, ptr) char c; struct elem *ptr; { struct elem *newelem = NewElem(c), *right; if(head == NULL){ /* список был пуст */ head=tail=newelem; return; } right = ptr->next; ptr->next = newelem; newelem->prev = ptr; newelem->next = right; if( right ) right->prev = newelem; else tail = newelem; } /* удалить ptr из списка */ Delete( ptr ) struct elem *ptr; { struct elem *left=ptr->prev, *right=ptr->next; if( right ) right->prev = left; if( left ) left->next = right; if( tail == ptr ) tail = left; if( head == ptr ) head = right; free((char *) ptr); } Напишите аналогичную программу для списка слов. struct elem *NewElem(char *s) { struct elem *p = (struct elem *) calloc(1, sizeof(struct elem)); p->word = strdup(s); return p; } void DeleteElem(struct elem *ptr){ free(ptr->word); free(ptr); } Усложнение: вставляйте слова в список в алфавитном порядке. Используйте для этого функцию strcmp(), просматривайте список так: struct elem *newelem; if (head == NULL){ /* список пуст */ head = tail = NewElem(новое_слово); return; } /* поиск места в списке */ for(cmp= -1, ptr=head, prev=NULL; ptr; prev=ptr, ptr=ptr->next ) if((cmp = strcmp(новое_слово, ptr->word)) <= 0 ) break; Если цикл окончился с cmp==0, то такое слово уже есть в списке. Если cmp < 0, то такого слова не было и ptr указывает элемент, перед которым надо вставить слово новое_слово, а prev - после которого (prev==NULL означает, что надо вставить в начало списка); т.е. слово вставляется между prev и ptr. Если cmp > 0, то слово надо доба- вить в конец списка (при этом ptr==NULL). head ==> "a" ==> "b" ==> "d" ==> NULL | | prev "c" ptr А. Богатырев, 1992-95 - 181 - Си в UNIX if(cmp == 0) return; /* слово уже есть */ newelem = NewElem( новое_слово ); if(prev == NULL){ /* в начало */ newelem->next = head; newelem->prev = NULL; head->prev = newelem; head = newelem; } else if(ptr == NULL){ /* в конец */ newelem->next = NULL; newelem->prev = tail; tail->next = newelem; tail = newelem; } else { /* между prev и ptr */ newelem->next = ptr; newelem->prev = prev; prev->next = newelem; ptr ->prev = newelem; } 5.13. Напишите функции для работы с комплексными числами struct complex { double re, im; }; Например, сложение выглядит так: struct complex add( c1, c2 ) struct complex c1, c2; { struct complex sum; sum.re = c1.re + c2.re; sum.im = c1.im + c2.im; return sum; } struct complex a = { 12.0, 14.0 }, b = { 13.0, 2.0 }; main(){ struct complex c; c = add( a, b ); printf( "(%g,%g)\n", c.re, c.im ); } 5.14. Массивы в Си нельзя присваивать целиком, зато структуры - можно. Иногда используют такой трюк: структуру из единственного поля-массива typedef struct { int ai[5]; } intarray5; intarray5 a, b = { 1, 2, 3, 4, 5 }; и теперь законно a = b; Зато доступ к ячейкам массива выглядит теперь менее изящно: А. Богатырев, 1992-95 - 182 - Си в UNIX a.ai[2] = 14; for(i=0; i < 5; i++) printf( "%d\n", a.ai[i] ); Также невозможно передать копию массива в качестве фактического параметра функции. Даже если мы напишем: typedef int ARR16[16]; ARR16 d; void f(ARR16 a){ printf( "%d %d\n", a[3], a[15]); a[3] = 2345; } void main(void){ d[3] = 9; d[15] = 98; f(d); printf("Now it is %d\n", d[3]); } то последний printf напечатает "Now it is 2345", поскольку в f передается адрес мас- сива, но не его копия; поэтому оператор a[3]=2345 изменяет исходный массив. Обойти это можно, использовав тот же трюк, поскольку при передаче структуры в качестве пара- метра передается уже не ее адрес, а копия всей структуры (как это и принято в Си во всех случаях, кроме массивов). 5.15. Напоследок упомянем про битовые поля - элементы структуры, занимающие только часть машинного слова - только несколько битов в нем. Размер поля в битах задается конструкцией :число_битов. Битовые поля используются для более компактного хранения информации в структурах (для экономии места). struct XYZ { /* битовые поля должны быть unsigned */ unsigned x:2; /* 0 .. 2**2 - 1 */ unsigned y:5; /* 0 .. 2**5 - 1 */ unsigned z:1; /* YES=1 NO=0 */ } xyz; main(){ printf("%u\n", sizeof(xyz)); /* == sizeof(int) */ xyz.z = 1; xyz.y = 21; xyz.x = 3; printf("%u %u %u\n", xyz.x, ++xyz.y, xyz.z); /* Значение битового поля берется по модулю * максимально допустимого числа 2**число_битов - 1 */ xyz.y = 32 /* максимум */ + 7; xyz.x = 16+2; xyz.z = 11; printf("%u %u %u\n", xyz.x, xyz.y, xyz.z); /* 2 7 1 */ } Поле ширины 1 часто используется в качестве битового флага: вместо #define FLAG1 01 #define FLAG2 02 #define FLAG3 04 int x; /* слово для нескольких флагов */ x |= FLAG1; x &= ~FLAG2; if(x & FLAG3) ...; используется struct flags { unsigned flag1:1, flag2:1, flag3:1; } x; x.flag1 = 1; x.flag2 = 0; if( x.flag3 ) ...; А. Богатырев, 1992-95 - 183 - Си в UNIX Следует однако учесть, что машинный код для работы с битовыми полями более сложен и занимает больше команд (т.е. медленнее и длиннее). К битовым полям нельзя применить операцию взятия адреса "&", у них нет адресов и смещений! 5.16. Пример на использование структур с полем переменного размера. Часть перемен- ной длины может быть лишь одна и обязана быть последним полем структуры. Внимание: это программистский трюк, использовать осторожно! #include <stdio.h> #define SZ 5 extern char *malloc(); #define VARTYPE char struct obj { struct header { /* постоянная часть */ int cls; int size; /* размер переменной части */ } hdr; VARTYPE body [1]; /* часть переменного размера: в описании ровно ОДИН элемент массива */ } *items [SZ]; /* указатели на структуры */ #define OFFSET(field, ptr) ((char *) &ptr->field - (char *)ptr) int body_offset; /* создание новой структуры */ struct obj *newObj( int cl, char *s ) { char *ptr; struct obj *op; int n = strlen(s); /* длина переменной части (штук VARTYPE) */ int newsize = sizeof(struct header) + n * sizeof(VARTYPE); printf("[n=%d newsize=%d]\n", n, newsize); /* newsize = (sizeof(struct obj) - sizeof(op->body)) + n * sizeof(op->body); При использовании этого размера не учитывается, что struct(obj) выровнена на границу sizeof(int). Но в частности следует учитывать и то, на границу чего выровнено начало поля op->body. То есть самым правильным будет newsize = body_offset + n * sizeof(op->body); */ /* отвести массив байт без внутренней структуры */ ptr = (char *) malloc(newsize); /* наложить поверх него структуру */ op = (struct obj *) ptr; op->hdr.cls = cl; op->hdr.size = n; strncpy(op->body, s, n); return op; } А. Богатырев, 1992-95 - 184 - Си в UNIX void printobj( struct obj *p ) { register i; printf( "OBJECT(cls=%d,size=%d)\n", p->hdr.cls, p->hdr.size); for(i=0; i < p->hdr.size; i++ ) putchar( p->body[i] ); putchar( '\n' ); } char *strs[] = { "a tree", "a maple", "an oak", "the birch", "the fir" }; int main(int ac, char *av[]){ int i; printf("sizeof(struct header)=%d sizeof(struct obj)=%d\n", sizeof(struct header), sizeof(struct obj)); { struct obj *sample; printf("offset(cls)=%d\n", OFFSET(hdr.cls, sample)); printf("offset(size)=%d\n", OFFSET(hdr.size, sample)); printf("offset(body)=%d\n", body_offset = OFFSET(body, sample)); } for( i=0; i < SZ; i++ ) items[i] = newObj( i, strs[i] ); for( i=0; i < SZ; i++ ){ printobj( items[i] ); free( items[i] ); items[i] = NULL; } return 0; } 5.17. Напишите программу, реализующую список со "старением". Элемент списка, к которому обращались последним, находится в голове списка. Самый старый элемент вытесняется к хвосту списка и в конечном счете из списка удаляется. Такой алгоритм использует ядро UNIX для кэширования блоков файла в оперативной памяти: блоки, к которым часто бывают обращения оседают в памяти (а не на диске). /* Список строк, упорядоченных по времени их добавления в список, * т.е. самая "свежая" строка - в начале, самая "древняя" - в конце. * Строки при поступлении могут и повторяться! По подобному принципу * можно организовать буферизацию блоков при обмене с диском. */ #include <stdio.h> extern char *malloc(), *gets(); #define MAX 3 /* максимальная длина списка */ int nelems = 0; /* текущая длина списка */ struct elem { /* СТРУКТУРА ЭЛЕМЕНТА СПИСКА */ char *key; /* Для блоков - это целое - номер блока */ struct elem *next; /* следующий элемент списка */ /* ... и может что-то еще ... */ } *head; /* голова списка */ void printList(), addList(char *), forget(); А. Богатырев, 1992-95 - 185 - Си в UNIX void main(){ /* Введите a b c d b a c */ char buf[128]; while(gets(buf)) addList(buf), printList(); } /* Распечатка списка */ void printList(){ register struct elem *ptr; printf( "В списке %d элементов\n", nelems ); for(ptr = head; ptr != NULL; ptr = ptr->next ) printf( "\t\"%s\"\n", ptr->key ); } /* Добавление в начало списка */ void addList(char *s) { register struct elem *p, *new; /* Анализ - нет ли уже в списке */ for(p = head; p != NULL; p = p->next ) if( !strcmp(s, p->key)){ /* Есть. Перенести в начало списка */ if( head == p ) return; /* Уже в начале */ /* Удаляем из середины списка */ new = p; /* Удаляемый элемент */ for(p = head; p->next != new; p = p->next ); /* p указывает на предшественника new */ p->next = new->next; goto Insert; } /* Нет в списке */ if( nelems >= MAX ) forget(); /* Забыть старейший */ if((new = (struct elem *) malloc(sizeof(struct elem)))==NULL) goto bad; if((new->key = malloc(strlen(s) + 1)) == NULL) goto bad; strcpy(new->key, s); nelems++; Insert: new->next = head; head = new; return; bad: printf( "Нет памяти\n" ); exit(13); } /* Забыть хвост списка */ void forget(){ struct elem *prev = head, *tail; if( head == NULL ) return; /* Список пуст */ /* Единственный элемент ? */ if((tail = head->next) == NULL){ tail=head; head=NULL; goto Del; } for( ; tail->next != NULL; prev = tail, tail = tail->next ); prev->next = NULL; Del: free(tail->key); free(tail); nelems--; } А. Богатырев, 1992-95 - 186 - Си в UNIX  * 6. Системные вызовы и взаимодействие с UNIX. *  В этой главе речь пойдет о процессах. Скомпилированная программа хранится на диске как обычный нетекстовый файл. Когда она будет загружена в память компьютера и начнет выполняться - она станет процессом. UNIX - многозадачная система (мультипрограммная). Это означает, что одновре- менно может быть запущено много процессов. Процессор выполняет их в режиме разделения времени - выделяя по очереди квант времени одному процессу, затем другому, третьему... В результате создается впечатление параллельного выполнения всех процес- сов (на многопроцессорных машинах параллельность истинная). Процессам, ожидающим некоторого события, время процессора не выделяется. Более того, "спящий" процесс может быть временно откачан (т.е. скопирован из памяти машины) на диск, чтобы освобо- дить память для других процессов. Когда "спящий" процесс дождется события, он будет "разбужен" системой, переведен в ранг "готовых к выполнению" и, если был откачан - будет возвращен с диска в память (но, может быть, на другое место в памяти!). Эта процедура носит название "своппинг" (swapping). Можно запустить несколько процессов, выполняющих программу из одного и того же файла; при этом все они будут (если только специально не было предусмотрено иначе) независимыми друг от друга. Так, у каждого пользователя, работающего в системе, име- ется свой собственный процесс-интерпретатор команд (своя копия), выполняющий прог- рамму из файла /bin/csh (или /bin/sh). Процесс представляет собой изолированный "мир", общающийся с другими "мирами" во Вселенной при помощи: a) Аргументов функции main: void main(int argc, char *argv[], char *envp[]); Если мы наберем команду $ a.out a1 a2 a3 то функция main программы из файла a.out вызовется с argc = 4 /* количество аргументов */ argv[0] = "a.out" argv[1] = "a1" argv[2] = "a2" argv[3] = "a3" argv[4] = NULL По соглашению argv[0] содержит имя выполняемого файла из которого загружена эта программа|-. b) Так называемого "окружения" (или "среды") char *envp[], продублированного также в предопределенной переменной extern char **environ; Окружение состоит из строк вида "ИМЯПЕРЕМЕННОЙ=значение" Массив этих строк завершается NULL (как и argv). Для получения значения пере- менной с именем ИМЯ существует стандартная функция char *getenv( char *ИМЯ ); Она выдает либо значение, либо NULL если переменной с таким именем нет. c) Открытых файлов. По умолчанию (неявно) всегда открыты 3 канала: ВВОД В Ы В О Д FILE * stdin stdout stderr соответствует fd 0 1 2 связан с клавиатурой дисплеем ____________________ |- Именно это имя показывает команда ps -ef #include <stdio.h> main(ac, av) char **av; { execl("/bin/sleep", "Take it easy", "1000", NULL); } А. Богатырев, 1992-95 - 187 - Си в UNIX Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, прог- рамма может сама явно открывать файлы (при помощи open, creat, pipe, fopen). Всего программа может одновременно открыть до 20 файлов (считая стандартные каналы), а в некоторых системах и больше (например, 64). В MS DOS есть еще 2 предопределенных канала вывода: stdaux - в последовательный коммуникационный порт, stdprn - на принтер. d) Процесс имеет уникальный номер, который он может узнать вызовом int pid = getpid(); а также узнать номер "родителя" вызовом int ppid = getppid(); Процессы могут по этому номеру посылать друг другу сигналы: kill(pid /* кому */, sig /* номер сигнала */); и реагировать на них signal (sig /*по сигналу*/, f /*вызывать f(sig)*/); e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая память, сетевые коммуникации. f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий каталог, который достается в наследство от процесса-"родителя", и может быть затем изменен системным вызовом chdir(char *имя_нового_каталога); У каждого процесса есть свой собственный текущий рабочий каталог (в отличие от MS DOS, где текущий каталог одинаков для всех задач). К "прочим" характеристи- кам отнесем также: управляющий терминал; группу процессов (pgrp); идентификатор (номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции и маски, заданные на различные сигналы; и.т.п. g) Издания других запросов (системных вызовов) к операционной системе ("богу") для выполнения различных "внешних" операций. h) Все остальные действия происходят внутри процесса и никак не влияют на другие процессы и устройства ("миры"). В частности, один процесс НИКАК не может полу- чить доступ к памяти другого процесса, если тот не позволил ему это явно (меха- низм shared memory); адресные пространства процессов независимы и изолированы (равно и пространство ядра изолировано от памяти процессов). Операционная система выступает в качестве коммуникационной среды, связывающей "миры"-процессы, "миры"-внешние устройства (включая терминал пользователя); а также в качестве распорядителя ресурсов "Вселенной", в частности - времени (по очереди выде- ляемого активным процессам) и пространства (в памяти компьютера и на дисках). Мы уже неоднократно упоминали "системные вызовы". Что же это такое? С точки зрения Си-программиста - это обычные функции. В них передают аргументы, они возвра- щают значения. Внешне они ничем не отличаются от написанных нами или библиотечных функций и вызываются из программ одинаковым с ними способом. С точки же зрения реализации - есть глубокое различие. Тело функции-сисвызова расположено не в нашей программе, а в резидентной (т.е. постоянно находящейся в памяти компьютера) управляющей программе, называемой ядром операционной системы|-. ____________________ |- Собственно, операционная система характеризуется набором предоставляемых ею сис- темных вызовов, поскольку все концепции, заложенные в системе, доступны нам только через них. Если мы имеем две реализации системы с разным внутренним устройством ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться, но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX-ами на однопроцессорных и многопроцессорных машинах. Но для нас ядро - это "черный ящик", полностью определяемый его поведением, т.е. своим интерфейсом с программами, но не внутренним устройством. Вторым параметром, характеризующим ОС, являются фор- маты данных, используемые системой: форматы данных для сисвызовов и формат информации в различных файлах, в том числе формат оформления выполняемых файлов (формат данных в физической памяти машины в этот список не входит - он зависим от реализации и от про- цессора). Как правило, программа пишется так, чтобы использовать соглашения, приня- тые в данной системе, для чего она просто включает ряд стандартных include-файлов с описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы. А. Богатырев, 1992-95 - 188 - Си в UNIX Сам термин "системный вызов" как раз означает "вызов системы для выполнения дейст- вия", т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме, в котором имеет доступ к некоторым системным таблицам|=, регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в поль- зовательской программе вызывается библиотечная функция-"корешок", тело которой напи- сано на ассемблере и содержит команду генерации программного прерывания. Это - глав- ное отличие от нормальных Си-функций - вызов по прерыванию. Вторым этапом является реакция ядра на прерывание: 1. переход в привелегированный режим; 2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к адресному пространству ядра (context switching); 3. извлечение аргументов из памяти запросившего процесса; 4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам - это номер системного вызова); 5. проверка корректности остальных аргументов; 6. проверка прав процесса на допустимость выполнения такого запроса; 7. вызов тела требуемого системного вызова - это обычная Си-функция в ядре; 8. возврат ответа в память процесса; 9. выключение привелегированного режима; 10. возврат из прерывания. Во время системного вызова (шаг 7) процесс может "заснуть", дожидаясь некоторого события (например, нажатия кнопки на клавиатуре). В это время ядро передаст управле- ние другому процессу. Когда наш процесс будет "разбужен" (событие произошло) - он продолжит выполнение шагов системного вызова. Большинство системных вызовов возвращают в программу в качестве своего значения признак успеха: 0 - все сделано, (-1) - сисвызов завершился неудачей; либо некоторое содержательное значение при успехе (вроде дескриптора файла в open(), и (-1) при неу- даче. В случае неудачного завершения в предопределенную переменную errno заносится номер ошибки, описывающий причину неудачи (коды ошибок предопределены, описаны в include-файле <errno.h> и имеют вид Eчтото). Заметим, что при УДАЧЕ эта переменная просто не изменяется и может содержать любой мусор, поэтому проверять ее имеет смысл лишь в случае, если ошибка действительно произошла: #include <errno.h> /* коды ошибок */ extern int errno; extern char *sys_errlist[]; int value; if((value = sys_call(...)) < 0 ){ printf("Error:%s(%d)\n", sys_errlist[errno], errno ); exit(errno); /* принудительное завершение программы */ } ____________________ Поведение всех программ в системе вытекает из поведения системных вызовов, кото- рыми они пользуются. Даже то, что UNIX является многозадачной системой, непосредст- венно вытекает из наличия системных вызовов fork, exec, wait и спецификации их функ- ционирования! То же можно сказать про язык Си - мобильность программы зависит в основном от набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта са- мого языка, который должен удовлетворять стандарту на язык Си). Если две разные сис- темы предоставляют все эти функции (которые могут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково. |= Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого процесса), и.т.п. А. Богатырев, 1992-95 - 189 - Си в UNIX Предопределенный массив sys_errlist, хранящийся в стандартной библиотеке, содержит строки-расшифровку смысла ошибок (по-английски). Посмотрите описание функции per- ror(). 6.1. Файлы и каталоги. 6.1.1. Используя системный вызов stat, напишите программу, определяющую тип файла: обычный файл, каталог, устройство, FIFO-файл. Ответ: #include <sys/types.h> #include <sys/stat.h> typeOf( name ) char *name; { int type; struct stat st; if( stat( name, &st ) < 0 ){ printf( "%s не существует\n", name ); return 0; } printf("Файл имеет %d имен\n", st.st_nlink); switch(type = (st.st_mode & S_IFMT)){ case S_IFREG: printf( "Обычный файл размером %ld байт\n", st.st_size ); break; case S_IFDIR: printf( "Каталог\n" ); break; case S_IFCHR: /* байтоориентированное */ case S_IFBLK: /* блочноориентированное */ printf( "Устройство\n" ); break; case S_IFIFO: printf( "FIFO-файл\n" ); break; default: printf( "Другой тип\n" ); break; } return type; } 6.1.2. Напишите программу, печатающую: свои аргументы, переменные окружения, инфор- мацию о всех открытых ею файлах и используемых трубах. Для этой цели используйте системный вызов struct stat st; int used, fd; for(fd=0; fd < NOFILE; fd++ ){ used = fstat(fd, &st) < 0 ? 0 : 1; ... } Программа может использовать дескрипторы файлов с номерами 0..NOFILE-1 (обычно 0..19). Если fstat для какого-то fd вернул код ошибки (<0), это означает, что данный дескриптор не связан с открытым файлом (т.е. не используется). NOFILE определено в include-файле <sys/param.h>, содержащем разнообразные параметры данной системы. 6.1.3. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего каталога (файла с именем ".") без сортировки имен по алфавиту. Предусмотрите чтение каталога, чье имя задается как аргумент программы. Имена "." и ".." не выдавать. Формат каталога описан в header-файле <sys/dir.h> и в "канонической" версии выг- лядит так: каталог - это файл, состоящий из структур direct, каждая описывает одно имя файла, входящего в каталог: А. Богатырев, 1992-95 - 190 - Си в UNIX struct direct { unsigned short d_ino; /* 2 байта: номер I-узла */ char d_name[DIRSIZ]; /* имя файла */ }; В семействе BSD формат каталога несколько иной - там записи имеют разную длину, зави- сящую от длины имени файла, которое может иметь длину от 1 до 256 символов. Имя файла может состоять из любых символов, кроме '\0', служащего признаком конца имени и '/', служащего разделителем. В имени допустимы пробелы, управляющие символы (но не рекомендуются!), любое число точек (в отличие от MS DOS, где допустима единственная точка, отделяющая собственно имя от суффикса (расширения)), разрешены даже непечатные (т.е. управляющие) символы! Если имя файла имеет длину 14 (DIRSIZ) символов, то оно не оканчивается байтом '\0'. В этом случае для печати имени файла возможны три подхода: 1. Выводить символы при помощи putchar()-а в цикле. Цикл прерывать по индексу рав- ному DIRSIZ, либо по достижению байта '\0'. 2. Скопировать поле d_name в другое место: char buf[ DIRSIZ + 1 ]; strncpy(buf, d.d_name, DIRSIZ); buf[ DIRSIZ ] = '\0'; Этот способ лучший, если имя файла надо не просто напечатать, но и запомнить на будущее, чтобы использовать в своей программе. 3. Использовать такую особенность функции printf(): #include <sys/types.h> #include <sys/dir.h> struct direct d; ... printf( "%*.*s\n", DIRSIZ, DIRSIZ, d.d_name ); Если файл был стерт, то в поле d_ino записи каталога будет содержаться 0 (именно поэтому I-узлы нумеруются начиная с 1, а не с 0). При удалении файла содержимое его (блоки) уничтожается, I-узел освобождается, но имя в каталоге не затирается физи- чески, а просто помечается как стертое: d_ino=0; Каталог при этом никак не уплотня- ется и не укорачивается! Поэтому имена с d_ino==0 выдавать не следует - это имена уже уничтоженных файлов. При создании нового имени (creat, link, mknod) система просматривает каталог и переиспользует первый от начала свободный слот (ячейку каталога) где d_ino==0, запи- сывая новое имя в него (только в этот момент старое имя-призрак окончательно исчезнет физически). Если пустых мест нет - каталог удлиняется. Любой каталог всегда содержит два стандартных имени: "." - ссылка на этот же каталог (на его собственный I-node), ".." - на вышележащий каталог. У корневого каталога "/" оба этих имени ссылаются на него же самого (т.е. содержат d_ino==2). Имя каталога не содержится в нем самом. Оно содержится в "родительском" каталоге ... Каталог в UNIX - это обычный дисковый файл. Вы можете читать его из своих прог- рамм. Однако никто (включая суперпользователя|=) не может записывать что-либо в ката- лог при помощи write. Изменения содержимого каталогов выполняет только ядро, отвечая на запросы в виде системных вызовов creat, unlink, link, mkdir, rmdir, rename, mknod. Коды доступа для каталога интерпретируются следующим образом: w запись S_IWRITE. Означает право создавать и уничтожать в каталоге имена файлов при ____________________ |= Суперпользователь (superuser) имеет uid==0. Это "привелегированный" пользова- тель, который имеет право делать ВСЕ. Ему доступны любые сисвызовы и файлы, несмотря на коды доступа и.т.п. А. Богатырев, 1992-95 - 191 - Си в UNIX помощи этих вызовов. То есть: право создавать, удалять и переименовывать файлы в каталоге. Отметим, что для переименования или удаления файла вам не требуется иметь доступ по записи к самому файлу - достаточно иметь доступ по записи к каталогу, содержащему его имя! r чтение S_IREAD. Право читать каталог как обычный файл (право выполнять opendir, см. ниже): благодаря этому мы можем получить список имен файлов, содержащихся в каталоге. Однако, если мы ЗАРАНЕЕ знаем имена файлов в каталоге, мы МОЖЕМ рабо- тать с ними - если имеем право доступа "выполнение" для этого каталога! x выполнение S_IEXEC. Разрешает поиск в каталоге. Для открытия файла, создания/удаления файла, перехода в другой каталог (chdir), система выполняет следующие действия (осуществляемые функцией namei() в ядре): чтение каталога и поиск в нем указан- ного имени файла или каталога; найденному имени соответствует номер I-узла d_ino; по номеру узла система считывает с диска сам I-узел нужного файла и по нему добирается до содержимого файла. Код "выполнение" - это как раз разрешение такого просмотра каталога системой. Если каталог имеет доступ на чтение - мы можем получить список файлов (т.е. применить команду ls); но если он при этом не имеет кода доступа "выполнение" - мы не сможем получить доступа ни к одному из файлов каталога (ни открыть, ни удалить, ни создать, ни сделать stat, ни chdir). Т.е. "чтение" разрешает применение вызова read, а "выполнение" - функции ядра namei. Фактически "выполнение" означает "доступ к файлам в данном каталоге"; еще более точно - к I-nodам файлов этого каталога. t sticky bit S_ISVTX - для каталога он означает, что удалить или переименовать некий файл в данном каталоге могут только: владелец каталога, владелец данного файла, супер- пользователь. И никто другой. Это исключает удаление файлов чужими. Совет: для каталога полезно иметь такие коды доступа: chmod o-w,+t каталог В системах BSD используется, как уже было упомянуто, формат каталога с переменной длиной записей. Чтобы иметь удобный доступ к именам в каталоге, возникли специальные функции чтения каталога: opendir, closedir, readdir. Покажем, как простейшая команда ls реализуется через эти функции. А. Богатырев, 1992-95 - 192 - Си в UNIX #include <stdio.h> #include <sys/types.h> #include <dirent.h> int listdir(char *dirname){ register struct dirent *dirbuf; DIR *fddir; ino_t dot_ino = 0, dotdot_ino = 0; if((fddir = opendir (dirname)) == NULL){ fprintf(stderr, "Can't read %s\n", dirname); return 1; } /* Без сортировки по алфавиту */ while ((dirbuf = readdir (fddir)) != NULL ) { if (dirbuf->d_ino == 0) continue; if (strcmp (dirbuf->d_name, "." ) == 0){ dot_ino = dirbuf->d_ino; continue; } else if(strcmp (dirbuf->d_name, "..") == 0){ dotdot_ino = dirbuf->d_ino; continue; } else printf("%s\n", dirbuf->d_name); } closedir (fddir); if(dot_ino == 0) printf("Поврежденный каталог: нет имени \".\"\n"); if(dotdot_ino == 0) printf("Поврежденный каталог: нет имени \"..\"\n"); if(dot_ino && dot_ino == dotdot_ino) printf("Это корневой каталог диска\n"); return 0; } int main(int ac, char *av[]){ int i; if(ac > 1) for(i=1; i < ac; i++) listdir(av[i]); else listdir("."); return 0; } Обратите внимание, что тут не требуется добавление '\0' в конец поля d_name, пос- кольку его предоставляет нам сама функция readdir(). 6.1.4. Напишите программу удаления файлов и каталогов, заданных в argv. Делайте stat, чтобы определить тип файла (файл/каталог). Программа должна отказываться уда- лять файлы устройств. Для удаления пустого каталога (не содержащего иных имен, кроме "." и "..") сле- дует использовать сисвызов rmdir(имя_каталога); (если каталог не пуст - errno получит значение EEXIST); а для удаления обычных файлов (не каталогов) unlink(имя_файла); Программа должна запрашивать подтверждение на удаление каждого файла, выдавая его имя, тип, размер в килобайтах и вопрос "удалить ?". 6.1.5. Напишите функцию рекурсивного обхода дерева подкаталогов и печати имен всех файлов в нем. Ключ U42 означает файловую систему с длинными именами файлов (BSD 4.2). А. Богатырев, 1992-95 - 193 - Си в UNIX /*#!/bin/cc -DFIND -DU42 -DMATCHONLY treemk.c match.c -o tree -lx * Обход поддерева каталогов (по мотивам Керниган & Ритчи). * Ключи компиляции: * BSD-4.2 BSD-4.3 -DU42 * XENIX с канонической файл.сист. ничего * XENIX с библиотекой -lx -DU42 * программа поиска файлов -DFIND * программа рекурсивного удаления -DRM_REC * программа подсчета используемого места на диске БЕЗ_КЛЮЧА */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/param.h> /* для MAXPATHLEN */ #if defined(M_XENIX) && defined(U42) # include <sys/ndir.h> /* XENIX + U42 эмуляция */ #else # include <dirent.h> # define stat(f,s) lstat(f,s) /* не проходить по символьным ссылкам */ # define d_namlen d_reclen #endif /* проверка: каталог ли это */ #define isdir(st) ((st.st_mode & S_IFMT) == S_IFDIR) struct stat st; /* для сисвызова stat() */ char buf[MAXPATHLEN+1]; /* буфер для имени файла */ #define FAILURE (-1) /* код неудачи */ #define SUCCESS 1 /* код успеха */ #define WARNING 0 /* нефатальная ошибка */ /* Сообщения об ошибках во время обхода дерева: */ #ifndef ERR_CANT_READ # define ERR_CANT_READ(name) \ fprintf( stderr, "\tНе могу читать \"%s\"\n", name), WARNING # define ERR_NAME_TOO_LONG() \ fprintf( stderr, "\tСлишком длинное полное имя\n" ), WARNING #endif /* Прототипы для предварительного объявления функций. */ extern char *strrchr(char *, char); int directory (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st)); /* Функции-обработчики enter, leave, touch должны * возвращать (-1) для прерывания просмотра дерева, * либо значение >= 0 для продолжения. */ А. Богатырев, 1992-95 - 194 - Си в UNIX /* Обойти дерево с корнем в rootdir */ int walktree ( char *rootdir, /* корень дерева */ int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st) ){ /* проверка корректности корня */ if( stat(rootdir, &st) < 0 || !isdir(st)){ fprintf( stderr, "\tПлохой корень дерева \"%s\"\n", rootdir ); return FAILURE; /* неудача */ } strcpy (buf, rootdir); return act (buf, 0, enter, leave, touch); } /* Оценка файла с именем name. */ int act (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st)) { if (stat (name, &st) < 0) return WARNING; /* ошибка, но не фатальная */ if(isdir(st)){ /* позвать обработчик каталогов */ if(enter) if( enter(name, level, &st) == FAILURE ) return FAILURE; return directory (name, level+1, enter, leave, touch); } else { /* позвать обработчик файлов */ if(touch) return touch (name, level, &st); else return SUCCESS; } } А. Богатырев, 1992-95 - 195 - Си в UNIX /* Обработать каталог: прочитать его и найти подкаталоги */ int directory (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st)) { #ifndef U42 struct direct dirbuf; int fd; #else register struct dirent *dirbuf; DIR *fd; extern DIR *opendir(); #endif char *nbp, *tail, *nep; int i, retcode = SUCCESS; #ifndef U42 if ((fd = open (name, 0)) < 0) { #else if ((fd = opendir (name)) == NULL) { #endif return ERR_CANT_READ(name); } tail = nbp = name + strlen (name); /* указатель на закрывающий \0 */ if( strcmp( name, "/" )) /* если не "/" */ *nbp++ = '/'; *nbp = '\0'; #ifndef U42 if (nbp + DIRSIZ + 2 >= name + MAXPATHLEN) { *tail = '\0'; return ERR_NAME_TOO_LONG(); } #endif #ifndef U42 while (read(fd, (char *) &dirbuf, sizeof(dirbuf)) == sizeof(dirbuf)){ if (dirbuf.d_ino == 0) /* стертый файл */ continue; if (strcmp (dirbuf.d_name, "." ) == 0 || strcmp (dirbuf.d_name, "..") == 0) /* не интересуют */ continue; for (i = 0, nep = nbp; i < DIRSIZ; i++) *nep++ = dirbuf.d_name[i]; # else /*U42*/ while ((dirbuf = readdir (fd)) != NULL ) { if (dirbuf->d_ino == 0) continue; if (strcmp (dirbuf->d_name, "." ) == 0 || strcmp (dirbuf->d_name, "..") == 0) continue; for (i = 0, nep = nbp; i < dirbuf->d_namlen ; i++) *nep++ = dirbuf->d_name[i]; #endif /*U42*/ *nep = '\0'; if( act(name, level, enter, leave, touch) == FAILURE) { retcode = FAILURE; break; } } А. Богатырев, 1992-95 - 196 - Си в UNIX #ifndef U42 close (fd); #else closedir(fd); #endif *tail = '\0'; /* восстановить старое name */ if(retcode != FAILURE && leave) if( leave(name, level) == FAILURE) retcode = FAILURE; return retcode; } /* -------------------------------------------------------------- */ /* Disk Usage -- Оценка места, занимаемого файлами поддерева */ /* -------------------------------------------------------------- */ /* Пересчет байтов в килобайты */ #define KB(s) (((s)/1024L) + ((s)%1024L ? 1L:0L)) /* или #define KB(s) (((s) + 1024L - 1) / 1024L) */ long size; /* общий размер */ long nfiles; /* всего файлов */ long ndirs; /* из них каталогов */ #define WARNING_LIMIT 150L /* подозрительно большой файл */ static int du_touch (char *name, int level, struct stat *st){ long sz; size += (sz = KB(st->st_size)); /* размер файла в Кб. */ nfiles++; #ifndef TREEONLY if( sz >= WARNING_LIMIT ) fprintf(stderr,"\tВнимание! \"%s\" очень большой: %ld Кб.\n", name, sz); #endif /*TREEONLY*/ return SUCCESS; } static int du_enter (char *name, int level, struct stat *st){ #ifndef TREEONLY fprintf( stderr, "Каталог \"%s\"\n", name ); #endif size += KB(st->st_size); /* размер каталога в Кб. */ nfiles++; ++ndirs; return SUCCESS; } long du (char *name){ size = nfiles = ndirs = 0L; walktree(name, du_enter, NULL, du_touch ); return size; } А. Богатырев, 1992-95 - 197 - Си в UNIX /* -------------------------------------------------------------- */ /* Рекурсивное удаление файлов и каталогов */ /* -------------------------------------------------------------- */ int deleted; /* сколько файлов и каталогов удалено */ static int recrm_dir (char *name, int level){ if( rmdir(name) >= 0){ deleted++; return SUCCESS; } fprintf(stderr, "Не могу rmdir '%s'\n", name); return WARNING; } static int recrm_file(char *name, int level, struct stat *st){ if( unlink(name) >= 0){ deleted++; return SUCCESS; } fprintf(stderr, "Не могу rm '%s'\n", name); return WARNING; } int recrmdir(char *name){ int ok_code; deleted = 0; ok_code = walktree(name, NULL, recrm_dir, recrm_file); printf("Удалено %d файлов и каталогов в %s\n", deleted, name); return ok_code; } /* -------------------------------------------------------------- */ /* Поиск файлов с подходящим именем (по шаблону имени) */ /* -------------------------------------------------------------- */ char *find_PATTERN; static int find_check(char *fullname, int level, struct stat *st){ char *basename = strrchr(fullname, '/'); if(basename) basename++; else basename = fullname; if( match(basename, find_PATTERN)) printf("Level#%02d %s\n", level, fullname); if( !strcmp( basename, "core")){ printf("Найден дамп %s, поиск прекращен.\n", fullname); return FAILURE; } return SUCCESS; } void find (char *root, char *pattern){ find_PATTERN = pattern; walktree(root, find_check, NULL, find_check); } А. Богатырев, 1992-95 - 198 - Си в UNIX /* -------------------------------------------------------------- */ #ifndef TREEONLY void main(int argc, char *argv[]){ #ifdef FIND if(argc != 3){ fprintf(stderr, "Arg count\n"); exit(1); } find(argv[1], argv[2]); #else # ifdef RM_REC for(argv++; *argv; argv++) recrmdir(*argv); # else du( argc == 1 ? "." : argv[1] ); printf( "%ld килобайт в %ld файлах.\n", size, nfiles ); printf( "%ld каталогов.\n", ndirs ); # endif #endif exit(0); } #endif /*TREEONLY*/ 6.1.6. Используя предыдущий алгоритм, напишите программу рекурсивного копирования поддерева каталогов в другое место. Для создания новых каталогов используйте систем- ный вызов mkdir(имя_каталога, коды_доступа); 6.1.7. Используя тот же алгоритм, напишите программу удаления каталога, которая уда- ляет все файлы в нем и, рекурсивно, все его подкаталоги. Таким образом, удаляется дерево каталогов. В UNIX подобную операцию выполняет команда rm -r имя_каталога_корня_дерева 6.1.8. Используя все тот же алгоритм обхода, напишите аналог команды find, который будет позволять: - находить все файлы, чьи имена удовлетворяют заданному шаблону (используйте функ- цию match() из главы "Текстовая обработка"); - находить все выполняемые файлы: обычные файлы S_IFREG, у которых (st.st_mode & 0111) != 0 Как уже ясно, следует пользоваться вызовом stat для проверки каждого файла. 6.2. Время в UNIX. 6.2.1. Напишите функцию, переводящую год, месяц, день, часы, минуты и секунды в число секунд, прошедшее до указанного момента с 00 часов 00 минут 00 секунд 1 Января 1970 года. Внимание: результат должен иметь тип long (точнее time_t). Эта функция облегчит вам сравнение двух моментов времени, заданных в общеприня- том "человеческом" формате, поскольку сравнить два long числа гораздо проще, чем сравнивать по очереди годы, затем, если они равны - месяцы, если месяцы равны - даты, и.т.д.; а также облегчит измерение интервала между двумя событиями - он вычисляется просто как разность двух чисел. В системе UNIX время обрабатывается и хранится именно в виде числа секунд; в частности текущее астрономическое время можно узнать системным вызовом #include <sys/types.h> #include <time.h> time_t t = time(NULL); /* time(&t); */ Функция struct tm *tm = localtime( &t ); А. Богатырев, 1992-95 - 199 - Си в UNIX разлагает число секунд на отдельные составляющие, содержащиеся в int-полях структуры: tm_year год (надо прибавлять 1900) tm_yday день в году 0..365 tm_mon номер месяца 0..11 (0 - Январь) tm_mday дата месяца 1..31 tm_wday день недели 0..6 (0 - Воскресенье) tm_hour часы 0..23 tm_min минуты 0..59 tm_sec секунды 0..59 Номера месяца и дня недели начинаются с нуля, чтобы вы могли использовать их в качестве индексов: char *months[] = { "Январь", "Февраль", ..., "Декабрь" }; printf( "%s\n", months[ tm->tm_mon ] ); Пример использования этих функций есть в приложении. Установить время в системе может суперпользователь вызовом stime(&t); 6.2.2. Напишите функцию печати текущего времени в формате ЧЧ:ММ:СС ДД-МЕС-ГГ. Используйте системный вызов time() и функцию localtime(). Существует стандартная функция ctime(), которая печатает время в формате: /* Mon Mar 25 18:56:36 1991 */ #include <stdio.h> #include <time.h> main(){ /* команда date */ time_t t = time(NULL); char *s = ctime(&t); printf("%s", s); } Обратите внимание, что строка s уже содержит на конце символ '\n'. 6.2.3. Структура stat, заполняемая системным вызовом stat(), кроме прочих полей содержит поля типа time_t st_ctime, st_mtime и st_atime - время последнего изменения содержимого I-узла файла, время последнего изменения файла и время последнего доступа к файлу. - Поле st_ctime изменяется (устанавливается равным текущему астрономическому вре- мени) при применении к файлу вызовов creat, chmod, chown, link, unlink, mknod, utime|-, write (т.к. изменяется длина файла); Это поле следует рассматривать как время модификации прав доступа к файлу; - st_mtime - write, creat, mknod, utime; Это поле следует рассматривать как время модификации содержимого файла (данных); - st_atime - read, creat, mknod, utime; Это поле следует рассматривать как время чтения содержимого файла (данных). Модифицируйте функцию typeOf(), чтобы она печатала еще и эти даты. ____________________ |- Время модификации файла можно изменить на текущее астрономическое время и не производя записи в файл. Для этого используется вызов utime(имяФайла, NULL); Он используется для взаимодействия с программой make - в команде touch. Изменить время можно только своему файлу. А. Богатырев, 1992-95 - 200 - Си в UNIX 6.2.4. Напишите аналог команды ls -tm, выдающей список имен файлов текущего ката- лога, отсортированный по убыванию поля st_mtime, то есть недавно модифицированные файлы выдаются первыми. Для каждого прочитанного из каталога имени надо сделать stat; имена файлов и времена следует сохранить в массиве структур, а затем отсортиро- вать его. 6.2.5. Напишите аналогичную программу, сортирующую файлы в порядке возрастания их размера (st_size). 6.2.6. Напишите аналог команды ls -l, выдающий имена файлов каталога и их коды дос- тупа в формате rwxrw-r--. Для получения кодов доступа используйте вызов stat stat( имяФайла, &st); кодыДоступа = st.st_mode & 0777; Для изменения кодов доступа используется вызов chmod(имя_файла, новые_коды); Можно изменять коды доступа, соответствующие битовой маске 0777 | S_ISUID | S_ISGID | S_ISVTX (смотри <sys/stat.h>). Тип файла (см. функцию typeOf) не может быть изменен. Изме- нить коды доступа к файлу может только его владелец. Печатайте еще номер I-узла файла: поле d_ino каталога либо поле st_ino структуры stat. 6.2.7. Вот программа, которая каждые 2 секунды проверяет - не изменилось ли содержи- мое текущего каталога: #include <sys/types.h> #include <sys/stat.h> extern char *ctime(); main(){ time_t last; struct stat st; for( stat(".", &st), last=st.st_mtime; ; sleep(2)){ stat(".", &st); if(last != st.st_mtime){ last = st.st_mtime; printf("Был создан или удален какой-то файл: %s", ctime(&last)); } } } Модифицируйте ее, чтобы она сообщала какое имя (имена) было удалено или создано (для этого надо при запуске программы прочитать и запомнить содержимое каталога, а при обнаружении модификации - перечитать каталог и сравнить его с прежним содержимым). 6.2.8. Напишите по аналогии программу, которая выдает сообщение, если указанный вами файл был кем-то прочитан, записан или удален. Вам следует отслеживать изменение полей st_atime, st_mtime и значение stat() < 0 соответственно. Если файл удален - программа завершается. 6.2.9. Современные UNIX-машины имеют встроенные таймеры (как правило несколько) с довольно высоким разрешением. Некоторые из них могут использоваться как "будильники" с обратным отсчетом времени: в таймер загружается некоторое значение; таймер ведет обратный отсчет, уменьшая загруженный счетчик; как только это время истекает - посы- лается сигнал процессу, загрузившему таймер. А. Богатырев, 1992-95 - 201 - Си в UNIX Вот как, к примеру, выглядит функция задержки в микросекундах (миллионных долях секунды). Примечание: эту функцию не следует использовать вперемежку с функциями sleep и alarm (смотри статью про них ниже, в главе про сигналы). #include <sys/types.h> #include <signal.h> #include <sys/time.h> void do_nothing() {} /* Задержка на usec миллионных долей секунды (микросекунд) */ void usleep(unsigned int usec) { struct itimerval new, old; /* struct itimerval содержит поля: struct timeval it_interval; struct timeval it_value; Где struct timeval содержит поля: long tv_sec; -- число целых секунд long tv_usec; -- число микросекунд */ struct sigaction new_vec, old_vec; if (usec == 0) return; /* Поле tv_sec содержит число целых секунд. Поле tv_usec содержит число микросекунд. it_value - это время, через которое В ПЕРВЫЙ раз таймер "прозвонит", то есть пошлет нашему процессу сигнал SIGALRM. Время, равное нулю, немедленно остановит таймер. it_interval - это интервал времени, который будет загружаться в таймер после каждого "звонка" (но не в первый раз). Время, равное нулю, остановит таймер после его первого "звонка". */ new.it_interval.tv_sec = 0; new.it_interval.tv_usec = 0; new.it_value.tv_sec = usec / 1000000; new.it_value.tv_usec = usec % 1000000; А. Богатырев, 1992-95 - 202 - Си в UNIX /* Сохраняем прежнюю реакцию на сигнал SIGALRM в old_vec, заносим в качестве новой реакции do_nothing() */ new_vec.sa_handler = do_nothing; sigemptyset(&new_vec.sa_mask); new_vec.sa_flags = 0; sighold(SIGALRM); sigaction(SIGALRM, &new_vec, &old_vec); /* Загрузка интервального таймера значением new, начало отсчета. * Прежнее значение спасти в old. * Вместо &old можно также NULL - не спасать. */ setitimer(ITIMER_REAL, &new, &old); /* Ждать прихода сигнала SIGALRM */ sigpause(SIGALRM); /* Восстановить реакцию на SIGALRM */ sigaction(SIGALRM, &old_vec, (struct sigaction *) 0); sigrelse(SIGALRM); /* Восстановить прежние параметры таймера */ setitimer(ITIMER_REAL, &old, (struct itimerval *) 0); } 6.2.10. Второй пример использования таймера - это таймер, отсчитывающий текущее время суток (а также дату). Чтобы получить значение этого таймера используется вызов функции gettimeofday #include <time.h> void main(){ struct timeval timenow; gettimeofday(&timenow, NULL); printf("%u sec, %u msec\n", timenow.tv_sec, timenow.tv_usec ); printf("%s", ctime(&timenow.tv_sec)); exit(0); } Поле tv_sec содержит число секунд, прошедшее с полуночи 1 января 1970 года до данного момента; в чем полностью соответствует системному вызову time. Однако плюс к тому поле tv_usec содержит число миллионных долей текущей секунды (значение этого поля всегда меньше 1000000). 6.2.11. К данному параграфу вернитесь, изучив раздел про fork() и exit(). Каждый процесс может пребывать в двух фазах: системной (внутри тела системного вызова - его выполняет для нас ядро операционной системы) и пользовательской (внутри кода самой программы). Время, затраченное процессом в каждой фазе, может быть измеряно системным вызовом times(). Кроме того, этот вызов позволяет узнать суммарное время, затраченное порожденными процессами (порожденными при помощи fork). Системный вызов заполняет структуру А. Богатырев, 1992-95 - 203 - Си в UNIX struct tms { clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock_t tms_cstime; }; и возвращает значение #include <sys/times.h> struct tms time_buf; clock_t real_time = times(&time_buf); Все времена измеряются в "тиках" - некоторых долях секунды. Число тиков в секунде можно узнать таким системным вызовом (в системе Solaris): #include <unistd.h> clock_t HZ = sysconf(_SC_CLK_TCK); В старых системах, где таймер работал от сети переменного тока, это число получалось равным 60 (60 Герц - частота сети переменного тока). В современных системах это 100. Поля структуры содержат: tms_utime время, затраченное вызывающим процессом в пользовательской фазе. tms_stime время, затраченное вызывающим процессом в системной фазе. tms_cutime время, затраченное порожденными процессами в пользовательской фазе: оно равно сумме всех tms_utime и tms_cutime порожденных процессов (рекурсивное суммирова- ние). tms_cstime время, затраченное порожденными процессами в системной фазе: оно равно сумме всех tms_stime и tms_cstime порожденных процессов (рекурсивное суммирование). real_time время, соответствующее астрономическому времени системы. Имеет смысл мерять только их разность. Вот пример программы: #include <stdio.h> #include <unistd.h> /* _SC_CLK_TCK */ #include <signal.h> /* SIGALRM */ #include <sys/time.h> /* не используется */ #include <sys/times.h> /* struct tms */ struct tms tms_stop, tms_start; clock_t real_stop, real_start; clock_t HZ; /* число ticks в секунде */ А. Богатырев, 1992-95 - 204 - Си в UNIX /* Засечь время момента старта процесса */ void hello(void){ real_start = times(&tms_start); } /* Засечь время окончания процесса */ void bye(int n){ real_stop = times(&tms_stop); #ifdef CRONO /* Разность времен */ tms_stop.tms_utime -= tms_start.tms_utime; tms_stop.tms_stime -= tms_start.tms_stime; #endif /* Распечатать времена */ printf("User time = %g seconds [%lu ticks]\n", tms_stop.tms_utime / (double)HZ, tms_stop.tms_utime); printf("System time = %g seconds [%lu ticks]\n", tms_stop.tms_stime / (double)HZ, tms_stop.tms_stime); printf("Children user time = %g seconds [%lu ticks]\n", tms_stop.tms_cutime / (double)HZ, tms_stop.tms_cutime); printf("Children system time = %g seconds [%lu ticks]\n", tms_stop.tms_cstime / (double)HZ, tms_stop.tms_cstime); printf("Real time = %g seconds [%lu ticks]\n", (real_stop - real_start) / (double)HZ, real_stop - real_start); exit(n); } /* По сигналу SIGALRM - завершить процесс */ void onalarm(int nsig){ printf("Выход #%d ================\n", getpid()); bye(0); } /* Порожденный процесс */ void dochild(int n){ hello(); printf("Старт #%d ================\n", getpid()); signal(SIGALRM, onalarm); /* Заказать сигнал SIGALRM через 1 + n*3 секунд */ alarm(1 + n*3); for(;;){} /* зациклиться в user mode */ } А. Богатырев, 1992-95 - 205 - Си в UNIX #define NCHLD 4 int main(int ac, char *av[]){ int i; /* Узнать число тиков в секунде */ HZ = sysconf(_SC_CLK_TCK); setbuf(stdout, NULL); hello(); for(i=0; i < NCHLD; i++) if(fork() == 0) dochild(i); while(wait(NULL) > 0); printf("Выход MAIN =================\n"); bye(0); return 0; } и ее выдача: Старт #3883 ================ Старт #3884 ================ Старт #3885 ================ Старт #3886 ================ Выход #3883 ================ User time = 0.72 seconds [72 ticks] System time = 0.01 seconds [1 ticks] Children user time = 0 seconds [0 ticks] Children system time = 0 seconds [0 ticks] Real time = 1.01 seconds [101 ticks] Выход #3884 ================ User time = 1.88 seconds [188 ticks] System time = 0.01 seconds [1 ticks] Children user time = 0 seconds [0 ticks] Children system time = 0 seconds [0 ticks] Real time = 4.09 seconds [409 ticks] Выход #3885 ================ User time = 4.41 seconds [441 ticks] System time = 0.01 seconds [1 ticks] Children user time = 0 seconds [0 ticks] Children system time = 0 seconds [0 ticks] Real time = 7.01 seconds [701 ticks] Выход #3886 ================ User time = 8.9 seconds [890 ticks] System time = 0 seconds [0 ticks] Children user time = 0 seconds [0 ticks] Children system time = 0 seconds [0 ticks] Real time = 10.01 seconds [1001 ticks] Выход MAIN ================= User time = 0.01 seconds [1 ticks] System time = 0.04 seconds [4 ticks] Children user time = 15.91 seconds [1591 ticks] Children system time = 0.03 seconds [3 ticks] Real time = 10.41 seconds [1041 ticks] Обратите внимание, что 72+188+441+890=1591 (поле tms_cutime для main). 6.2.12. Еще одна программа: хронометрирование выполнения другой программы. Пример: timer ls -l А. Богатырев, 1992-95 - 206 - Си в UNIX /* Хронометрирование выполнения программы */ #include <stdio.h> #include <unistd.h> #include <sys/times.h> extern errno; typedef struct _timeStamp { clock_t real_time; clock_t cpu_time; clock_t child_time; clock_t child_sys, child_user; } TimeStamp; TimeStamp TIME(){ struct tms tms; TimeStamp st; st.real_time = times(&tms); st.cpu_time = tms.tms_utime + tms.tms_stime + tms.tms_cutime + tms.tms_cstime; st.child_time = tms.tms_cutime + tms.tms_cstime; st.child_sys = tms.tms_cstime; st.child_user = tms.tms_cutime; return st; } void PRTIME(TimeStamp start, TimeStamp stop){ clock_t HZ = sysconf(_SC_CLK_TCK); clock_t real_time = stop.real_time - start.real_time; clock_t cpu_time = stop.cpu_time - start.cpu_time; clock_t child_time = stop.child_time - start.child_time; printf("%g real, %g cpu, %g child (%g user, %g sys), %ld%%\n", real_time / (double)HZ, cpu_time / (double)HZ, child_time / (double)HZ, stop.child_user / (double)HZ, stop.child_sys / (double)HZ, (child_time * 100L) / (real_time ? real_time : 1) ); } А. Богатырев, 1992-95 - 207 - Си в UNIX TimeStamp start, stop; int main(int ac, char *av[]){ char *prog = *av++; if(*av == NULL){ fprintf(stderr, "Usage: %s command [args...]\n", prog); return(1); } start = TIME(); if(fork() == 0){ execvp(av[0], av); perror(av[0]); exit(errno); } while(wait(NULL) > 0); stop = TIME(); PRTIME(start, stop); return(0); } 6.3. Свободное место на диске. 6.3.1. Системный вызов ustat() позволяет узнать количество свободного места в файло- вой системе, содержащей заданный файл (в примере ниже - текущий каталог): #include <sys/types.h> #include <sys/stat.h> #include <ustat.h> struct stat st; struct ustat ust; void main(int ac, char *av[]){ char *file = (ac==1 ? "." : av[1]); if( stat(file, &st) < 0) exit(1); ustat(st.st_dev, &ust); printf("На диске %*.*s\n" "%ld свободных блоков (%ld Кб)\n" "%d свободных I-узлов\n", sizeof ust.f_fname, sizeof ust.f_fname, ust.f_fname, /* название файловой системы (метка) */ ust.f_tfree, /* блоки по 512 байт */ (ust.f_tfree * 512L) / 1024, ust.f_tinode ); } Обратите внимание на запись длинной строки в printf: строки, перечисленные последова- тельно, склеиваются ANSI C компилятором в одну длинную строку: char s[] = "This is" " a line " "of words"; совпадает с char s[] = "This is a line of words"; 6.3.2. Более правильно, однако, пользоваться сисвызовом statvfs - статистика по вир- туальной файловой системе. Рассмотрим его в следующем примере: копирование файла с проверкой на наличие свободного места. А. Богатырев, 1992-95 - 208 - Си в UNIX #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdarg.h> #include <fcntl.h> /* O_RDONLY */ #include <sys/types.h> #include <sys/stat.h> #include <sys/statvfs.h> #include <sys/param.h> /* MAXPATHLEN */ char *progname; /* имя программы */ void error(char *fmt, ...){ va_list args; va_start(args, fmt); fprintf(stderr, "%s: ", progname); vfprintf(stderr, fmt, args); fputc('\n', stderr); va_end(args); } int copyFile(char *to, char *from){ /* куда, откуда */ char newname[MAXPATHLEN+1]; char answer[20]; struct stat stf, stt; int fdin, fdout; int n, code = 0; char iobuf[64 * 1024]; char *dirname = NULL, *s; if((fdin = open(from, O_RDONLY)) < 0){ error("Cannot read %s", from); return (-1); } fstat(fdin, &stf); if((stf.st_mode & S_IFMT) == S_IFDIR){ close(fdin); error("%s is a directory", from); return (-2); } А. Богатырев, 1992-95 - 209 - Си в UNIX if(stat(to, &stt) >= 0){ /* Файл уже существует */ if((stt.st_mode & S_IFMT) == S_IFDIR){ /* И это каталог */ /* Выделить последнюю компоненту пути from */ if((s = strrchr(from, '/')) && s[1]) s++; else s = from; dirname = to; /* Целевой файл - файл в этом каталоге */ sprintf(newname, "%s/%s", to, s); to = newname; if(stat(to, &stt) < 0) goto not_exist; } if(stt.st_dev == stf.st_dev && stt.st_ino == stf.st_ino){ error("%s: cannot copy file to itself", from); return (-3); } switch(stt.st_mode & S_IFMT){ case S_IFBLK: case S_IFCHR: case S_IFIFO: break; default: printf("%s already exists, overwrite ? ", to); fflush(stdout); *answer = '\0'; gets(answer); if(*answer != 'y'){ /* NO */ close(fdin); return (-4); } break; } } А. Богатырев, 1992-95 - 210 - Си в UNIX not_exist: printf("COPY %s TO %s\n", from, to); if((stf.st_mode & S_IFMT) == S_IFREG){ /* Проверка наличия свободного места в каталоге dirname */ struct statvfs fs; char tmpbuf[MAXPATHLEN+1]; if(dirname == NULL){ /* То 'to' - это имя файла, а не каталога */ strcpy(tmpbuf, to); if(s = strrchr(tmpbuf, '/')){ if(*tmpbuf != '/' || s != tmpbuf){ /* Имена "../xxx" * и второй случай: * абсолютные имена не в корне, * то есть не "/" и не "/xxx" */ *s = '\0'; }else{ /* "/" или "/xxx" */ if(s[1]) s[1] = '\0'; } dirname = tmpbuf; } else dirname = "."; } if(statvfs(dirname, &fs) >= 0){ size_t size = (geteuid() == 0 ) ? /* Доступно суперпользователю: байт */ fs.f_frsize * fs.f_bfree : /* Доступно обычному пользователю: байт */ fs.f_frsize * fs.f_bavail; if(size < stf.st_size){ error("Not enough free space on %s: have %lu, need %lu", dirname, size, stf.st_size); close(fdin); return (-5); } } } if((fdout = creat(to, stf.st_mode)) < 0){ error("Can't create %s", to); close(fdin); return (-6); } else { fchmod(fdout, stf.st_mode); fchown(fdout, stf.st_uid, stf.st_gid); } А. Богатырев, 1992-95 - 211 - Си в UNIX while (n = read (fdin, iobuf, sizeof iobuf)) { if(n < 0){ error ("read error"); code = (-7); goto done; } if(write (fdout, iobuf, n) != n) { error ("write error"); code = (-8); goto done; } } done: close (fdin); close (fdout); /* Проверить: соответствует ли результат ожиданиям */ if(stat(to, &stt) >= 0 && (stt.st_mode & S_IFMT) == S_IFREG){ if(stf.st_size < stt.st_size){ error("File has grown at the time of copying"); } else if(stf.st_size > stt.st_size){ error("File too short, target %s removed", to); unlink(to); code = (-9); } } return code; } int main(int argc, char *argv[]){ int i, code = 0; progname = argv[0]; if(argc < 3){ error("Usage: %s from... to", argv[0]); return 1; } for(i=1; i < argc-1; i++) code |= copyFile(argv[argc-1], argv[i]) < 0 ? 1 : 0; return code; } Возвращаемая структура struct statvfs содержит такие поля (в частности): Типа long: f_frsize размер блока f_blocks размер файловой системы в блоках f_bfree свободных блоков (для суперпользователя) f_bavail свободных блоков (для всех остальных) f_files число I-nodes в файловой системе f_ffree свободных I-nodes (для суперпользователя) f_favail свободных I-nodes (для всех остальных) Типа char * f_basetype тип файловой системы: ufs, nfs, ... А. Богатырев, 1992-95 - 212 - Си в UNIX По два значения дано потому, что операционная система резервирует часть файловой сис- темы для использования ТОЛЬКО суперпользователем (чтобы администратор смог распихать файлы в случае переполнения диска, и имел резерв на это). ufs - это UNIX file system из BSD 4.x 6.4. Сигналы. Процессы в UNIX используют много разных механизмов взаимодействия. Одним из них являются сигналы. Сигналы - это асинхронные события. Что это значит? Сначала объясним, что такое синхронные события: я два раза в день подхожу к почтовому ящику и проверяю - нет ли в нем почты (событий). Во-первых, я произвожу опрос - "нет ли для меня события?", в программе это выглядело бы как вызов функции опроса и, может быть, ожидания события. Во-вторых, я знаю, что почта может ко мне прийти, поскольку я подписался на какие-то газеты. То есть я предварительно заказывал эти события. Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожи- дает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС высту- пает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процес- сора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.). Сигналы - это асинхронные события. Они приходят неожиданно, в любой момент вре- мени - вроде телефонного звонка. Кроме того, их не требуется заказывать - сигнал процессу может поступить совсем без повода. Аналогия из жизни такова: человек сидит и пишет письмо. Вдруг его окликают посреди фразы - он отвлекается, отвечает на воп- рос, и вновь продолжает прерванное занятие. Человек не ожидал этого оклика (быть может, он готов к нему, но он не озирался по сторонам специально). Кроме того, сиг- нал мог поступить когда он писал 5-ое предложение, а мог - когда 34-ое. Момент вре- мени, в который произойдет прерывание, не фиксирован. Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в include- файле <signal.h> и имеют вид SIGнечто. Допустимы сигналы с номерами 1..NSIG-1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями: - От другого процесса, который явно посылает его нам вызовом kill(pid, sig); где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользова- телем. - От операционной системы. Система может посылать процессу ряд сигналов, сигнали- зирующих об ошибках, например при обращении программы по несуществующему адресу или при ошибочном номере системного вызова. Такие сигналы обычно прекращают наш процесс. - От пользователя - с клавиатуры терминала можно нажимом некоторых клавиш послать сигналы SIGINT и SIGQUIT. Собственно, сигнал посылается драйвером терминала при получении им с клавиатуры определенных символов. Так можно прервать зациклившу- юся или надоевшую программу. Процесс-получатель должен как-то отреагировать на сигнал. Программа может: А. Богатырев, 1992-95 - 213 - Си в UNIX - проигнорировать сигнал (не ответить на звонок); - перехватить сигнал (снять трубку), выполнить какие-то действия, затем продолжить прерванное занятие; - быть убитой сигналом (звонок был подкреплен броском гранаты в окно); В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом signal: #include <signal.h> void (*signal(int sig, void (*react)() )) (); Параметр react может иметь значение: SIG_IGN сигнал sig будет отныне игнорироваться. Некоторые сигналы (например SIGKILL) невозможно перехватить или проигнорировать. SIG_DFL восстановить реакцию по умолчанию (обычно - смерть получателя). имя_функции Например void fr(gotsig){ ..... } /* обработчик */ ... signal (sig, fr); ... /* задание реакции */ Тогда при получении сигнала sig будет вызвана функция fr, в которую в качестве аргумента системой будет передан номер сигнала, действительно вызвавшего ее - gotsig==sig. Это полезно, т.к. можно задать одну и ту же функцию в качестве реакции для нескольких сигналов: ... signal (sig1, fr); signal(sig2, fr); ... После возврата из функции fr() программа продолжится с прерванного места. Перед вызовом функции-обработчика реакция автоматически сбрасывается в реакцию по умолчанию SIG_DFL, а после выхода из обработчика снова восстанавливается в fr. Это значит, что во время работы функции-обработчика может прийти сигнал, который убьет программу. Приведем список некоторых сигналов; полное описание посмотрите в документации. Колонки таблицы: G - может быть перехвачен; D - по умолчанию убивает процесс (k), игнорируется (i); C - образуется дамп памяти процесса: файл core, который затем может быть исследован отладчиком adb; F - реакция на сигнал сбрасывается; S - посылается обычно системой, а не явно. сигнал G D C F S смысл SIGTERM + k - + - завершить процесс SIGKILL - k - + - убить процесс SIGINT + k - + - прерывание с клавиш SIGQUIT + k + + - прерывание с клавиш SIGALRM + k - + + будильник SIGILL + k + - + запрещенная команда SIGBUS + k + + + обращение по неверному SIGSEGV + k + + + адресу SIGUSR1, USR2 + i - + - пользовательские SIGCLD + i - + + смерть потомка - Сигнал SIGILL используется иногда для эмуляции команд с плавающей точкой, что происходит примерно так: при обнаружении "запрещенной" команды для отсутствую- щего процессора "плавающей" арифметики аппаратура дает прерывание и система посылает процессу сигнал SIGILL. По сигналу вызывается функция-эмулятор плаваю- щей арифметики (подключаемая к выполняемому файлу автоматически), которая и обрабатывает требуемую команду. Это может происходить много раз, именно поэтому А. Богатырев, 1992-95 - 214 - Си в UNIX реакция на этот сигнал не сбрасывается. - SIGALRM посылается в результате его заказа вызовом alarm() (см. ниже). - Сигнал SIGCLD посылается процессу-родителю при выполнении процессом-потомком сисвызова exit (или при смерти вследствие получения сигнала). Обычно процесс- родитель при получении такого сигнала (если он его заказывал) реагирует, выпол- няя в обработчике сигнала вызов wait (см. ниже). По-умолчанию этот сигнал игно- рируется. - Реакция SIG_IGN не сбрасывается в SIG_DFL при приходе сигнала, т.е. сигнал игно- рируется постоянно. - Вызов signal возвращает старое значение реакции, которое может быть запомнено в переменную вида void (*f)(); а потом восстановлено. - Синхронное ожидание (сисвызов) может иногда быть прервано асинхронным событием (сигналом), но об этом ниже. Некоторые версии UNIX предоставляют более развитые средства работы с сигналами. Опишем некоторые из средств, имеющихся в BSD (в других системах они могут быть смоде- лированы другими способами). Пусть у нас в программе есть "критическая секция", во время выполнения которой приход сигналов нежелателен. Мы можем "заморозить" (заблокировать) сигнал, отложив момент его поступления до "разморозки": | sighold(sig); заблокировать сигнал | : КРИТИЧЕСКАЯ :<---процессу послан сигнал sig, СЕКЦИЯ : но он не вызывает реакцию немедленно, | : а "висит", ожидая разрешения. | : sigrelse(sig); разблокировать |<----------- sig | накопившиеся сигналы доходят, | вызывается реакция. Если во время блокировки процессу было послано несколько одинаковых сигналов sig, то при разблокировании поступит только один. Поступление сигналов во время блокировки просто отмечается в специальной битовой шкале в паспорте процесса (примерно так): mask |= (1 << (sig - 1)); и при разблокировании сигнала sig, если соответствующий бит выставлен, то приходит один такой сигнал (система вызывает функцию реакции). То есть sighold заставляет приходящие сигналы "накапливаться" в специальной маске, вместо того, чтобы немедленно вызывать реакцию на них. А sigrelse разрешает "нако- пившимся" сигналам (если они есть) прийти и вызывает реакцию на них. Функция sigset(sig, react); аналогична функции signal, за исключением того, что на время работы обработчика сиг- нала react, приход сигнала sig блокируется; то есть перед вызовом react как бы дела- ется sighold, а при выходе из обработчика - sigrelse. Это значит, что если во время работы обработчика сигнала придет такой же сигнал, то программа не будет убита, а "запомнит" пришедший сигнал, и обработчик будет вызван повторно (когда сработает sigrelse). Функция sigpause(sig); вызывается внутри "рамки" sighold(sig); ... sigpause(sig); ... sigrelse(sig); А. Богатырев, 1992-95 - 215 - Си в UNIX и вызывает задержку выполнения процесса до прихода сигнала sig. Функция разрешает приход сигнала sig (обычно на него должна быть задана реакция при помощи sigset), и "засыпает" до прихода сигнала sig. В UNIX стандарта POSIX для управления сигналами есть вызовы sigaction, sigproc- mask, sigpending, sigsuspend. Посмотрите в документацию! 6.4.1. Напишите программу, выдающую на экран файл /etc/termcap. Перехватывайте сиг- нал SIGINT, при получении сигнала запрашивайте "Продолжать?". По ответу 'y' - про- должить выдачу; по 'n' - завершить программу; по 'r' - начать выдавать файл с начала: lseek(fd,0L,0). Не забудьте заново переустановить реакцию на SIGINT, поскольку после получения сигнала реакция автоматически сбрасывается. #include <signal.h> void onintr(sig){ /* sig - номер сигнала */ signal (sig, onintr); /* восстановить реакцию */ ... запрос и действия ... } main(){ signal (SIGINT, onintr); ... } Сигнал прерывания можно игнорировать. Это делается так: signal (SIGINT, SIG_IGN); Такую программу нельзя прервать с клавиатуры. Напомним, что реакция SIG_IGN сохраня- ется при приходе сигнала. 6.4.2. Системный вызов, находящийся в состоянии ожидания какого-то события (read ждущий нажатия кнопки на клавиатуре, wait ждущий окончания процесса-потомка, и.т.п.), может быть прерван сигналом. При этом сисвызов вернет значение "ошибка" (-1) и errno станет равно EINTR. Это позволяет нам писать системные вызовы с выставлением тайма- ута: если событие не происходит в течение заданного времени, то завершить ожидание и прервать сисвызов. Для этой цели используется вызов alarm(sec), заказывающий посылку сигнала SIGALRM нашей программе через целое число sec секунд (0 - отменяет заказ): #include <signal.h> void (*oldaction)(); int alarmed; /* прозвонил будильник */ void onalarm(nsig){ alarmed++; } ... /* установить реакцию на сигнал */ oldaction = signal (SIGALRM, onalarm); /* заказать будильник через TIMEOUT сек. */ alarmed = 0; alarm ( TIMEOUT /* sec */ ); sys_call(...); /* ждет события */ // если нас сбил сигнал, то по сигналу будет // еще вызвана реакция на него - onalarm if(alarmed){ // событие так и не произошло. // вызов прерван сигналом т.к. истекло время. }else{ alarm(0); /* отменить заказ сигнала */ // событие произошло, сисвызов успел // завершиться до истечения времени. } signal (SIGALRM, oldaction); Напишите программу, которая ожидает ввода с клавиатуры в течение 10 секунд. Если ничего не введено - печатает "Нет ввода", иначе - печатает "Спасибо". Для ввода можно использовать как вызов read, так и функцию gets (или getchar), поскольку А. Богатырев, 1992-95 - 216 - Си в UNIX функция эта все равно внутри себя издает системный вызов read. Исследуйте, какое значение возвращает fgets (gets) в случае прерывания ее системным вызовом. /* Копирование стандартного ввода на стандартный вывод * с установленным тайм-аутом. * Это позволяет использовать программу для чтения из FIFO-файлов * и с клавиатуры. * Небольшая модификация позволяет использовать программу * для копирования "растущего" файла (т.е. такого, который в * настоящий момент еще продолжает записываться). * Замечание: * В ДЕМОС-2.2 сигнал НЕ сбивает чтение из FIFO-файла, * а получение сигнала откладывается до выхода из read() * по успешному чтению информации. Пользуйтесь open()-ом * с флагом O_NDELAY, чтобы получить требуемый эффект. * * Вызов: a.out /dev/tty * * По мотивам книги М.Дансмура и Г.Дейвиса. */ #define WAIT_TIME 5 /* ждать 5 секунд */ #define MAX_TRYS 5 /* максимум 5 попыток */ #define BSIZE 256 #define STDIN 0 /* дескриптор стандартного ввода */ #define STDOUT 1 /* дескриптор стандартного вывода */ #include <signal.h> #include <errno.h> #