char *string = "абвгдежзиклмноп"; setlocale(LC_ALL, ""); for(;c = *string;string++){ #ifdef DEBUG printf("%c %d %d\n", *string, *string, c); #endif if(isprint(c)) printf("%c - печатный символ\n", c); } return 0; } Эта программа неожиданно печатает % a.out в - печатный символ з - печатный символ И все. В чем дело??? Рассмотрим к примеру символ 'г'. Его код '\307'. В операторе c = *string; Символ c получает значение -57 (десятичное), которое ОТРИЦАТЕЛЬНО. В системном файле /usr/include/ctype.h макрос isprint определен так: #define isprint(c) ((_ctype + 1)[c] & (_P|_U|_L|_N|_B)) И значение c используется в нашем случае как отрицательный индекс в массиве, ибо индекс приводится к типу int (signed). Откуда теперь извлекается значение флагов - нам неизвестно; можно только с уверенностью сказать, что НЕ из массива _ctype. А. Богатырев, 1992-95 - 129 - Си в UNIX Проблему решает либо использование isprint(c & 0xFF) либо isprint((unsigned char) c) либо объявление в нашем примере unsigned char c; В первом случае мы явно приводим signed к unsigned битовой операцией, обнуляя лишние биты. Во втором и третьем - unsigned char расширяется в unsigned int, который оста- нется положительным. Вероятно, второй путь предпочтительнее. 3.12. Итак, снова напомним, что русские буквы char, а не unsigned char дают отрица- тельные индексы в массиве. char c = 'г'; int x[256]; ...x[c]... /* индекс < 0 */ ...x['г']... Поэтому байтовые индексы должны быть либо unsigned char, либо & 0xFF. Как в следую- щем примере: /* Программа преобразования символов в файле: транслитерация tr abcd prst заменяет строки xxxxdbcaxxxx -> xxxxtrspxxxx По мотивам книги М.Дансмура и Г.Дейвиса. */ #include <stdio.h> #define ASCII 256 /* число букв в алфавите ASCII */ /* BUFSIZ определено в stdio.h */ char mt[ ASCII ]; /* таблица перекодировки */ /* начальная разметка таблицы */ void mtinit(){ register int i; for( i=0; i < ASCII; i++ ) mt[i] = (char) i; } А. Богатырев, 1992-95 - 130 - Си в UNIX int main(int argc, char *argv[]) { register char *tin, *tout; /* unsigned char */ char buffer[ BUFSIZ ]; if( argc != 3 ){ fprintf( stderr, "Вызов: %s что наЧто\n", argv[0] ); return(1); } tin = argv[1]; tout = argv[2]; if( strlen(tin) != strlen(tout)){ fprintf( stderr, "строки разной длины\n" ); return(2); } mtinit(); do{ mt[ (*tin++) & 0xFF ] = *tout++; /* *tin - имеет тип char. * & 0xFF подавляет расширение знака */ } while( *tin ); tout = mt; while( fgets( buffer, BUFSIZ, stdin ) != NULL ){ for( tin = buffer; *tin; tin++ ) *tin = tout[ *tin & 0xFF ]; fputs( buffer, stdout ); } return(0); } 3.13. int main(int ac, char *av[]){ char c = 'г'; if('a' <= c && c < 256) printf("Это одна буква.\n"); return 0; } Увы, эта программа не печатает НИЧЕГО. Просто потому, что signed char в сравнении (в операторе if) приводится к типу int. А как целое число - русская буква отрицательна. Снова решением является либо использование везде (c & 0xFF), либо объявление unsigned char c. В частности, этот пример показывает, что НЕЛЬЗЯ просто так сравни- вать две переменные типа char. Нужно принимать предохранительные меры по подавлению расширения знака: if((ch1 & 0xFF) < (ch2 & 0xFF))...; Для unsigned char такой проблемы не будет. 3.14. Почему неверно: А. Богатырев, 1992-95 - 131 - Си в UNIX #include <stdio.h> main(){ char c; while((c = getchar()) != EOF) putchar(c); } Потому что c описано как char, в то время как EOF - значение типа int равное (-1). Русская буква "Большой твердый знак" в кодировке КОИ-8 имеет код '\377' (0xFF). Если мы подадим на вход этой программе эту букву, то в сравнении signed char со зна- чением знакового целого EOF, c будет приведено тоже к знаковому целому - расширением знака. 0xFF превратится в (-1), что означает, что поступил символ EOF. Сюрприз!!! Посему данная программа будет делать вид, что в любом файле с большим русским твердым знаком после этого знака (и включая его) дальше ничего нет. Что есть досадное заблуж- дение. Решением служит ПРАВИЛЬНОЕ объявление int c. 3.15. Изучите поведение программы #define TYPE char void f(TYPE c){ if(c == 'й') printf("Это буква й\n"); printf("c=%c c=\\%03o c=%03d c=0x%0X\n", c, c, c, c); } int main(){ f('г'); f('й'); f('z'); f('Z'); return 0; } когда TYPE определено как char, unsigned char, int. Объясните поведение. Выдачи в этих трех случаях таковы (int == 32 бита): c=г c=\37777777707 c=-57 c=0xFFFFFFC7 Это буква й c=й c=\37777777712 c=-54 c=0xFFFFFFCA c=z c=\172 c=122 c=0x7A c=Z c=\132 c=090 c=0x5A c=г c=\307 c=199 c=0xC7 c=й c=\312 c=202 c=0xCA c=z c=\172 c=122 c=0x7A c=Z c=\132 c=090 c=0x5A и снова как 1 случай. Рассмотрите альтернативу if(c == (unsigned char) 'й') printf("Это буква й\n"); где предполагается, что знак у русских букв и у c НЕ расширяется. В данном случае фраза 'Это буква й' не печатается ни с типом char, ни с типом int, поскольку в срав- нении c приводится к типу signed int расширением знакового бита (который равен 1). Слева получается отрицательное число! В таких случаях вновь следует писать if((unsigned char)c == (unsigned char)'й') printf("Это буква й\n"); А. Богатырев, 1992-95 - 132 - Си в UNIX 3.16. Обычно возникают проблемы при написании функций с переменным числом аргумен- тов. В языке Си эта проблема решается использованием макросов va_args, не зависящих от соглашений о вызовах функций на данной машине, и использующих эти макросы специ- альных функций. Есть два стиля оформления таких программ: с использованием <varargs.h> и <stdarg.h>. Первый был продемонстрирован в первой главе на примере функции poly(). Для иллюстрации второго приведем пример функции трассировки, записы- вающей собщение в файл: #include <stdio.h> #include <stdarg.h> void trace(char *fmt, ...) { va_list args; static FILE *fp = NULL; if(fp == NULL){ if((fp = fopen("TRACE", "w")) == NULL) return; } va_start(args, fmt); /* второй аргумент: арг-т после которого * в заголовке функции идет ... */ vfprintf(fp, fmt, args); /* библиотечная ф-ция */ fflush(fp); /* вытолкнуть сообщение в файл */ va_end(args); } main(){ trace( "%s\n", "Go home."); trace( "%d %d\n", 12, 34); } Символ `...' (троеточие) в заголовке функции обозначает переменный (возможно пустой) список аргументов. Он должен быть самым последним, следуя за всеми обязательными аргументами функции. Макрос va_arg(args,type), извлекающий из переменного списка аргументов `...' очередное значение типа type, одинаков в обоех моделях. Функция vfprintf может быть написана через функцию vsprintf (в действительности обе функции - стандартные): int vfprintf(FILE *fp, const char *fmt, va_list args){ /*static*/ char buffer[1024]; int res; res = vsprintf(buffer, fmt, args); fputs(buffer, fp); return res; } Функция vsprintf(str,fmt,args); аналогична функции sprintf(str,fmt,...) - записывает преобразованную по формату строку в байтовый массив str, но используется в контексте, подобном приведенному. В конец сформированной строки sprintf записывает '\0'. 3.17. Напишите функцию printf, понимающую форматы %c (буква), %d (целое), %o (вось- меричное), %x (шестнадцатеричное), %b (двоичное), %r (римское), %s (строка), %ld (длинное целое). Ответ смотри в приложении. 3.18. Для того, чтобы один и тот же исходный текст программы транслировался на раз- ных машинах (в разных системах), приходится выделять в программе системно-зависимые части. Такие части должны по-разному выглядеть на разных машинах, поэтому их оформ- ляют в виде так называемых "условно компилируемых" частей: #ifdef XX ... вариант1 #else ... вариант2 #endif А. Богатырев, 1992-95 - 133 - Си в UNIX Эта директива препроцессора ведет себя следующим образом: если макрос с именем XX был определен #define XX то в программу подставляется вариант1, если же нет - вариант2. Оператор #else не обя- зателен - при его отсутствии вариант2 пуст. Существует также оператор #ifndef, кото- рый подставляет вариант1 если макрос XX не определен. Есть еще и оператор #elif - else if: #ifdef макро1 ... #elif макро2 ... #else ... #endif Определить макрос можно не только при помощи #define, но и при помощи ключа компиля- тора, так cc -DXX file.c ... соответствует включению в начало файла file.c директивы #define XX А для программы main(){ #ifdef XX printf( "XX = %d\n", XX); #else printf( "XX undefined\n"); #endif } ключ cc -D"XX=2" file.c ... эквивалентен заданию директивы #define XX 2 Что будет, если совсем не задать ключ -D в данном примере? Этот прием используется в частности в тех случаях, когда какие-то стандартные типы или функции в данной системе носят другие названия: cc -Dvoid=int ... cc -Dstrchr=index ... В некоторых системах компилятор автоматически определяет специальные макросы: так компиляторы в UNIX неявно подставляют один из ключей (или несколько сразу): -DM_UNIX -DM_XENIX -Dunix -DM_SYSV -D__SVR4 -DUSG ... бывают и другие А. Богатырев, 1992-95 - 134 - Си в UNIX Это позволяет программе "узнать", что ее компилируют для системы UNIX. Более под- робно про это написано в документации по команде cc. 3.19. Оператор #ifdef применяется в include-файлах, чтобы исключить повторное вклю- чение одного и того же файла. Пусть файлы aa.h и bb.h содержат aa.h bb.h #include "cc.h" #include "cc.h" typedef unsigned long ulong; typedef int cnt_t; А файлы cc.h и 00.c содержат cc.h 00.c ... #include "aa.h" struct II { int x, y; }; #include "bb.h" ... main(){ ... } В этом случае текст файла cc.h будет вставлен в 00.c дважды: из aa.h и из bb.h. При компиляции 00.c компилятор сообщит "Переопределение структуры II". Чтобы include- файл не подставлялся еще раз, если он уже однажды был включен, придуман следующий прием - следует оформлять файлы включений так: /* файл cc.h */ #ifndef _CC_H # define _CC_H /* определяется при первом включении */ ... struct II { int x, y; }; ... #endif /* _CC_H */ Второе и последующие включения такого файла будут подставлять пустое место, что и требуется. Для файла <sys/types.h> было бы использовано макроопределение _SYS_TYPES_H. 3.20. Любой макрос можно отменить, написав директиву #undef имяМакро Пример: #include <stdio.h> #undef M_UNIX #undef M_SYSV main() { putchar('!'); #undef putchar #define putchar(c) printf( "Буква '%c'\n", c); putchar('?'); #if defined(M_UNIX) || defined(M_SYSV) /* или просто #if M_UNIX */ printf("Это UNIX\n"); #else printf("Это не UNIX\n"); #endif /* UNIX */ } Обычно #undef используется именно для переопределения макроса, как putchar в этом примере (дело в том, что putchar - это макрос из <stdio.h>). Директива #if, использованная нами, является расширением оператора #ifdef и подставляет текст если выполнено указанное условие: А. Богатырев, 1992-95 - 135 - Си в UNIX #if defined(MACRO) /* равно #ifdef(MACRO) */ #if !defined(MACRO) /* равно #ifndef(MACRO) */ #if VALUE > 15 /* если целая константа #define VALUE 25 больше 15 (==, !=, <=, ...) */ #if COND1 || COND2 /* если верно любое из условий */ #if COND1 && COND2 /* если верны оба условия */ Директива #if допускает использование в качестве аргумента довольно сложных выраже- ний, вроде #if !defined(M1) && (defined(M2) || defined(M3)) 3.21. Условная компиляция может использоваться для трассировки программ: #ifdef DEBUG # define DEBUGF(body) \ { \ body; \ } #else # define DEBUGF(body) #endif int f(int x){ return x*x; } int main(int ac, char *av[]){ int x = 21; DEBUGF(x = f(x); printf("%s equals to %d\n", "x", x)); printf("x=%d\n", x); } При компиляции cc -DDEBUG file.c в выходном потоке программы будет присутствовать отладочная выдача. При компиляции без -DDEBUG этой выдачи не будет. 3.22. В языке C++ (развитие языка Си) слова class, delete, friend, new, operator, overload, template, public, private, protected, this, virtual являются зарезервиро- ванными (ключевыми). Это может вызвать небольшую проблему при переносе текста прог- раммы на Си в систему программирования C++, например: #include <termio.h> ... int fd_tty = 2; /* stderr */ struct termio old, new; ioctl (fd_tty, TCGETA, &old); new = old; new.c_lflag |= ECHO | ICANON; ioctl (fd_tty, TCSETAW, &new); ... Строки, содержащие имя переменной (или функции) new, окажутся неправильными в C++. Проще всего эта проблема решается переименованием переменной (или функции). Чтобы не производить правки во всем тексте, достаточно переопределить имя при помощи директивы define: А. Богатырев, 1992-95 - 136 - Си в UNIX #define new new_modes ... старый текст ... #undef new При переносе программы на Си в C++ следует также учесть, что в C++ для каждой функции должен быть задан прототип, прежде чем эта функция будет использована (Си позволяет опускать прототипы для многих функций, особенно возвращающих значения типов int или void). А. Богатырев, 1992-95 - 137 - Си в UNIX 4. Работа с файлами. Файлы представляют собой области памяти на внешнем носителе (как правило магнит- ном диске), предназначенные для: - хранения данных, превосходящих по объему память компьютера (меньше, разумеется, тоже можно); - долговременного хранения информации (она сохраняется при выключении машины). В UNIX и в MS DOS файлы не имеют предопределенной структуры и представляют собой просто линейные массивы байт. Если вы хотите задать некоторую структуру хранимой информации - вы должны позаботиться об этом в своей программе сами. Файлы отличаются от обычных массивов тем, что - они могут изменять свой размер; - обращение к элементам этих массивов производится не при помощи операции индекса- ции [], а при помощи специальных системных вызовов и функций; - доступ к элементам файла происходит в так называемой "позиции чтения/записи", которая автоматически продвигается при операциях чтения/записи, т.е. файл прос- матривается последовательно. Есть, правда, функции для произвольного изменения этой позиции. Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и простых файлов. Об этом и о системе именования файлов прочитайте в документации по UNIX. 4.1. Для работы с каким-либо файлом наша программа должна открыть этот файл - уста- новить связь между именем файла и некоторой переменной в программе. При открытии файла в ядре операционной системы выделяется "связующая" структура file "открытый файл", содержащая: f_offset: указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи; f_flag: режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги; f_inode: расположение файла на диске (в UNIX - в виде ссылки на I-узел файла|-); и кое-что еще. У каждого процесса имеется таблица открытых им файлов - это массив ссылок на упомянутые "связующие" структуры|=. При открытии файла в этой таблице ищется ____________________ |- I-узел (I-node, индексный узел) - своеобразный "паспорт", который есть у каждого файла (в том числе и каталога). В нем содержатся: - длина файла long di_size; - номер владельца файла int di_uid; - коды доступа и тип файла ushort di_mode; - время создания и последней модификации time_t di_ctime, di_mtime; - начало таблицы блоков файла char di_addr[...]; - количество имен файла short di_nlink; и.т.п. Содержимое некоторых полей этого паспорта можно узнать вызовом stat(). Все I-узлы собраны в единую область в начале файловой системы - так называемый I-файл. Все I- узлы пронумерованы, начиная с номера 1. Корневой каталог (файл с именем "/") как правило имеет I-узел номер 2. |= У каждого процесса в UNIX также есть свой "паспорт". Часть этого паспорта нахо- дится в таблице процессов в ядре ОС, а часть - "приклеена" к самому процессу, однако не доступна из программы непосредственно. Эта вторая часть паспорта носит название "u-area" или структура user. В нее, в частности, входят таблица открытых процессом файлов А. Богатырев, 1992-95 - 138 - Си в UNIX свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого "дескриптора файла". При закрытии файла связная структура в ядре уничтожается, ячейка в таблице счи- тается свободной, т.е. связь программы и файла разрывается. Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных програм- мах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь: несколько или один процессов могут открыть один и тот же файл одновременно несколько раз. При этом будет создано несколько "связующих" структур (по одной для каждого открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуа- ция, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание вызова dup2. fd u_ofile[] struct file 0 ## ------------- 1---##---------------->| f_flag | 2 ## | f_count=3 | 3---##---------------->| f_inode---------* ... ## *-------------->| f_offset | | процесс1 | ------!------ | | ! V 0 ## | struct file ! struct inode 1 ## | ------------- ! ------------- 2---##-* | f_flag | ! | i_count=2 | 3---##--->| f_count=1 | ! | i_addr[]----* ... ## | f_inode----------!-->| ... | | адреса процесс2 | f_offset | ! ------------- | блоков -------!----- *=========* | файла ! ! V 0 ! указатели R/W ! i_size-1 @@@@@@@@@@@!@@@@@@@@@@@@@@@@@@@@@!@@@@@@ файл на диске /* открыть файл */ int fd = open(char имя_файла[], int как_открыть); ... /* какие-то операции с файлом */ close(fd); /* закрыть */ Параметр как_открыть: #include <fcntl.h> O_RDONLY - только для чтения. O_WRONLY - только для записи. O_RDWR - для чтения и записи. O_APPEND - иногда используется вместе с открытием для записи, "добавление" в файл: O_WRONLY|O_APPEND, O_RDWR|O_APPEND Если файл еще не существовал, то его нельзя открыть: open вернет значение (-1), ____________________ struct file *u_ofile[NOFILE]; ссылка на I-узел текущего каталога struct inode *u_cdir; а также ссылка на часть паспорта в таблице процессов struct proc *u_procp; А. Богатырев, 1992-95 - 139 - Си в UNIX сигнализирующее об ошибке. В этом случае файл надо создать: int fd = creat(char имя_файла[], int коды_доступа); Дескриптор fd будет открыт для записи в этот новый пустой файл. Если же файл уже существовал, creat опустошает его, т.е. уничтожает его прежнее содержимое и делает его длину равной 0L байт. Коды_доступа задают права пользователей на доступ к файлу. Это число задает битовую шкалу из 9и бит, соответствующих строке биты: 876 543 210 rwx rwx rwx r - можно читать файл w - можно записывать в файл x - можно выполнять программу из этого файла Первая группа - эта права владельца файла, вторая - членов его группы, третяя - всех прочих. Эти коды для владельца файла имеют еще и мнемонические имена (используемые в вызове stat): #include <sys/stat.h> /* Там определено: */ #define S_IREAD 0400 #define S_IWRITE 0200 #define S_IEXEC 0100 Подробности - в руководствах по системе UNIX. Отметим в частности, что open() может вернуть код ошибки fd < 0 не только в случае, когда файл не существует (errno==ENOENT), но и в случае, когда вам не разрешен соответствующий доступ к этому файлу (errno==EACCES; про переменную кода ошибки errno см. в главе "Взаимодействие с UNIX"). Вызов creat - это просто разновидность вызова open в форме fd = open( имя_файла, O_WRONLY|O_TRUNC|O_CREAT, коды_доступа); O_TRUNC означает, что если файл уже существует, то он должен быть опустошен при откры- тии. Коды доступа и владелец не изменяются. O_CREAT означает, что файл должен быть создан, если его не было (без этого флага файл не создастся, а open вернет fd < 0). Этот флаг требует задания третьего аргумента коды_доступа|-. Если файл уже существует - этот флаг не имеет никакого эффекта, но зато вступает в действие O_TRUNC. Существует также флаг O_EXCL который может использоваться совместно с O_CREAT. Он делает следующее: если файл уже существует, open вернет код ошибки (errno==EEXIST). Если файл не ____________________ |- Заметим, что на самом деле коды доступа у нового файла будут равны di_mode = (коды_доступа & ~u_cmask) | IFREG; (для каталога вместо IFREG будет IFDIR), где маска u_cmask задается системным вызовом umask(u_cmask); (вызов выдает прежнее значение маски) и в дальнейшем наследуется всеми потомками дан- ного процесса (она хранится в u-area процесса). Эта маска позволяет запретить доступ к определенным операциям для всех создаваемых нами файлов, несмотря на явно заданные коды доступа, например umask(0077); /* ???------ */ делает значащими только первые 3 бита кодов доступа (для владельца файла). Остальные биты будут равны нулю. Все это относится и к созданию каталогов вызовом mkdir. А. Богатырев, 1992-95 - 140 - Си в UNIX существовал - срабатывает O_CREAT и файл создается. Это позволяет предохранить уже существующие файлы от уничтожения. Файл удаляется при помощи int unlink(char имя_файла[]); У каждой программы по умолчанию открыты три первых дескриптора, обычно связанные 0 - с клавиатурой (для чтения) 1 - с дисплеем (выдача результатов) 2 - с дисплеем (выдача сообщений об ошибках) Если при вызове close(fd) дескриптор fd не соответствует открытому файлу (не был отк- рыт) - ничего не происходит. Часто используется такая метафора: если представлять себе файлы как книжки (только чтение) и блокноты (чтение и запись), стоящие на полке, то открытие файла - это выбор блокнота по заглавию на его обложке и открытие обложки (на первой стра- нице). Теперь можно читать записи, дописывать, вычеркивать и править записи в сере- дине, листать книжку! Страницы можно сопоставить блокам файла (см. ниже), а "полку" с книжками - каталогу. 4.2. Напишите программу, которая копирует содержимое одного файла в другой (новый) файл. При этом используйте системные вызовы чтения и записи read и write. Эти сис- вызовы пересылают массивы байт из памяти в файл и наоборот. Но любую переменную можно рассматривать как массив байт, если забыть о структуре данных в переменной! Читайте и записывайте файлы большими кусками, кратными 512 байтам. Это уменьшит число обращений к диску. Схема: char buffer[512]; int n; int fd_inp, fd_outp; ... while((n = read (fd_inp, buffer, sizeof buffer)) > 0) write(fd_outp, buffer, n); Приведем несколько примеров использования write: char c = 'a'; int i = 13, j = 15; char s[20] = "foobar"; char p[] = "FOOBAR"; struct { int x, y; } a = { 666, 999 }; /* создаем файл с доступом rw-r--r-- */ int fd = creat("aFile", 0644); write(fd, &c, 1); write(fd, &i, sizeof i); write(fd, &j, sizeof(int)); write(fd, s, strlen(s)); write(fd, &a, sizeof a); write(fd, p, sizeof(p) - 1); close(fd); Обратите внимание на такие моменты: - При использовании write() и read() надо передавать АДРЕС данного, которое мы хотим записать в файл (места, куда мы хотим прочитать данные из файла). - Операции read и write возвращают число действительно прочитанных/записанных байт (при записи оно может быть меньше указанного нами, если на диске не хватает места; при чтении - если от позиции чтения до конца файла содержится меньше информации, чем мы затребовали). - Операции read/write продвигают указатель чтения/записи RWptr += прочитанное_или_записанное_число_байт; При открытии файла указатель стоит на начале файла: RWptr=0. При записи файл А. Богатырев, 1992-95 - 141 - Си в UNIX если надо автоматически увеличивает свой размер. При чтении - если мы достигнем конца файла, то read будет возвращать "прочитано 0 байт" (т.е. при чтении указа- тель чтения не может стать больше размера файла). - Аргумент сколькоБайт имеет тип unsigned, а не просто int: int n = read (int fd, char *адрес, unsigned сколькоБайт); int n = write(int fd, char *адрес, unsigned сколькоБайт); Приведем упрощенные схемы логики этих сисвызовов, когда они работают с обычным диско- вым файлом (в UNIX устройства тоже выглядят для программ как файлы, но иногда с осо- быми свойствами): 4.2.1. m = write(fd, addr, n); если( ФАЙЛ[fd] не открыт на запись) то вернуть (-1); если(n == 0) то вернуть 0; если( ФАЙЛ[fd] открыт на запись с флагом O_APPEND ) то RWptr = длина_файла; /* т.е. встать на конец файла */ если( RWptr > длина_файла ) то заполнить нулями байты файла в интервале ФАЙЛ[fd][ длина_файла..RWptr-1 ] = '\0'; скопировать байты из памяти процесса в файл ФАЙЛ[fd][ RWptr..RWptr+n-1 ] = addr[ 0..n-1 ]; отводя на диске новые блоки, если надо RWptr += n; если( RWptr > длина_файла ) то длина_файла = RWptr; вернуть n; 4.2.2. m = read(fd, addr, n); если( ФАЙЛ[fd] не открыт на чтение) то вернуть (-1); если( RWptr >= длина_файла ) то вернуть 0; m = MIN( n, длина_файла - RWptr ); скопировать байты из файла в память процесса addr[ 0..m-1 ] = ФАЙЛ[fd][ RWptr..RWptr+m-1 ]; RWptr += m; вернуть m; 4.3. Найдите ошибки в фрагменте программы: #define STDOUT 1 /* дескриптор стандартного вывода */ int i; static char s[20] = "hi\n"; char c = '\n'; struct a{ int x,y; char ss[5]; } po; scanf( "%d%d%d%s%s", i, po.x, po.y, s, po.ss); write( STDOUT, s, strlen(s)); write( STDOUT, c, 1 ); /* записать 1 байт */ Ответ: в функции scanf перед аргументом i должна стоять операция "адрес", то есть &i. Аналогично про &po.x и &po.y. Заметим, что s - это массив, т.е. s и так есть адрес, поэтому перед s операция & не нужна; аналогично про po.ss - здесь & не требуется. В системном вызове write второй аргумент должен быть адресом данного, которое мы хотим записать в файл. Поэтому мы должны были написать &c (во втором вызове write). Ошибка в scanf - указание значения переменной вместо ее адреса - является довольно распространенной и не может быть обнаружена компилятором (даже при использо- вании прототипа функции scanf(char *fmt, ...), так как scanf - функция с переменным А. Богатырев, 1992-95 - 142 - Си в UNIX числом аргументов заранее не определенных типов). Приходится полагаться исключительно на собственную внимательность! 4.4. Как по дескриптору файла узнать, открыт он на чтение, запись, чтение и запись одновременно? Вот два варианта решения: #include <fcntl.h> #include <stdio.h> #include <sys/param.h> /* там определено NOFILE */ #include <errno.h> char *typeOfOpen(fd){ int flags; if((flags=fcntl (fd, F_GETFL, NULL)) < 0 ) return NULL; /* fd вероятно не открыт */ flags &= O_RDONLY | O_WRONLY | O_RDWR; switch(flags){ case O_RDONLY: return "r"; case O_WRONLY: return "w"; case O_RDWR: return "r+w"; default: return NULL; } } char *type2OfOpen(fd){ extern errno; /* см. главу "системные вызовы" */ int r=1, w=1; errno = 0; read(fd, NULL, 0); if( errno == EBADF ) r = 0; errno = 0; write(fd, NULL, 0); if( errno == EBADF ) w = 0; return (w && r) ? "r+w" : w ? "w" : r ? "r" : "closed"; } main(){ int i; char *s, *p; for(i=0; i < NOFILE; i++ ){ s = typeOfOpen(i); p = type2OfOpen(i); printf("%d:%s %s\n", i, s? s: "closed", p); } } Константа NOFILE означает максимальное число одновременно открытых файлов для одного процесса (это размер таблицы открытых процессом файлов, таблицы дескрипторов). Изу- чите описание системного вызова fcntl (file control). 4.5. Напишите функцию rename() для переименования файла. Указание: используйте сис- темные вызовы link() и unlink(). Ответ: А. Богатырев, 1992-95 - 143 - Си в UNIX rename( from, to ) char *from, /* старое имя */ *to; /* новое имя */ { unlink( to ); /* удалить файл to */ if( link( from, to ) < 0 ) /* связать */ return (-1); unlink( from ); /* стереть старое имя */ return 0; /* OK */ } Вызов link(существующее_имя, новое_имя); создает файлу альтернативное имя - в UNIX файл может иметь несколько имен: так каждый каталог имеет какое-то имя в родительском каталоге, а также имя "." в себе самом. Каталог же, содержащий подкаталоги, имеет некоторое имя в своем родительском ката- логе, имя "." в себе самом, и по одному имени ".." в каждом из своих подкаталогов. Этот вызов будет неудачен, если файл новое_имя уже существует; а также если мы попытаемся создать альтернативное имя в другой файловой системе. Вызов unlink(имя_файла) удаляет имя файла. Если файл больше не имеет имен - он уничтожается. Здесь есть одна тонкость: рассмотрим фрагмент int fd; close(creat("/tmp/xyz", 0644)); /*Создать пустой файл*/ fd = open("/tmp/xyz", O_RDWR); unlink("/tmp/xyz"); ... close(fd); Первый оператор создает пустой файл. Затем мы открываем файл и уничтожаем его единственное имя. Но поскольку есть программа, открывшая этот файл, он не удаляется немедленно! Программа далее работает с безымянным файлом при помощи дескриптора fd. Как только файл закрывается - он будет уничтожен системой (как не имеющий имен). Такой трюк используется для создания временных рабочих файлов. Файл можно удалить из каталога только в том случае, если данный каталог имеет для вас код доступа "запись". Коды доступа самого файла при удалении не играют роли. В современных версиях UNIX есть системный вызов rename, который делает то же самое, что и написанная нами одноименная функция. 4.6. Существование альтернативных имен у файла позволяет нам решить некоторые проб- лемы, которые могут возникнуть при использовании чужой программы, от которой нет исходного текста (которую нельзя поправить). Пусть программа выдает некоторую инфор- мацию в файл zz.out (и это имя жестко зафиксировано в ней, и не задается через аргу- менты программы): /* Эта программа компилируется в a.out */ main(){ int fd = creat("zz.out", 0644); write(fd, "It's me\n", 8); } Мы же хотим получить вывод на терминал, а не в файл. Очевидно, мы должны сделать файл zz.out синонимом устройства /dev/tty (см. конец этой главы). Это можно сделать коман- дой ln: $ rm zz.out ; ln /dev/tty zz.out $ a.out $ rm zz.out или программно: А. Богатырев, 1992-95 - 144 - Си в UNIX /* Эта программа компилируется в start */ /* и вызывается вместо a.out */ #include <stdio.h> main(){ unlink("zz.out"); link("/dev/tty", "zz.out"); if( !fork()){ execl("a.out", NULL); } else wait(NULL); unlink("zz.out"); } (про fork, exec, wait смотри в главе про UNIX). Еще один пример: программа a.out желает запустить программу /usr/bin/vi (смотри про функцию system() сноску через несколько страниц): main(){ ... system("/usr/bin/vi xx.c"); ... } На вашей же машине редактор vi помещен в /usr/local/bin/vi. Тогда вы просто создаете альтернативное имя этому редактору: $ ln /usr/local/bin/vi /usr/bin/vi Помните, что альтернативное имя файлу можно создать лишь в той же файловой системе, где содержится исходное имя. В семействе BSD |- это ограничение можно обойти, создав "символьную ссылку" вызовом symlink(link_to_filename,link_file_name_to_be_created); Символьная ссылка - это файл, содержащий имя другого файла (или каталога). Система не производит автоматический подсчет числа таких ссылок, поэтому возможны "висячие" ссылки - указывающие на уже удаленный файл. Прочесть содержимое файла-ссылки можно системным вызовом char linkbuf[ MAXPATHLEN + 1]; /* куда поместить ответ */ int len = readlink(pathname, linkbuf, sizeof linkbuf); linkbuf[len] = '\0'; Системный вызов stat автоматически разыменовывает символьные ссылки и выдает информа- цию про указуемый файл. Системный вызов lstat (аналог stat за исключением названия) выдает информацию про саму ссылку (тип файла S_IFLNK). Коды доступа к ссылке не имеют никакого значения для системы, существенны только коды доступа самого указуе- мого файла. Еще раз: символьные ссылки удобны для указания файлов и каталогов на другом диске. Пусть у вас не помещается на диск каталог /opt/wawa. Вы можете разместить каталог wawa на диске USR: /usr/wawa. После чего создать символьную ссылку из /opt: ln -s /usr/wawa /opt/wawa чтобы программы видели этот каталог под его прежним именем /opt/wawa. Еще раз: hard link - то, что создается системным вызовом link, имеет тот же I-node (и