Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
=> C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 Linux Kernel 2.6...5169 
 Trees...938 
 Максвелл 3...870 
 Go Web ...821 
 William Gropp...802 
 Ethreal 3...786 
 Gary V.Vaughan-> Libtool...772 
 Ethreal 4...770 
 Rodriguez 6...763 
 Ext4 FS...754 
 Steve Pate 1...754 
 Clickhouse...753 
 Ethreal 1...741 
 Secure Programming for Li...731 
 C++ Patterns 3...716 
 Ulrich Drepper...696 
 Assembler...694 
 DevFS...660 
 Стивенс 9...649 
 MySQL & PosgreSQL...630 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org

C в UNIX

Андрей Богатырев 1992-1995

Тут лежат примеры к статье.

1.34.

Напишите программу, распечатывающую простые числа до 1000.
             1, 2, 3, 5, 7, 11, 13, 17, ...
     /*#!/bin/cc primes.c -o primes -lm
      *        Простые числа.
      */
     #include <stdio.h>
     #include <math.h>
     int debug = 0;
     /* Корень квадратный из числа по методу Ньютона */
     #define eps 0.0001
     double  sqrt (x) double x;
     {
         double  sq, sqold, EPS;
         if (x < 0.0)
             return -1.0;
         if (x == 0.0)
             return 0.0;  /* может привести к делению на 0 */
         EPS = x * eps;
         sq = x;
         sqold = x + 30.0;         /* != sq */
         while (fabs (sq * sq - x) >= EPS) {
         /*     fabs( sq - sqold )>= EPS    */
             sqold = sq;
             sq = 0.5 * (sq + x / sq);
         }
         return sq;
     }
     /* таблица прoстых чисел */
     int is_prime (t) register int    t; {
         register int    i, up;
         int             not_div;
         if (t == 2 || t == 3 || t == 5 || t == 7)
             return 1;               /* prime */
         if (t % 2 == 0 || t == 1)
             return 0;               /* composite */
         up = ceil (sqrt ((double) t)) + 1;
         i = 3;
         not_div = 1;
         while (i <= up && not_div) {
             if (t % i == 0) {
                 if (debug)
                     fprintf (stderr, "%d поделилось на %d\n",
                                        t,               i);
                 not_div = 0;
                 break;
             }
             i += 2;  /*
                       * Нет смысла проверять четные,
                       * потому что если делится на 2*n,
                       * то делится и на 2,
                       * а этот случай уже обработан выше.
                       */
         }
         return not_div;
     }
     #define COL 6
     int     n;
     main (argc, argv) char **argv;
     {
         int     i,
                 j;
         int     n;
         if( argc < 2 ){
             fprintf( stderr, "Вызов: %s число [-]\n", argv[0] );
             exit(1);
         }
         i = atoi (argv[1]); /* строка -> целое, ею изображаемое */
         if( argc > 2 ) debug = 1;
         printf ("\t*** Таблица простых чисел от 2 до %d ***\n", i);
         n = 0;
         for (j = 1; j <= i; j++)
             if (is_prime (j)){
                 /* распечатка в COL колонок */
                 printf ("%3d%s", j, n == COL-1 ? "\n" : "\t");
                 if( n == COL-1 ) n = 0;
                 else             n++;
             }
         printf( "\n---\n" );
         exit (0);
     }
 

1.49.

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

     /*
      * Сортировка по методу Шелла.
      * Сортировке подвергается массив указателей на данные типа obj.
      *      v------.-------.------.-------.------0
      *             !       !      !       !
      *             *       *      *       *
      *            элементы типа obj
      * Программа взята из книги Кернигана и Ритчи.
      */
     #include <stdio.h>
     #include <string.h>
     #include <locale.h>
     #define obj char
     static shsort (v,n,compare)
     int n;              /* длина массива */
     obj *v[];           /* массив указателей */
     int (*compare)();   /* функция сравнения соседних элементов */
     {
             int g,      /* расстояние, на котором происходит сравнение */
                 i,j;    /* индексы сравниваемых элементов */
             obj *temp;
             for( g = n/2 ; g > 0  ; g /= 2 )
             for( i = g   ; i < n  ; i++    )
             for( j = i-g ; j >= 0 ; j -= g )
             {
                     if((*compare)(v[j],v[j+g]) <= 0)
                         break;      /* уже в правильном порядке */
                     /* обменять указатели */
                     temp = v[j]; v[j] = v[j+g]; v[j+g] = temp;
                     /* В качестве упражнения можете написать
                      * при помощи curses-а программу,
                      * визуализирующую процесс сортировки:
                      * например, изображающую эту перестановку
                      * элементов массива */
             }
     }
     /* сортировка строк */
     ssort(v) obj **v;
     {
             extern less();  /* функция сравнения строк */
             int len;
             /* подсчет числа строк */
             len=0;
             while(v[len]) len++;
             shsort(v,len,less);
     }
     /* Функция сравнения строк.
      * Вернуть целое меньше нуля, если a <  b
      *                      ноль, если a == b
      *               больше нуля, если a >  b
      */
     less(a,b) obj *a,*b;
     {
             return strcoll(a,b);
             /* strcoll - аналог strcmp,
              * но с учетом алфавитного порядка букв.
              */
     }
     char *strings[] = {
             "Яша", "Федя", "Коля",
             "Гриша", "Сережа", "Миша",
             "Андрей Иванович", "Васька",
             NULL
     };
     int main(){
             char **next;
             setlocale(LC_ALL, "");
             ssort( strings );
             /* распечатка */
             for( next = strings ; *next ; next++ )
                     printf( "%s\n", *next );
             return 0;
     }
 

1.146.

Рассмотрим процесс сборки программы из нескольких файлов на языке Си. Пусть мы имеем файлы file1.c, file2.c, file3.c (один из них должен содержать среди других функций функцию main). Ключ компилятора -o заставляет создавать выполняемую программу с именем, указанным после этого ключа. Если этот ключ не задан - будет создан выполняемый файл a.out

     cc file1.c file2.c file3.c -o file
 
Мы получили выполняемую программу file. Это эквивалентно 4-м командам:
     cc -c file1.c           получится     file1.o
     cc -c file2.c                         file2.o
     cc -c file3.c                         file3.o
     cc file1.o file2.o file3.o -o file
 

Ключ -c заставляет компилятор превратить файл на языке Си в "объектный" файл (содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда "склеивает" объектные файлы в единое целое - выполняемую программу*. При этом, если какие-то функции, используемые в нашей программе, не были определены (т.е. спрограммированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных функций. Если же каких-то функций не окажется и там - будет выдано сообщение об ошибке.

Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать только новые Си-файлы:

     cc -c file4.c
     cc file1.o file2.o file3.o file4.o -o file
        или (что то же самое,
        но cc сам разберется, что надо делать)
     cc file1.o file2.o file3.o file4.c -o file
 

Существующие у нас объектные файлы с отлаженными функциями удобно собрать в библиотеку - файл специальной структуры, содержащий все указанные файлы (все файлы склеены в один длинный файл, разделяясь специальными заголовками, см. include-файл <ar.h>):

     ar r file.a file1.o file2.o file3.o
 

Будет создана библиотека file.a, содержащая перечисленные .o файлы (имена библиотек в UNIX имеют суффикс .a - от слова archive, архив). После этого можно использовать библиотеку:

     cc file4.o file5.o file.a -o file
 

Механизм таков: если в файлах file4.o и file5.o не определена какая-то функция (функции), то просматривается библиотека, и в список файлов для "склейки" добавляется файл из библиотеки, содержащий определение этой функции (из библиотеки он не удаляется!).

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

     cc file6.c file7.o  \
        file.a mylib.a /lib/libLIBR1.a -o file
 
Таким образом, в команде cc можно смешивать имена файлов: исходных текстов на Си .c, объектных файлов .o и файлов-библиотек .a.

Просмотр библиотек, находящихся в стандартных местах (каталогах /lib и /usr/lib), можно включить и еще одним способом: указав ключ -l. Если библиотека называется

     /lib/libLIBR1.a   или     /usr/lib/libLIBR2.a
 
то подключение делается ключами
     -lLIBR1           и       -lLIBR2
 
соответственно.
     cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file
 
Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o файлов.

Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее при сборке, если какие-то функции, использованные вами, не были вами определены), то есть просматривается всегда (именно эта библиотека содержит коды, например, для printf, strcat, read).

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

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

Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы часто используете printf(), но задумываетесь ли вы о ее внутреннем устройстве?). Некоторые библиотечные функции могут быть вообще написаны не на Си, а на ассемблере или другом языке программирования***. Еще раз обращаю ваше внимание, что библиотека содержит не исходные тексты функций, а скомпилированные коды (и include-файлы содержат (как правило) не тексты функций, а только описание форматов данных)! Библиотека может также содержать статические данные, вроде массивов строк-сообщений об ошибках.

Посмотреть список файлов, содержащихся в библиотеке, можно командой

     ar tv имяФайлаБиблиотеки
 
а список имен функций - командой
     nm имяФайлаБиблиотеки
 
Извлечь файл (файлы) из архива (скопировать его в текущий каталог), либо удалить его из библиотеки можно командами
     ar x имяФайлаБиблиотеки имяФайла1 ...
     ar d имяФайлаБиблиотеки имяФайла1 ...
 
где ... означает список имен файлов.

"Лицом" библиотек служат прилагаемые к ним include-файлы. Системные includeфайлы, содержащие общие форматы данных для стандартных библиотечных функций, хранятся в каталоге /usr/include и подключаются так:

     для /usr/include/файл.h     надо  #include <файл.h>
     для /usr/include/sys/файл.h       #include <sys/файл.h>
 

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

      #include "файл.h"         /*  ./файл.h       */
      #include "../h/файл.h"    /*  ../h/файл.h    */
      #include "/usr/my/файл.h" /*  /usr/my/файл.h */
 
Непременно изучите содержимое стандартных include-файлов в своей системе!

В качестве резюме - схема, поясняющая "превращения" Си-программы из текста на языке программирования в выполняемый код: все файлы .c могут использовать общие include-файлы; их подстановку в текст, а также обработку #define произведет препроцессор cpp

     file1.c    file2.c    file3.c
       |          |          |       "препроцессор"
       | cpp      | cpp      | cpp
       |          |          |       "компиляция"
       | cc -c    | cc -c    | cc -c
       |          |          |
     file1.o    file2.o    file3.o
       |          |          |
       -----------*----------            |       Неявно добавятся:
              ld  |<----- /lib/libc.a (библ. станд. функций)
                  |       /lib/crt0.o (стартер)
     "связывание" |
     "компоновка" |<----- Явно указанные библиотеки:
                  |       -lm       /lib/libm.a
                  V
                a.out
 

1.147.

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

     static char id[] = "This is /usr/abs/mybin/xprogram";
 

Тогда в случае аварии в файловой системе, если вдруг ваш файл "потеряется" (то есть у него пропадет имя - например из-за порчи каталога), то он будет найден программой проверки файловой системы - fsck - и помещен в каталог /lost+found под специальным кодовым именем, ничего общего не имеющим со старым. Чтобы понять, что это был за файл и во что его следует переименовать (чтобы восстановить правильное имя), мы применим команду

     strings имя_файла
 

Эта команда покажет все длинные строки из печатных символов, содержащиеся в данном файле, в частности и нашу строку id[]. Увидев ее, мы сразу поймем, что файл надо переименовать так:

     mv имя_файла /usr/abs/mybin/xprogram
 

1.148.

Где размещать include-файлы и как программа узнает, где же они лежат? Стандартные системные include-файлы размещены в /usr/include и подкаталогах. Если мы пишем некую свою программу (проект) и используем директивы

     #include "имяФайла.h"
 

то обычно include-файлы имяФайла.h лежат в текущем каталоге (там же, где и файлы с программой на Си). Однако мы можем помещать ВСЕ наши include-файлы в одно место (скажем, известное группе программистов, работающих над одним и тем же проектом). Хорошее место для всех ваших личных include-файлов - каталог (вами созданный)

     $HOME/include
 
где $HOME - ваш домашний каталог. Хорошее место для общих include-файлов - каталог
     /usr/local/include
 

Как сказать компилятору, что #include "" файлы надо брать из определенного места, а не из текущего каталога? Это делает ключ компилятора

     cc -Iимя_каталога ...
 
Например:
     /* Файл x.c */
     #include "x.h"
     int main(int ac, char *av[]){
             ....
             return 0;
     }
 

И файл x.h находится в каталоге /home/abs/include/x.h (/home/abs - мой домашний каталог). Запуск программы на компиляцию выглядит так:

     cc -I/home/abs/include -O x.c -o x
 
или
     cc -I$HOME/include -O x.c -o x
 
Или, если моя программа x.c находится в /home/abs/progs
     cc -I../include -O x.c -o x
 
Ключ -O задает вызов компилятора с оптимизацией.

Ключ -I оказывает влияние и на #include <> директивы тоже. Для ОС Solaris на машинах Sun программы для оконной системы X Window System содержат строки вроде

     #include <X11/Xlib.h>
     #include <X11/Xutil.h>
 

На Sun эти файлы находятся не в /usr/include/X11, а в /usr/openwin/include/X11. Поэтому запуск на компиляцию оконных программ на Sun выглядит так:

     cc -O -I/usr/openwin/include xprogram.c \
           -o xprogram -L/usr/openwin/lib -lX11
 
где -lX11 задает подключение графической оконной библиотеки Xlib.

2. Массивы, строки, указатели.

Массив представляет собой агрегат из нескольких переменных одного и того же типа. Массив с именем a из LENGTH элементов типа TYPE объявляется так:

     TYPE a[LENGTH];
 

Это соответствует тому, что объявляются переменные типа TYPE со специальными именами a[0], a[1], ..., a[LENGTH-1]. Каждый элемент массива имеет свой номер - индекс. Доступ к x-ому элементу массива осуществляется при помощи операции индексации:

     int x = ... ;      /* целочисленный индекс   */
     TYPE value = a[x]; /* чтение x-ого элемента  */
          a[x] = value; /* запись в x-тый элемент */
 

В качестве индекса может использоваться любое выражение, выдающее значение целого типа: char, short, int, long. Индексы элементов массива в Си начинаются с 0 (а не с 1), и индекс последнего элемента массива из LENGTH элементов - это LENGTH-1 (а не LENGTH). Поэтому цикл по всем элементам массива - это

     TYPE a[LENGTH]; int indx;
     for(indx=0; indx < LENGTH; indx++)
        ...a[indx]...;
 

indx < LENGTH равнозначно indx <= LENGTH-1. Выход за границы массива (попытка чтения/записи несуществующего элемента) может привести к непредсказуемым результатам и поведению программы. Отметим, что это одна из самых распространенных ошибок.

Статические массивы можно объявлять с инициализацией, перечисляя значения их элементов в {} через запятую. Если задано меньше элементов, чем длина массива остальные элементы считаются нулями:

     int a10[10] = { 1, 2, 3, 4 }; /* и 6 нулей */
 

Если при описании массива с инициализацией не указать его размер, он будет подсчитан компилятором:

     int a3[] = { 1, 2, 3 }; /* как бы a3[3] */
 

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

Данное может занимать несколько подряд идущих байт. Размер в байтах участка памяти, требуемого для хранения значения типа TYPE, можно узнать при помощи операции sizeof(TYPE), а размер переменной - при помощи sizeof(var). Всегда выполняется sizeof(char)==1. В некоторых машинах адреса переменных (а также агрегатов данных массивов и структур) кратны sizeof(int) или sizeof(double) - это так называемое "выравнивание (alignment) данных на границу типа int". Это позволяет делать доступ к данным более быстрым (аппаратура работает эффективнее).

Язык Си предоставляет нам средство для работы с адресами данных - указатели (pointer)| -. Указатель физически - это адрес некоторой переменной ("указуемой" переменной). Отличие указателей от машинных адресов состоит в том, что указатель может содержать адреса данных только определенного типа. Указатель ptr, который может указывать на данные типа TYPE, описывается так:

     TYPE  var;     /* переменная       */
     TYPE *ptr;     /* объявление ук-ля */
           ptr = & var;
 

В данном случае мы занесли в указательную переменную ptr адрес переменной var. Будем говорить, что указатель ptr указывает на переменную var (или, что ptr установлен на var). Пусть TYPE равно int, и у нас есть массив и указатели:

     int  array[LENGTH], value;
     int *ptr, *ptr1;
 
Установим указатель на x-ый элемент массива
     ptr = & array[x];
 

Указателю можно присвоить значение другого указателя на такой же тип. В результате оба указателя будут указывать на одно и то же место в памяти: ptr1 = ptr;

Мы можем изменять указуемую переменную при помощи операции *

     *ptr = 128;   /* занести 128 в указуемую перем. */
     value = *ptr; /* прочесть указуемую переменную  */
 

В данном случае мы заносим и затем читаем значение переменной array[x], на которую поставлен указатель, то есть

     *ptr  означает сейчас  array[x]
 

Таким образом, операция * (значение по адресу) оказывается обратной к операции & (взятие адреса):

     & (*ptr) == ptr    и    * (&value) == value
 

Операция * объясняет смысл описания TYPE *ptr; оно означает, что значение выражения *ptr будет иметь тип TYPE. Название же типа самого указателя - это (TYPE *). В частности, TYPE может сам быть указательным типом - можно объявить указатель на указатель, вроде char **ptrptr;

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

         массив           указатель
            ____________       _____
     array: | array[0] |   ptr:| * |
            | array[1] |         |
            | array[2] |<--------- сейчас равен &array[2]
            |  ...     |
 
Следствием такой интерпретации имен массивов является то, что для того чтобы поставить указатель на начало массива, надо писать
     ptr = array;  или  ptr = &array[0];
             но не
     ptr = &array;
 
Операция & перед одиноким именем массива не нужна и недопустима!

Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0];

Указатели - не целые числа! Хотя физически это и номера байтов, адресная арифметика отличается от обычной. Так, если дан указатель TYPE *ptr; и номер байта (адрес), на который указывает ptr, равен byteaddr, то

     ptr = ptr + n; /* n - целое, может быть и < 0 */
 
заставит ptr указывать не на байт номер byteaddr + n, а на байт номер
     byteaddr + (n * sizeof(TYPE))
 

то есть прибавление единицы к указателю продвигает адрес не на 1 байт, а на размер указываемого указателем типа данных! Пусть указатель ptr указывает на x-ый элемент массива array. Тогда после

     TYPE *ptr2 = array + L;  /* L - целое */
     TYPE *ptr1 = ptr   + N;  /* N - целое */
           ptr += M;          /* M - целое */
 
указатели указывают на
     ptr1 == &array[x+N]   и   ptr  == &array[x+M]
     ptr2 == &array[L]
 
Если мы теперь рассмотрим цепочку равенств
     *ptr2 = *(array + L) = *(&array[L]) =
               array[L]
 

то получим ОСНОВНОЕ ПРАВИЛО: пусть ptr - указатель или имя массива. Тогда операции индексации, взятия значения по адресу, взятия адреса и прибавления целого к указателю связаны соотношениями:

      ptr[x]  тождественно *(ptr+x)
     &ptr[x]  тождественно   ptr+x
 
(тождества верны в обе стороны), в том числе при x==0 и x < 0. Так что, например,
     ptr[-1] означает  *(ptr-1)
     ptr[0]  означает  *ptr
 
Указатели можно индексировать подобно массивам. Рассмотрим пример:
             /* индекс:     0    1    2    3    4   */
     double  numbers[5] = { 0.0, 1.0, 2.0, 3.0, 4.0 };
     double *dptr   = &numbers[2];
     double  number =  dptr[2];  /* равно 4.0 */
     numbers: [0]   [1]   [2]   [3]   [4]
                           |
             [-2]  [-1]   [0]   [1]   [2]
                          dptr
 
поскольку
     если dptr    = &numbers[x] = numbers + x
     то   dptr[i] = *(dptr + i) =
                  = *(numbers + x + i) = numbers[x + i]
 

Указатель на один тип можно преобразовать в указатель на другой тип: такое преобразование не вызывает генерации каких-либо машинных команд, но заставляет компилятор изменить параметры адресной арифметики, а также операции выборки данного по указателю (собственно, разница в указателях на данные разных типов состоит только в размерах указуемых типов; а также в генерации команд `->' для выборки полей структур, если указатель - на структурный тип).

Целые (int или long) числа иногда можно преобразовывать в указатели. Этим пользуются при написании драйверов устройств для доступа к регистрам по физическим адресам, например:

     unsigned short *KISA5 = (unsigned short *) 0172352;
 
Здесь возникают два тонких момента:
  1. Как уже было сказано, адреса данных часто выравниваются на границу некоторого типа. Мы же можем задать невыровненное целое значение. Такой адрес будет некорректен.
  2. Структура адреса, поддерживаемая процессором, может не соответствовать формату целых (или длинных целых) чисел. Так обстоит дело с IBM PC 8086/80286, где адрес состоит из пары short int чисел, хранящихся в памяти подряд. Однако весь адрес (если рассматривать эти два числа как одно длинное целое) не является обычным long-числом, а вычисляется более сложным способом: адресная пара SEGMENT:OFFSET преобразуется так
         unsigned short SEGMENT, OFFSET; /*16 бит: [0..65535]*/
         unsigned long  ADDRESS = (SEGMENT << 4) + OFFSET;
           получается 20-и битный физический адрес ADDRESS
     

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

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

Отметим, что указатель можно направить в неправильное место - на участок памяти, содержащий данные не того типа, который задан в описании указателя; либо вообще содержащий неизвестно что:

     int i = 2, *iptr = &i;
     double x = 12.76;
       iptr += 7;  /* куда же он указал ?! */
       iptr = (int *) &x;  i = *iptr;
 

Само присваивание указателю некорректного значения еще не является ошибкой. Ошибка возникнет лишь при обращении к данным по этому указателю (такие ошибки довольно тяжело искать!).

При передаче имени массива в качестве параметра функции, как аргумент передается не копия САМОГО МАССИВА (это заняло бы слишком много места), а копия АДРЕСА 0-ого элемента этого массива (т.е. указатель на начало массива).

     f(int x   ){ x++;     }
     g(int xa[]){ xa[0]++; }
     int a[2] = { 1, 1 }; /* объявление с инициализацией */
     main(){
      f(a[0]); printf("%d\n",a[0]); /* a[0] осталось равно 1*/
      g(a   ); printf("%d\n",a[0]); /* a[0] стало равно 2   */
     }
 

В f() в качестве аргумента передается копия элемента a[0] (и изменение этой копии не приводит к изменению самого массива - аргумент x является локальной переменной в f()), а в g() таким локалом является АДРЕС массива a - но не сам массив, поэтому xa[0]++ изменяет сам массив a (зато, например, xa++ внутри g() изменило бы лишь локальную указательную переменную xa, но не адрес массива a).

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

     вместо    Fun(int xa[5]) { ... }
     можно     Fun(int xa[] ) { ... }
     или даже  Fun(int *xa  ) { ... }
 
Если функция должна знать длину массива - передавайте ее как дополнительный аргумент:
      int sum( int a[], int len ){
        int s=0, i;
        for(i=0; i < len; i++) s += a[i];
        return( s );
      }
      ... int arr[10] = { ... };
      ... int sum10 = sum(arr, 10); ...
 
Количество элементов в массиве TYPE arr[N]; можно вычислить специальным образом, как
     #define LENGTH (sizeof(arr) / sizeof(arr[0]))
 
или
     #define LENGTH (sizeof(arr) / sizeof(TYPE))
 
Оба способа выдадут число, равное N. Эти конструкции обычно употребляются для вычисления длины массивов, задаваемых в виде
     TYPE arr[] = { ....... };
 
без явного указания размера. sizeof(arr) выдает размер всего массива в байтах.

sizeof(arr[0]) выдает размер одного элемента. И все это не зависит от типа элемента (просто потому, что все элементы массивов имеют одинаковый размер).

Строка в Си - это последовательность байт (букв, символов, литер, character), завершающаяся в конце специальным признаком - байтом '\0'. Этот признак добавляется компилятором автоматически, когда мы задаем строку в виде "строка". Длина строки (т.е. число литер, предшествующих '\0') нигде явно не хранится. Длина строки ограничена лишь размером массива, в котором сохранена строка, и может изменяться в процессе работы программы в пределах от 0 до длины массива-1. При передаче строки в качестве аргумента в функцию, функции не требуется знать длину строки, т.к. передается указатель на начало массива, а наличие ограничителя '\0' позволяет обнаружить конец строки при ее просмотре.

С массивами байт можно использовать следующую конструкцию, задающую массивы (строки) одинакового размера:

     char stringA [ITSSIZE];
     char stringB [sizeof stringA];
 
В данном разделе мы в основном будем рассматривать строки и указатели на символы.

2.15.

Двумерные массивы в памяти представляются как одномерные. Например, если
     int a[N][M];
 
то конструкция a[y][x] превращается при компиляции в одномерную конструкцию, подобную такой:
     int a[N * M]; /* массив развернут построчно */
     #define a_yx(y, x)   a[(x) + (y) * M]
 
то есть
     a[y][x] есть *(&a[0][0] + y * M + x)
 

Следствием этого является то, что компилятор для генерации индексации двумерных (и более) массовов должен знать M - размер массива по 2-ому измерению (а также 3-ему, 4-ому, и.т.д.). В частности, при передаче многомерного массива в функцию

     f(arr) int arr[N][M]; { ... }   /* годится    */
     f(arr) int arr[] [M]; { ... }   /* годится    */
     f(arr) int arr[] [];  { ... }   /* не годится */
     f(arr) int (*arr)[M]; { ... }   /* годится    */
     f(arr) int  *arr [M]; { ... }   /* не годится:
                   это уже не двумерный массив,
                   а одномерный массив указателей  */
 
А также при описании внешних массивов:
     extern int a[N][M];     /* годится */
     extern int a[ ][M];     /* годится */
     extern int a[ ][ ];     /* не годится: компилятор
               не сможет сгенерить операцию индексации */
 
Вот как, к примеру, должна выглядеть работа с двумерным массивом arr[ROWS][COLS], отведенным при помощи malloc();
     void f(int array[][COLS]){
             int x, y;
             for(y=0; y < ROWS; y++)
                 for(x=0; x < COLS; x++)
                     array[y][x] = 1;
     }
     void main(){
             int *ptr = (int *) malloc(sizeof(int) * ROWS * COLS);
             f( (int (*) [COLS]) ptr);
     }
 

2.16.

Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую программу:
     #include <stdio.h>
     #define First  3
     #define Second 5
     char arr[First][Second] = {
             "ABC.",
             { 'D', 'E', 'F', '?', '\0' },
             { 'G', 'H', 'Z', '!', '\0' }
     };
     char (*ptr)[Second];
     main(){
             int i;
             ptr = arr;      /* arr и ptr теперь взаимозаменимы */
             for(i=0; i < First; i++)
                     printf("%s\t%s\t%c\n", arr[i], ptr[i], ptr[i][2]);
     }
 

Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумерные индексы.

Попробуйте сами объявить

     char (*ptr)[4];
     char (*ptr)[6];
     char **ptr;
 

и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y].

Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению

             { 'A', 'B', 'C', '.', '\0' },
 

2.17.

Массив s моделирует двумерный массив char s[H][W]; Перепишите пример при помощи указателей, избавьтесь от операции умножения. Прямоугольник (x0,y0,width,height) лежит целиком внутри (0,0,W,H).

     char s[W*H]; int x,y; int x0,y0,width,height;
     for(x=0; x < W*H; x++) s[x] = '.';
          ...
     for(y=y0; y < y0+height; y++)
       for(x=x0; x < x0+width; x++)
           s[x + W*y] = '*';
 
Ответ:
     char s[W*H]; int i,j; int x0,y0,width,height;
     char *curs;
          ...
     for(curs = s + x0 + W*y0, i=0;
         i < height; i++, curs += W-width)
       for(j=0; j < width; j++)
             *curs++ = '*';
 
Такая оптимизация возможна в некоторых функциях из главы "Работа с видеопамятью".

2.18.

Что означают описания?
     int i;            // целое.
     int *pi;          // указатель на целое.
     int *api[3];      // массив из 3х ук-лей на целые.
     int (*pai)[3];    // указатель на массив из 3х целых.
                       // можно описать как    int **pai;
     int fi();         // функция, возвращающая целое.
     int *fpi();       // ф-ция, возвр. ук-ль на целое.
     int (*pfi)();     // ук-ль на ф-цию, возвращающую целое.
     int *(*pfpi)();   // ук-ль на ф-цию, возвр. ук-ль на int.
     int (*pfpfi())(); // ф-ция, возвращающая указатель на
                       // "функцию, возвращающую целое".
     int (*fai())[3];  // ф-ция, возвр. ук-ль на массив
                       // из 3х целых. иначе ее
                       // можно описать как    int **fai();
     int (*apfi[3])(); // массив из 3х ук-лей на функции,
                       // возвращающие целые.
 
Переменные в Си описываются в формате их использования. Так описание
     int (*f)();
 
означает, что f можно использовать в виде
     int value;
     value = (*f)(1, 2, 3 /* список аргументов */);
 

Однако из такого способа описания тип самой описываемой переменной и его смысл довольно неочевидны. Приведем прием (позаимствованный из журнала "Communications of the ACM"), позволяющий прояснить смысл описания. Описание на Си переводится в описание в стиле языка Algol-68. Далее

     ref      ТИП    означает  "указатель на ТИП"
     proc()   ТИП              "функция, возвращающая ТИП"
     array of ТИП              "массив из элементов ТИПа"
     x:       ТИП              "x имеет тип ТИП"
 
Приведем несколько примеров, из которых ясен и способ преобразования:
     int (*f())();     означает
             (*f())()  :                    int
              *f()     :             proc() int
               f()     :         ref proc() int
               f       :  proc() ref proc() int
 
то есть f - функция, возвращающая указатель на функцию, возвращающую целое.
     int (*f[3])();    означает
             (*f[])()  :                      int
              *f[]     :               proc() int
               f[]     :           ref proc() int
               f       :  array of ref proc() int
 
f - массив указателей на функции, возвращающие целые. Обратно: опишем
g как указатель на функцию, возвращающую указатель на массив из 5и указателей на функции, возвращающие указатели на целые.
            g          : ref p() ref array of ref p() ref int
           *g          :     p() ref array of ref p() ref int
          (*g)()       :         ref array of ref p() ref int
         *(*g)()       :             array of ref p() ref int
        (*(*g)())[5]   :                      ref p() ref int
       *(*(*g)())[5]   :                          p() ref int
      (*(*(*g)())[5])():                              ref int
     *(*(*(*g)())[5])():                                  int
                          int *(*(*(*g)())[5])();
 
В Си невозможны функции, возвращающие массив:
     proc() array of ...
             а только
     proc() ref array of ...
 
Само название типа (например, для использования в операции приведения типа) получается вычеркиванием имени переменной (а также можно опустить размер массива):
             g = ( int *(*(*(*)())[])() ) 0;
 

2.19.

Напишите функцию strcat(d,s), приписывающую строку s к концу строки d.

Ответ:

      char *strcat(d,s) register char *d, *s;
      {  while( *d ) d++;      /* ищем конец строки d */
         while( *d++ = *s++ ); /* strcpy(d, s)        */
         return (d-1);         /* конец строки        */
      }
 
Цикл, помеченный "strcpy" - это наиболее краткая запись операторов
         do{ char c;
             c = (*d = *s); s++; d++;
         } while(c != '\0');
 
На самом деле strcat должен по стандарту возвращать свой первый аргумент, как и функция strcpy:
      char *strcat(d,s) register char *d, *s;
      {  char *p = d;
         while( *d ) d++;
         strcpy(d, s); return p;
      }
 

2.56.

Кроме функций работы со строками (где предполагается, что массив байт завершается признаком конца '\0'), в Си предусмотрены также функции для работы с массивами байт без ограничителя. Для таких функций необходимо явно указывать длину обрабатываемого массива. Напишите функции: пересылки массива длиной n байт memcpy(dst,src,n); заполнения массива символом c memset(s,c,n); поиска вхождения символа в массив memchr(s,c,n); сравнения двух массивов memcmp(s1,s2,n); Ответ:

     #define REG register
     char *memset(s, c, n) REG char *s, c;
     {    REG char *p = s;
          while( --n >= 0 ) *p++ = c;
          return s;
     }
     char *memcpy(dst, src, n)
           REG char *dst, *src;
           REG int n;
     {     REG char *d = dst;
           while( n-- > 0 ) *d++ = *src++;
           return dst;
     }
     char *memchr(s, c, n) REG char *s, c;
     {
           while(n-- && *s++ != c);
           return( n < 0 ? NULL : s-1 );
     }
     int memcmp(s1, s2, n)
           REG char *s1, *s2; REG n;
     {
           while(n-- > 0 && *s1 == *s2)
             s1++, s2++;
           return( n < 0 ? 0 : *s1 - *s2 );
     }
 
Есть такие стандартные функции.

2.57.

Почему лучше пользоваться стандартными функциями работы со строками и памятью (strcpy, strlen, strchr, memcpy, ...)?

Ответ: потому, что они обычно реализованы поставщиками системы ЭФФЕКТИВНО, то есть написаны не на Си, а на ассемблере с использованием специализированных машинных команд и регистров. Это делает их более быстрыми. Написанный Вами эквивалент на Си может использоваться для повышения мобильности программы, либо для внесения поправок в стандартные функции.

2.58.

Рассмотрим программу, копирующую строку саму в себя:
     #include <stdio.h>
     #include <string.h>
     char string[] = "abcdefghijklmn";
     void main(void){
             memcpy(string+2, string, 5);
             printf("%s\n", string);
             exit(0);
 

Она печатает abababahijklmn. Мы могли бы ожидать, что кусок длины 5 символов "abcde" будет скопирован как есть: ab[abcde]hijklmn, а получили ab[ababa]hijklmn - циклическое повторение первых двух символов строки... В чем дело? Дело в том, что когда области источника (src) и получателя (dst) перекрываются, то в некий момент *src берется из УЖЕ перезаписанной ранее области, то есть испорченной! Вот программа, иллюстрирующая эту проблему:

     #include <stdio.h>
     #include <string.h>
     #include <ctype.h>
     char string[] = "abcdefghijklmn";
     char *src = &string[0];
     char *dst = &string[2];
     int n     = 5;
     void show(int niter, char *msg){
             register length, i;
             printf("#%02d %s\n", niter, msg);
             length = src-string;
             putchar('\t');
             for(i=0; i < length+3; i++) putchar(' ');
             putchar('S');  putchar('\n');
             printf("\t...%s...\n", string);
             length = dst-string;
             putchar('\t');
             for(i=0; i < length+3; i++) putchar(' ');
             putchar('D');  putchar('\n');
     }
     void main(void){
             int iter = 0;
             while(n-- > 0){
                     show(iter,   "перед");
                       *dst++ = toupper(*src++);
                     show(iter++, "после");
             }
             exit(0);
     }
 
Она печатает:
     #00 перед
                S
             ...abcdefghijklmn...
                  D
     #00 после
                 S
             ...abAdefghijklmn...
                   D
     #01 перед
                 S
             ...abAdefghijklmn...
                   D
     #01 после
                  S
             ...abABefghijklmn...
                    D
     #02 перед
                  S
             ...abABefghijklmn...
                    D
     #02 после
                   S
             ...abABAfghijklmn...
                     D
     #03 перед
                   S
             ...abABAfghijklmn...
                     D
     #03 после
                    S
             ...abABABghijklmn...
                      D
     #04 перед
                    S
             ...abABABghijklmn...
                      D
     #04 после
                     S
             ...abABABAhijklmn...
                       D
 

Отрезки НЕ перекрываются, если один из них лежит либо целиком левее, либо целиком правее другого (n - длина обоих отрезков).

     dst        src                  src        dst
     ########   @@@@@@@@             @@@@@@@@   ########
        dst+n <= src         или          src+n <= dst
        dst <= src-n         или          dst >= src+n
 
Отрезки перекрываются в случае
     ! (dst <= src - n || dst >= src + n) =
       (dst >  src - n && dst <  src + n)
 
При этом опасен только случай dst > src. Таким образом опасная ситуация описывается условием
     src < dst && dst < src + n
 
(если dst==src, то вообще ничего не надо делать). Решением является копирование "от хвоста к голове":
     void bcopy(register char *src, register char *dst,
                register int n){
             if(dst >= src){
                     dst += n-1;
                     src += n-1;
                     while(--n >= 0)
                             *dst-- = *src--;
             }else{
                     while(n-- > 0)
                             *dst++ = *src++;
             }
     }
 
Или, ограничиваясь только опасным случаем:
     void bcopy(register char *src, register char *dst,
                register int n){
             if(dst==src || n <= 0) return;
             if(src < dst && dst < src + n) {
                     dst += n-1;
                     src += n-1;
                     while(--n >= 0)
                             *dst-- = *src--;
             }else   memcpy(dst, src, n);
     }
 
Программа
     #include <stdio.h>
     #include <string.h>
     #include <ctype.h>
     char string[] = "abcdefghijklmn";
     char *src = &string[0];
     char *dst = &string[2];
     int n     = 5;
     void show(int niter, char *msg){
             register length, i;
             printf("#%02d %s\n", niter, msg);
             length = src-string;
             putchar('\t');
             for(i=0; i < length+3; i++) putchar(' ');
             putchar('S');  putchar('\n');
             printf("\t...%s...\n", string);
             length = dst-string;
             putchar('\t');
             for(i=0; i < length+3; i++) putchar(' ');
             putchar('D');  putchar('\n');
     }
     void main(void){
             int iter = 0;
             if(dst==src || n <= 0){
                     printf("Ничего не надо делать\n");
                     return;
             }
             if(src < dst && dst < src + n) {
                     dst += n-1;
                     src += n-1;
                     while(--n >= 0){
                             show(iter,   "перед");
                               *dst-- = toupper(*src--);
                             show(iter++, "после");
                     }
             }else
                     while(n-- > 0){
                             show(iter,   "перед");
                               *dst++ = toupper(*src++);
                             show(iter++, "после");
                     }
             exit(0);
     }
 
Печатает
     #00 перед
                    S
             ...abcdefghijklmn...
                      D
     #00 после
                   S
             ...abcdefEhijklmn...
                     D
     #01 перед
                   S
             ...abcdefEhijklmn...
                     D
     #01 после
                  S
             ...abcdeDEhijklmn...
                    D
     #02 перед
                  S
             ...abcdeDEhijklmn...
                    D
     #02 после
                 S
             ...abcdCDEhijklmn...
                   D
     #03 перед
                 S
             ...abcdCDEhijklmn...
                   D
     #03 после
                S
             ...abcBCDEhijklmn...
                  D
     #04 перед
                S
             ...abcBCDEhijklmn...
                  D
     #04 после
               S
             ...abABCDEhijklmn...
                 D
 

Теперь bcopy() - удобная функция для копирования и сдвига массивов, в частности массивов указателей. Пусть у нас есть массив строк (выделенных malloc-ом):

     char *lines[NLINES];
 
Тогда циклическая перестановка строк выглядит так:
     void scrollUp(){
             char *save = lines[0];
             bcopy((char *) lines+1, /* from */
                   (char *) lines,   /* to */
                   sizeof(char *) * (NLINES-1));
             lines[NLINES-1] = save;
     }
     void scrollDown(){
             char *save = lines[NLINES-1];
             bcopy((char *) &lines[0], /* from */
                   (char *) &lines[1], /* to */
                   sizeof(char *) * (NLINES-1));
             lines[0] = save;
     }
 

Возможно, что написание по аналогии функции для копирования массивов элементов типа (void *) - обобщенных указателей - может оказаться еще понятнее и эффективнее. Такая функция - memmove - стандартно существует в UNIX SVR4. Заметьте, что порядок аргументов в ней обратный по отношению к bcopy. Следует отметить, что в SVR4 все функции mem... имеют указатели типа (void *) и счетчик типа size_t - тип для количества байт (вместо unsigned long); в частности длина файла имеет именно этот тип (смотри системные вызовы lseek и stat).

     #include <sys/types.h>
     void memmove(void *Dst, const void *Src,
                  register size_t n){
                 register caddr_t src = (caddr_t) Src,
                                  dst = (caddr_t) Dst;
                 if(dst==src || n <= 0) return;
                 if(src < dst && dst < src + n) {
                         dst += n-1;
                         src += n-1;
                         while(--n >= 0)
                                 *dst-- = *src--;
                 }else   memcpy(dst, src, n);
     }
 
caddr_t - это тип для указателей на БАЙТ, фактически это (unsigned char *). Зачем вообще понадобилось использовать caddr_t? Затем, что для
     void *pointer;
     int n;
 
значение
     pointer + n
 

не определено и невычислимо, ибо sizeof(void) не имеет смысла - это не 0, а просто ошибка, диагностируемая компилятором!

4. Работа с файлами.

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

В UNIX и в MS DOS файлы не имеют предопределенной структуры и представляют собой просто линейные массивы байт. Если вы хотите задать некоторую структуру хранимой информации - вы должны позаботиться об этом в своей программе сами.

Файлы отличаются от обычных массивов тем, что

  • они могут изменять свой размер;
  • обращение к элементам этих массивов производится не при помощи операции индексации [], а при помощи специальных системных вызовов и функций;
  • доступ к элементам файла происходит в так называемой "позиции чтения/записи", которая автоматически продвигается при операциях чтения/записи, т.е. файл просматривается последовательно. Есть, правда, функции для произвольного изменения этой позиции.

Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и простых файлов. Об этом и о системе именования файлов прочитайте в документации по UNIX.

4.1.

Для работы с каким-либо файлом наша программа должна открыть этот файл - установить связь между именем файла и некоторой переменной в программе. При открытии файла в ядре операционной системы выделяется "связующая" структура file "открытый файл", содержащая:
f_offset: указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как
RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи;
f_flag: режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги;
f_inode: расположение файла на диске (в UNIX - в виде ссылки на I-узел файла*);
и кое-что еще.

У каждого процесса имеется таблица открытых им файлов - это массив ссылок на упомянутые "связующие" структуры**. При открытии файла в этой таблице ищется

       - длина файла                  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. В нее, в частности, входят таблица открытых процессом файлов, свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого "дескриптора файла".

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

Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных программах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь: несколько или один процессов могут открыть один и тот же файл одновременно несколько раз. При этом будет создано несколько "связующих" структур (по одной для каждого открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуация, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание вызова 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;
сигнализирующее об ошибке. В этом случае файл надо создать:
     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.

существовал - срабатывает 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. При записи файл если надо автоматически увеличивает свой размер. При чтении - если мы достигнем конца файла, то 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 - функция с переменным числом аргументов заранее не определенных типов). Приходится полагаться исключительно на собственную внимательность!

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(). Ответ:

        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
 
или программно:
     /* Эта программа компилируется в 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 (индексный узел, паспорт), что и исходный файл. Это просто альтернативное имя файла, учитываемое в поле di_nlink в I-node.
symbolic link
- создается вызовом symlink. Это отдельный самостоятельный файл, с собственным I-node. Правда, коды доступа к этому файлу не играют никакой роли; значимы только коды доступа указуемого файла.

4.7.

Напишите программу, которая находит в файле символ @ и выдает файл с этого места дважды. Указание: для запоминания позиции в файле используйте вызов lseek() позиционирование указателя чтения/записи:

     long offset, lseek();
        ...
     /* Узнать текущую позицию чтения/записи:
      * сдвиг на 0 от текущей позиции. lseek вернет новую
      * позицию указателя (в байтах от начала файла). */
     offset = lseek(fd, 0L, 1);  /* ftell(fp) */
 
А для возврата в эту точку:
        lseek(fd, offset, 0);    /* fseek(fp, offset, 0) */
 
По поводу lseek надо помнить такие вещи:
  • lseek(fd, offset, whence) устанавливает указатель чтения/записи на расстояние offset байт при whence:
           0    от начала файла     RWptr  = offset;
           1    от текущей позиции  RWptr += offset;
           2    от конца файла      RWptr  = длина_файла + offset;
     
    Эти значения whence можно обозначать именами:
         #include <stdio.h>
           0    это   SEEK_SET
           1    это   SEEK_CUR
           2    это   SEEK_END
     
  • Установка указателя чтения/записи - это виртуальная операция, т.е. реального подвода магнитных головок и вообще обращения к диску она не вызывает. Реальное движение головок к нужному месту диска произойдет только при операциях чтения/записи read()/write(). Поэтому lseek() - дешевая операция.
  • lseek() возвращает новую позицию указателя чтения/записи RWptr относительно начала файла (long смещение в байтах). Помните, что если вы используете это значение, то вы должны предварительно описать lseek как функцию, возвращающую длинное целое: long lseek();
  • Аргумент offset должен иметь тип long (не ошибитесь!).
  • Если поставить указатель за конец файла (это допустимо!), то операция записи write() сначала заполнит байтом '\0' все пространство от конца файла до позиции указателя; операция read() при попытке чтения из-за конца файла вернет "прочитано 0 байт". Попытка поставить указатель перед началом файла вызовет ошибку.
  • Вызов lseek() неприменим к pipe и FIFO-файлам, поэтому попытка сдвинуться на 0 байт выдаст ошибку:
              /* это стандартная функция */
              int isapipe(int fd){
                   extern errno;
                   return (lseek(fd, 0L, SEEK_CUR) < 0 && errno == ESPIPE);
              }
     
    выдает "истину", если fd - дескриптор "трубы"(pipe).

4.8.

Каков будет эффект следующей программы?
     int fd = creat("aFile", 0644); /* creat создает файл
         открытый на запись, с доступом rw-r--r-- */
     write(fd, "begin", 5 );
     lseek(fd, 1024L * 1000, 0);
     write(fd, "end", 3 );
     close(fd);
 

Напомним, что при записи в файл, его длина автоматически увеличивается, когда мы записываем информацию за прежним концом файла. Это вызывает отведение места на диске для хранения новых данных (порциями, называемыми блоками - размером от 1/2 до 8 Кб в разных версиях). Таким образом, размер файла ограничен только наличием свободных блоков на диске.

В нашем примере получится файл длиной 1024003 байта. Будет ли он занимать на диске 1001 блок (по 1 Кб)?

В системе UNIX - нет! Вот кое-что про механику выделения блоков:

  • Блоки располагаются на диске не обязательно подряд - у каждого файла есть специальным образом организованная таблица адресов его блоков.
  • Последний блок файла может быть занят не целиком (если длина файла не кратна размеру блока), тем не менее число блоков у файла всегда целое (кроме семейства BSD, где блок может делиться на фрагменты, принадлежащие разным файлам). Операционная система в каждый момент времени знает длину файла с точностью до одного байта и не позволяет нам "заглядывать" в остаток блока, пока при своем "росте" файл не займет эти байты.
  • Блок на диске физически выделяется лишь после операции записи в этот блок.

В нашем примере: при создании файла его размер 0, и ему выделено 0 блоков. При первой записи файлу будет выделен один блок (логический блок номер 0 для файла) и в его начало запишется "begin". Длина файла станет равна 5 (остаток блока - 1019 байт - не используется и файлу логически не принадлежит!). Затем lseek поставит указатель записи далеко за конец файла и write запишет в 1000-ый блок слово "end". 1000-ый блок будет выделен на диске. В этот момент у файла "возникнут" и все промежуточные блоки 1..999. Однако они будут только "числиться за файлом", но на диске отведены не будут (в таблице блоков файла это обозначается адресом 0)! При чтении из них будут читаться байты '\0'. Это так называемая "дырка" в файле. Файл имеет размер 1024003 байта, но на диске занимает всего 2 блока (на самом деле чуть больше, т.к. часть таблицы блоков файла тоже находится в специальных блоках файла). Блок из "дырки" станет реальным, если в него что-нибудь записать.

Будьте готовы к тому, что "размер файла" (который, кстати, можно узнать системным вызовом stat) - это в UNIX не то же самое, что "место, занимаемое файлом на диске".

4.10.

Доступ к диску (чтение/запись) гораздо (на несколько порядков) медленнее, чем доступ к данным в оперативной памяти. Кроме того, если мы читаем или записываем файл при помощи системных вызовов маленькими порциями (по 1-10 символов)

     char c;
     while( read(0, &c, 1)) ... ; /* 0 - стандартный ввод */
 

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

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

Для решения этих двух проблем была построена специальная библиотека функций, названная stdio - "стандартная библиотека ввода/вывода" (standard input/output library). Она является частью библиотеки /lib/libc.a и представляет собой надстройку над системными вызовами (т.к. в конце концов все ее функции время от времени обращаются к системе, но гораздо реже, чем если использовать сисвызовы непосредственно).

Небезызвестная директива #include <stdio.h> включает в нашу программу файл с объявлением форматов данных и констант, используемых этой библиотекой.

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

Связь с файлом в этой модели обмена осуществляется уже не при помощи целого числа - дескриптора файла (file descriptor), а при помощи адреса "связной" структуры FILE. Указатель на такую структуру условно называют указателем на файл (file pointer)*. Структура FILE содержит в себе:

  • дескриптор fd файла для обращения к системным вызовам;
  • указатель на буфер, размещенный в памяти программы;
  • указатель на текущее место в буфере, откуда надо выдать или куда записать очередной символ; этот указатель продвигается при каждом вызове getc или putc;
  • счетчик оставшихся в буфере символов (при чтении) или свободного места (при записи);
  • режимы открытия файла (чтение/запись/чтение+запись) и текущее состояние файла. Одно из состояний - при чтении файла был достигнут его конец**;
  • способ буферизации;

Предусмотрено несколько стандартных структур FILE, указатели на которые называются stdin, stdout и stderr и связаны с дескрипторами 0, 1, 2 соответственно (стандартный ввод, стандартный вывод, стандартный вывод ошибок). Напомним, что эти каналы открыты неявно (автоматически) и, если не перенаправлены, связаны с вводом с клавиатуры и выводом на терминал.

Буфер в оперативной памяти нашей программы создается (функцией malloc) при открытии файла при помощи функции fopen(). После открытия файла все операции обмена с файлом происходят не по 1 байту, а большими порциями размером с буфер - обычно по 512 байт (константа BUFSIZ).

При чтении символа

             int c; FILE *fp = ... ;
             c = getc(fp);
 
getc выдает ее первый байт.

При последующих вызовах getc выдаются следующие байты из буфера, а обращений к диску уже не происходит! Лишь когда буфер будет исчерпан - произойдет очередное чтение с диска. Таким образом, информация читается из файла с опережением, заранее наполняя буфер; а по требованию выдается уже из буфера. Если мы читаем 1024 байта из файла при помощи getc(), то мы 1024 раза вызываем эту функцию, но всего 2 раза системный вызов read - для чтения двух порций информации из файла, каждая - по 512 байт.

При записи

             char c; FILE *fp = ... ;
             putc(c, fp);
 
выводимые символы накапливаются в буфере. Только когда в нем окажется большая порция информации, она за одно обращение write записывается на диск. Буфер записи "выталкивается" в файл в таких случаях:
  • буфер заполнен (содержит BUFSIZ символов).
  • при закрытии файла (fclose или exit***).
  • при вызове функции fflush (см. ниже).
  • в специальном режиме - после помещения в буфер символа '\n' (см. ниже).
  • в некоторых версиях - перед любой операцией чтения из канала stdin (например, при вызове gets), при условии, что stdout буферизован построчно (режим _IOLBF, смотри ниже), что по-умолчанию так и есть.

Приведем упрощенную схему, поясняющую взаимоотношения основных функций и макросов из stdio (кто кого вызывает). Далее s означает строку, c - символ, fp - указатель на структуру FILE****. Функции, работающие со строками, в цикле вызывают посимвольные операции. Обратите внимание, что в конце концов все функции обращаются к системным вызовам read и write, осуществляющим ввод/вывод низкого уровня.

Системные вызовы далее обозначены жирно, макросы - курсивом.

Открыть файл, создать буфер:

     #include <stdio.h>
     FILE *fp = fopen(char *name, char *rwmode);
                    |  вызывает
                    V
        int fd = open (char *name, int irwmode);
     Если открываем на запись и файл не существует (fd < 0),
     то создать файл вызовом:
            fd = creat(char *name, int accessmode);
            fd будет открыт для записи в файл.
 
По умолчанию fopen() использует для creat коды доступа accessmode равные 0666 (rwrw-rw-).

Соответствие аргументов fopen и open:

             rwmode          irwmode
             ------------------------
        "r"             O_RDONLY
             "w"             O_WRONLY|O_CREAT |O_TRUNC
             "r+"            O_RDWR
             "w+"            O_RDWR  |O_CREAT |O_TRUNC
             "a"             O_WRONLY|O_CREAT |O_APPEND
             "a+"            O_RDWR  |O_CREAT |O_APPEND
 

Для r, r+ файл уже должен существовать, в остальных случаях файл создается, если его не было.

Если fopen() не смог открыть (или создать) файл, он возвращает значение NULL:

     if((fp = fopen(name, rwmode)) == NULL){ ...неудача... }
 
Итак, схема:
     printf(fmt,...)--->--,----fprintf(fp,fmt,...)->--*
                      fp=stdout                       |
                               fputs(s,fp)--------->--|
     puts(s)----------->-------putchar(c)-----,---->--|
                                          fp=stdout   |
                       fwrite(array,size,count,fp)->--|
                                                      |
         Ядро ОС                               putc(c,fp)
     ------------------*                              |
     |файловая---<--write(fd,s,len)------------<----БУФЕР
     |система---->---read(fd,s,len)-*     _flsbuf(c,fp)
     |   |             !            |
     |системные буфера !            |
     |   |             !            V           ungetc(c,fp)
     |драйвер устр-ва  !            |                      |
     |(диск, терминал) !            |     _filbuf(fp)      |
     |   |             !            *--------->-----БУФЕР<-*
     |устройство       !                              |
     ------------------*                       c=getc(fp)
                                                      |
               rdcount=fread(array,size,count,fp)--<--|
     gets(s)-------<---------c=getchar()------,----<--|
                                          fp=stdout   |
                                                      |
                             fgets(sbuf,buflen,fp)-<--|
     scanf(fmt,.../*ук-ли*/)--<-,--fscanf(fp,fmt,...)-*
                             fp=stdin
 
Закрыть файл, освободить память выделенную под буфер:
     fclose(fp) ---> close(fd);
 
И чуть в стороне - функция позиционирования:
     fseek(fp,long_off,whence) ---> lseek(fd,long_off,whence);
 

Функции _flsbuf и _filbuf - внутренние для stdio, они как раз сбрасывают буфер в файл либо читают новый буфер из файла.

По указателю fp можно узнать дескриптор файла:

     int fd = fileno(fp);
 
Это макроопределение просто выдает поле из структуры FILE. Обратно, если мы открыли файл open-ом, мы можем ввести буферизацию этого канала:
     int fd = open(name, O_RDONLY);  /* или creat() */
             ...
     FILE *fp = fdopen(fd, "r");
 
(здесь надо вновь указать КАК мы открываем файл, что должно соответствовать режиму открытия open-ом). Теперь можно работать с файлом через fp, а не fd.

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

4.11.

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

     while((c = getchar()) != '+' );
     /* Прочли '+' */   ungetc(c ,stdin);
     /* А можно заменить этот символ на другой! */
     c = getchar();     /* снова прочтет '+' */
 

4.12.

Очень часто делают ошибку в функции fputc, путая порядок ее аргументов. Так ничего не стоит написать:

             FILE *fp = ......;
             fputc( fp, '\n' );
 
Запомните навсегда!
             int fputc( int c,  FILE *fp );
 
указатель файла идет вторым! Существует также макроопределение
             putc( c, fp );
 
Оно ведет себя как и функция fputc, но не может быть передано в качестве аргумента в функцию:
     #include <stdio.h>
     putNtimes(    fp,     c,     n,       f      )
             FILE *fp; int c; int n; int (*f)();
     {       while( n > 0 ){ (*f)( c, fp ); n--; }}
                  возможен вызов
             putNtimes( fp, 'a', 3, fputc );
                  но недопустимо
             putNtimes( fp, 'a', 3, putc );
 

Тем не менее всегда, где возможно, следует пользоваться макросом - он работает быстрее. Аналогично, есть функция fgetc(fp) и макрос getc(fp).

Отметим еще, что putchar и getchar это тоже всего лишь макросы

     #define putchar(c)      putc((c), stdout)
     #define getchar()       getc(stdin)
 

4.13.

Известная вам функция printf также является частью библиотеки stdio. Она входит в семейство функций:
     FILE   *fp; char bf[256];
     fprintf(fp, fmt, ... );
      printf(    fmt, ... );
     sprintf(bf, fmt, ... );
 

Первая из функций форматирует свои аргументы в соответствии с форматом, заданным строкой fmt (она содержит форматы в виде %-ов) и записывает строку-результат посимвольно (вызывая putc) в файл fp. Вторая - это всего-навсего fprintf с каналом fp равным stdout. Третяя выдает сформатированную строку не в файл, а записывает ее в массив bf. В конце строки sprintf добавляет нулевой байт '\0' - признак конца.

Для чтения данных по формату используются функции семейства

     fscanf(fp, fmt, /* адреса арг-тов */...);
      scanf(    fmt, ... );
     sscanf(bf, fmt, ... );
 

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

5. Структуры данных.

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

     struct {
             int x, y;   /* два целых поля */
             char s[10]; /* и одно - для строки */
     } s1;
 
Структурный тип может иметь имя:
     struct XYS {
             int x, y;     /* два целых поля */
             char str[10]; /* и одно - для строки  */
     };
 
Здесь мы объявили тип, но не отвели ни одной переменной этого типа (хотя могли бы). Теперь опишем переменную этого типа и указатель на нее:
     struct XYS s2, *sptr = &s2;
 
Доступ к полям структуры производится по имени поля (а не по индексу, как у массивов):
имя_структурной_переменной.имя_поля
указатель_на_структуру -> имя_поля

то есть

           не                        а
     #define ВЕС  0            struct { int вес, рост; } x;
     #define РОСТ 1            x.рост = 175;
     int x[2]; x[РОСТ] = 175;
 
Например
     s1.x = 13;
     strcpy(s2.str, "Finish");
     sptr->y = 27;
 
Структура может содержать структуры другого типа в качестве полей:
     struct XYS_Z {
             struct XYS xys;
             int z;
     } a1;
     a1.xys.x = 71; a1.z = 12;
 

Структура того же самого типа не может содержаться в качестве поля - рекурсивные определения запрещены. Зато нередко используются поля - ссылки на структуры такого же типа (или другого). Это позволяет организовывать списки структур:

     struct node {
             int value;
             struct node *next;
     };
 
Очень часто используются массивы структур:
     struct XYS array[20]; int i = 5, j;
     array[i].x = 12;
     j = array[i].x;
 
Статические структуры можно описывать с инициализацией, перечисляя значения их полей в {} через запятую:
     extern struct node n2;
     struct node n1 = { 1, &n2  },
                 n2 = { 2, &n1  },
                 n3 = { 3, NULL };
 

В этом примере n2 описано предварительно для того, чтобы &n2 в строке инициализации n1 было определено.

Структуры одинакового типа можно присваивать целиком (что соответствует присваиванию каждого из полей):

     struct XYS s1, s2;  ...
     s2 = s1;
 
в отличие от массивов, которые присваивать целиком нельзя:
     int a[5], b[5];  a = b; /* ОШИБОЧНО ! */
 
Пример обращения к полям структуры:
     typedef struct _Point {
         short x, y; /* координаты точки */
         char *s;    /* метка точки      */
     } Point;
     Point p; Point *pptr; short *iptr;
     struct _Curve {
       Point points[25]; /* вершины ломанной */
       int color;        /* цвет линии       */
     } aLine[10], *linePtr = & aLine[0];
             ...
     pptr = &p; /* указатель на структуру p */
     p.x = 1; p.y = 2; p.s = "Grue";
     linePtr->points[2].x  = 54; aLine[5].points[0].y  = 17;
 
                    В ы р а ж е н и е                 значение
     ---------+------------+------------+-----------+----------
     p.x      | pptr->x    | (*pptr).x  | (&p)->x   | 1
     ---------+------------+------------+-----------+----------
                                      &p->x    | ошибка
     -----------+----------------+------------------+----------
     iptr= &p.x | iptr= &pptr->x | iptr= &(pptr->x) | адрес поля
     -----------+----------------+--------+---------+----------
     *pptr->s   | *(pptr->s)     | *p.s   |  p.s[0] | 'G'
     -----------+----------------+--------+---------+----------
     pptr->s[1] | (&p)->s[1]     |  p.s[1]          | 'r'
     -----------+----------------+------------------+----------
              &p->s[1]                         | ошибка
     -----------+----------------+------------------+----------
     (*pptr).s  | pptr->s        | p.s              | "Grue"
     -----------+----------------+------------------+----------
     *pptr.s                                        | ошибка
     -----------------------------------------------+----------
 
             Вообще (&p)->field   =  p.field
                    pptr->field   =  (*pptr).field
 

Объединения - это агрегаты данных, которые могут хранить в себе значения данных разных типов на одном и том же месте.

             struct a{ int x, y; char *s; } A;
             union  b{ int i; char *s; struct a aa; } B;
 
             Структура:
        ________________________
     A: | A.x         int      |   Три поля
        ------------------------   расположены подряд.
        | A.y         int      |   Получается как бы
        ------------------------   "карточка" с графами.
        | A.s         char *   |
        -----------------------
     А у объединений поля расположены "параллельно",
     на одном месте в памяти.
        _______________________________________________________
     B: | B.i  int | B.s  char *  | B.aa     : B.aa.x  int    |
        -----------|              | struct a : B.aa.y  int    |
                   ---------------|          : B.aa.s  char * |
                                  |___________________________|
 

Это как бы "ящик" в который можно поместить значение любого типа из перечисленных, но не ВСЕ ВМЕСТЕ ("и то и это", как у структур), а ПО ОЧЕРЕДИ ("или/или"). Размер его достаточно велик, чтоб вместить самый большой из перечисленных типов данных.

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

      union lb {
             char s[2]; short i;
      } x;
      unsigned hi, lo;
      x.i = (02 << 8) | 01;
      hi = x.s[1]; lo = x.s[0];
      printf( "%d %d\n", hi, lo);
 
или так:
     #include <stdio.h>
     union {
             int i;
             unsigned char s[sizeof(int)];
     } u;
     void main(){
             unsigned char *p;
             int n;
             u.i = 0x12345678;
             for(n=0, p=u.s; n < sizeof(int); n++, p++){
                     printf("%02X ", *p);
             }
             putchar('\n');
     }
 
или порядок слов в long числах:
     union xx {
       long l;
       struct ab {
         short a;  /* low word  */
         short b;  /* high word */
       } ab;
     } c;
     main(){ /* На IBM PC 80386 печатает 00020001 */
       c.ab.a = 1; c.ab.b = 2; printf("%08lx\n", c.l );
     }
 

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;
        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;
     }
     /* вставка после 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
     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;
 
Зато доступ к ячейкам массива выглядит теперь менее изящно:
     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 ) ...;
 

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

К битовым полям нельзя применить операцию взятия адреса "&", у них нет адресов и смещений!

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;
     }
     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();
     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--;
     }
 

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
      связан с        клавиатурой     дисплеем
 
     #include <stdio.h>
     main(ac, av) char **av; {
       execl("/bin/sleep", "Take it easy", "1000", NULL);
     }
 

Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, программа может сама явно открывать файлы (при помощи 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 является многозадачной системой, непосредственно вытекает из наличия системных вызовов fork, exec, wait и спецификации их функционирования! То же можно сказать про язык Си - мобильность программы зависит в основном от набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта самого языка, который должен удовлетворять стандарту на язык Си). Если две разные системы предоставляют все эти функции (которые могут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково.

Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия", т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме, в котором имеет доступ к некоторым системным таблицам*, регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от 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); /* принудительное завершение программы */
     }
 

Предопределенный массив 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, каждая описывает одно имя файла, входящего в каталог:

     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. Означает право создавать и уничтожать в каталоге имена файлов при помощи этих вызовов. То есть: право создавать, удалять и переименовывать файлы в каталоге. Отметим, что для переименования или удаления файла вам не требуется иметь доступ по записи к самому файлу - достаточно иметь доступ по записи к каталогу, содержащему его имя!
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 реализуется через эти функции.

     #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(имя_файла);

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

* - Именно это имя показывает команда ps -ef

* - Собственно, операционная система характеризуется набором предоставляемых ею системных вызовов, поскольку все концепции, заложенные в системе, доступны нам только через них. Если мы имеем две реализации системы с разным внутренним устройством ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться, но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX-ами на однопроцессорных и многопроцессорных машинах. Но для нас ядро - это "черный ящик", полностью определяемый его поведением, т.е. своим интерфейсом с программами, но не внутренним устройством. Вторым параметром, характеризующим ОС, являются форматы данных, используемые системой: форматы данных для сисвызовов и формат информации в различных файлах, в том числе формат оформления выполняемых файлов (формат данных в физической памяти машины в этот список не входит - он зависим от реализации и от процессора). Как правило, программа пишется так, чтобы использовать соглашения, принятые в данной системе, для чего она просто включает ряд стандартных include-файлов с описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы.

* - Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого процесса), и.т.п.

* - Суперпользователь (superuser) имеет uid==0. Это "привелегированный" пользователь, который имеет право делать ВСЕ. Ему доступны любые сисвызовы и файлы, несмотря на коды доступа и.т.п.

6.1.5. Напишите функцию рекурсивного обхода дерева подкаталогов и печати имен всех файлов в нем. Ключ U42 означает файловую систему с длинными именами файлов (BSD 4.2).

     /*#!/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 для продолжения. */
     /* Обойти дерево с корнем в 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;
         }
     }
 
     /* Обработать каталог: прочитать его и найти подкаталоги */
     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;                          }
         }
 
     #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;
     }
 
     /* -------------------------------------------------------------- */
     /* Рекурсивное удаление файлов и каталогов                        */
     /* -------------------------------------------------------------- */
     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);
     }
 
     /* -------------------------------------------------------------- */
     #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.4. Сигналы.

Процессы в UNIX используют много разных механизмов взаимодействия. Одним из них являются сигналы.

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

Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожидает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС выступает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процессора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.).

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

Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в includeфайле <signal.h> и имеют вид SIGнечто. Допустимы сигналы с номерами 1..NSIG-1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями:

  • От другого процесса, который явно посылает его нам вызовом
             kill(pid, sig);
    где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользователем.
  • От операционной системы. Система может посылать процессу ряд сигналов, сигнализирующих об ошибках, например при обращении программы по несуществующему адресу или при ошибочном номере системного вызова. Такие сигналы обычно прекращают наш процесс.
  • От пользователя - с клавиатуры терминала можно нажимом некоторых клавиш послать сигналы SIGINT и SIGQUIT. Собственно, сигнал посылается драйвером терминала при получении им с клавиатуры определенных символов. Так можно прервать зациклившуюся или надоевшую программу.

Процесс-получатель должен как-то отреагировать на сигнал. Программа может:

  • проигнорировать сигнал (не ответить на звонок);
  • перехватить сигнал (снять трубку), выполнить какие-то действия, затем продолжить прерванное занятие;
  • быть убитой сигналом (звонок был подкреплен броском гранаты в окно);

В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом 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. По сигналу вызывается функция-эмулятор плавающей арифметики (подключаемая к выполняемому файлу автоматически), которая и обрабатывает требуемую команду. Это может происходить много раз, именно поэтому реакция на этот сигнал не сбрасывается.
  • 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);
 

и вызывает задержку выполнения процесса до прихода сигнала 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), поскольку функция эта все равно внутри себя издает системный вызов 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>
     #include <stdio.h>
     #include <fcntl.h>
     #include <sys/types.h>
     #include <sys/stat.h>
     char buffer [ BSIZE ];
     extern int errno;       /* код ошибки */
 
     void timeout(nsig){ signal( SIGALRM, timeout ); }
     void main(argc, argv) char **argv;{
        int fd, n, trys = 0;  struct stat stin, stout;
 
        if( argc != 2 ){
            fprintf(stderr, "Вызов: %s файл\n", argv[0]); exit(1);
        }
        if((fd = !strcmp(argv[1],"-")? STDIN : open(argv[1],O_RDONLY)) < 0){
            fprintf(stderr, "Не могу читать %s\n", argv[1]); exit(2);
        }
        /* Проверить, что ввод не совпадает с выводом,
         *     hardcat aFile >> aFile
         * кроме случая, когда вывод - терминал.
         * Такая проверка полезна для программ-фильтров (STDIN->STDOUT),
         * чтобы исключить порчу исходной информации */
        fstat(fd, &stin); fstat(STDOUT, &stout);
        if( !isatty(STDOUT) && stin.st_ino == stout.st_ino &&
                               stin.st_dev == stout.st_dev
        ){ fprintf(stderr,
           "\aВвод == выводу, возможно потеряна информация в %s.\n",argv[1]);
           exit(33);
        }
 
        signal( SIGALRM, timeout );
        while( trys < MAX_TRYS ){
                alarm( WAIT_TIME ); /* заказать сигнал через 5 сек */
 
                /* и ждем ввода ... */
                n = read( fd, buffer, BSIZE );
 
                alarm(0);       /* отменили заказ сигнала */
                      /* (хотя, возможно, он уже получен) */
 
                /* проверяем: почему мы слезли с вызова read() ? */
                if( n < 0 && errno == EINTR ){
                    /* Мы были сбиты сигналом SIGALRM,
                     * код ошибки EINTR - сисвызов прерван
                     * неким сигналом.
                     */
                    fprintf( stderr, "\7timed out (%d раз)\n", ++trys );
                    continue;
                }
 
                if( n < 0 ){
                    /* ошибка чтения */
                    fprintf( stderr, "read error.\n" ); exit(4);
                }
                if( n == 0 ){
                    /* достигнут конец файла */
                    fprintf( stderr, "Достигнут EOF.\n\n" ); exit(0);
                }
                /* копируем прочитанную информацию */
                write( STDOUT, buffer, n );
                trys = 0;
        }
        fprintf( stderr, "Все попытки провалились.\n" ); exit(5);
     }
 

Если мы хотим, чтобы сисвызов не мог прерываться сигналом, мы должны защитить его:

     #include <signal.h>
     void (*fsaved)();
             ...
     fsaved = signal (sig, SIG_IGN);
        sys_call(...);
     signal (sig, fsaved);
 

или так:

     sighold(sig);
        sys_call(...);
     sigrelse(sig);
 

Сигналами могут быть прерваны не все системные вызовы и не при всех обстоятельствах.

6.4.3. Напишите функцию sleep(n), задерживающую выполнение программы на n секунд. Воспользуйтесь системным вызовом alarm(n) (будильник) и вызовом pause(), который задерживает программу до получения любого сигнала. Предусмотрите рестарт при получении во время ожидания другого сигнала, нежели SIGALRM. Сохраняйте заказ alarm, сделанный до вызова sleep (alarm выдает число секунд, оставшееся до завершения предыдущего заказа). На самом деле есть такая СТАНДАРТНАЯ функция. Ответ:

     #include <sys/types.h>
     #include <stdio.h>
     #include <signal.h>
 
     int got;   /* пришел ли сигнал */
 
     void onalarm(int sig)
     { printf( "Будильник\n" ); got++; } /* сигнал получен */
 
     void sleep(int n){
        time_t time(), start = time(NULL);
        void (*save)();
        int oldalarm, during = n;
 
        if( n <= 0 ) return;
        got = 0;
        save = signal(SIGALRM, onalarm);
        oldalarm = alarm(3600); /* Узнать старый заказ */
        if( oldalarm ){
            printf( "Был заказан сигнал, который придет через %d сек.\n",
                     oldalarm );
            if(oldalarm > n) oldalarm -= n;
            else { during = n = oldalarm; oldalarm = 1; }
        }
        printf( "n=%d oldalarm=%d\n", n, oldalarm );
        while( n > 0 ){
          printf( "alarm(%d)\n", n );
          alarm(n);  /* заказать SIGALRM через n секунд */
 
          pause();
 
          if(got) break;
          /* иначе мы сбиты с pause другим сигналом */
          n = during - (time(NULL) - start); /* прошло времени */
        }
        printf( "alarm(%d) при выходе\n", oldalarm );
        alarm(oldalarm);  /* alarm(0) - отмена заказа сигнала */
        signal(SIGALRM, save); /* восстановить реакцию */
     }
 
     void onintr(int nsig){
        printf( "Сигнал SIGINT\n"); signal(SIGINT, onintr);
     }
 
     void onOldAlarm(int nsig){
        printf( "Звонит старый будильник\n");
     }
 
     void main(){
             int time1 = 0;  /* 5, 10, 20 */
             setbuf(stdout, NULL);
             signal(SIGINT, onintr);
             signal(SIGALRM, onOldAlarm); alarm(time1);
                         sleep(10);
             if(time1) pause();
             printf("Чао!\n");
     }
 

6.4.4. Напишите "часы", выдающие текущее время каждые 3 секунды.

     #include <signal.h>
     #include <time.h>
     #include <stdio.h>
     void tick(nsig){
        time_t tim; char *s;
        signal (SIGALRM, tick);
        alarm(3); time(&tim);
        s = ctime(&tim);
        s[ strlen(s)-1 ] = '\0'; /* обрубить '\n' */
        fprintf(stderr, "\r%s", s);
     }
     main(){ tick(0);
             for(;;) pause();
     }
 

6.5. Жизнь процессов.

6.5.1. Какие классы памяти имеют данные, в каких сегментах программы они расположены?

             char x[] = "hello";
             int y[25];
             char *p;
             main(){
                     int z = 12;
                     int v;
                     static int w = 25;
                     static int q;
                     char s[20];
                     char *pp;
                     ...
                     v = w + z;      /* #1 */
             }
 

Ответ:

     Переменная  Класс памяти     Сегмент   Начальное значение
          x        static        data/DATA      "hello"
          y        static        data/BSS     {0, ..., 0}
          p        static        data/BSS        NULL
          z        auto          stack            12
          v        auto          stack        не определено
          w        static        data/DATA        25
          q        static        data/BSS          0
          s        auto          stack        не определено
          pp       auto          stack        не определено
          main     static        text/TEXT
 

Большими буквами обозначены сегменты, хранимые в выполняемом файле:

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

BSS (Block Started by Symbol) - неинициализированные статические данные. Они по умолчанию имеют начальное значение 0 (NULL, "", '\0'). Эта память расписывается нулями при запуске программы, а в файле хранится лишь ее размер.

TEXT - сегмент, содержащий машинные команды (код).

Хранящаяся в файле выполняемая программа имеет также заголовок - в нем в частности содержатся размеры перечисленных сегментов и их местоположение в файле; и еще - в самом конце файла - таблицу имен. В ней содержатся имена всех функций и переменных, используемых в программе, и их адреса. Эта таблица используется отладчиками adb и sdb, а также при сборке программы из нескольких объектных файлов программой ld. Просмотреть ее можно командой

     nm имяФайла

Для экономии дискового пространства эту таблицу часто удаляют, что делается командой

     strip имяФайла

Размеры сегментов можно узнать командой

     size имяФайла

Программа, загруженная в память компьютера (т.е. процесс), состоит из 3x сегментов, относящихся непосредственно к программе:

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

data - сегмент, в который склеены сегменты статических данных DATA и BSS, загруженные из файла. Этот сегмент также может изменять свой размер, но делать это надо явно - системными вызовами sbrk или brk. В частности, функция malloc() для размещения динамически отводимых данных увеличивает размер этого сегмента.

text - это выполняемые команды, копия сегмента TEXT из файла. Так строка с меткой #1 содержится в виде машинных команд именно в этом сегменте.

Кроме того, каждый процесс имеет еще:

proc - это резидентная часть паспорта процесса в таблице процессов в ядре операционной системы;

user - это 4-ый сегмент процесса - нерезидентная часть паспорта (u-area). К этому сегменту имеет доступ только ядро, но не сама программа.

Паспорт процесса был поделен на 2 части только из соображений экономии памяти в ядре: контекст процесса (таблица открытых файлов, ссылка на I-узел текущего каталога, таблица реакций на сигналы, ссылка на I-узел управляющего терминала, и.т.п.) нужен ядру только при обслуживании текущего активного процесса. Когда активен другой процесс эта информация в памяти ядра не нужна. Более того, если процесс из-за нехватки места в памяти машины был откачан на диск, эта информация также может быть откачана на диск и подкачана назад лишь вместе с процессом. Поэтому контекст был выделен в отдельный сегмент, и сегмент этот подключается к адресному пространству ядра лишь при выполнении процессом какого-либо системного вызова (это подключение называется "переключение контекста" - context switch). Четыре сегмента процесса могут располагаться в памяти машины не обязательно подряд - между ними могут лежать сегменты других процессов.

Схема составных частей процесса:

             П  Р  О  Ц  Е  С  С
       таблица процессов:
       паспорт  в ядре       сегменты в памяти
 
        struct proc[]
             ####---------------> stack      1
             ####                 data       2
                                  text       3
                    контекст:   struct user  4
 

Каждый процесс имеет уникальный номер, хранящийся в поле p_pid в структуре proc*. В ней также хранятся: адреса сегментов процесса в памяти машины (или на диске, если процесс откачан); p_uid - номер владельца процесса; p_ppid - номер процесса-родителя; p_pri, p_nice - приоритеты процесса; p_pgrp - группа процесса; p_wchan - ожидаемое процессом событие; p_flag и p_stat - состояние процесса; и многое другое. Структура proc определена в include-файле <sys/proc.h>, а структура user - в <sys/user.h>.

6.5.2. Системный вызов fork() (вилка) создает новый процесс: копию процесса, издавшего вызов. Отличие этих процессов состоит только в возвращаемом fork-ом значении:

     0                   - в новом процессе.
     pid нового процесса - в исходном.
 

Вызов fork может завершиться неудачей если таблица процессов переполнена. Простейший способ сделать это:

     main(){
           while(1)
             if( ! fork()) pause();
     }
 

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

Вызов fork создает копию всех 4х сегментов процесса и выделяет порожденному процессу новый паспорт и номер. Иногда сегмент text не копируется, а используется процессами совместно ("разделяемый сегмент") в целях экономии памяти. При копировании сегмента user контекст порождающего процесса наследуется порожденным процессом (см. ниже).

Проведите опыт, доказывающий что порожденный системным вызовом fork() процесс и породивший его - равноправны. Повторите несколько раз программу:

     #include <stdio.h>
     int pid, i, fd; char c;
     main(){
        fd = creat( "TEST", 0644);
        if( !(pid = fork())){ /* сын: порожденный процесс */
                c = 'a';
                for(i=0; i < 5; i++){
                  write(fd, &c, 1); c++; sleep(1);
                }
                printf("Сын %d окончен\n", getpid());
                exit(0);
        }
        /* else процесс-отец */
        c = 'A';
        for(i=0; i < 5; i++){
                write(fd, &c, 1); c++; sleep(1);
        }
        printf("Родитель %d процесса %d окончен\n",
                getpid(), pid );
     }
 

В файле TEST мы будем от случая к случаю получать строки вида

    aABbCcDdEe  или  AaBbcdCDEe

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

Данный опыт основан на следующем свойстве системы UNIX: при системном вызове fork() порожденный процесс получает все открытые порождающим процессом файлы "в наследство" - это соответствует тому, что таблица открытых процессом файлов копируется в процесс-потомок. Именно так, в частности, передаются от отца к сыну стандартные каналы 0, 1, 2: порожденному процессу не нужно открывать стандартные ввод, вывод и вывод ошибок явно. Изначально же они открываются специальной программой при вашем входе в систему.

до вызова fork();

      таблица открытых
      файлов  процесса
         0   ## ---<--- клавиатура
         1   ## --->--- дисплей
         2   ## --->--- дисплей
         ... ##
         fd  ## --->--- файл TEST
         ... ##
 

после fork();

      ПРОЦЕСС-ПАПА                   ПРОЦЕСС-СЫН
      0   ## ---<--- клавиатура --->--- ## 0
      1   ## --->--- дисплей    ---<--- ## 1
      2   ## --->--- дисплей    ---<--- ## 2
      ... ##                            ## ...
      fd  ## --->--- файл TEST  ---<--- ## fd
      ... ##            |               ## ...
                        *--RWptr-->ФАЙЛ
 

Ссылки из таблиц открытых файлов в процессах указывают на структуры "открытый файл" в ядре (см. главу про файлы). Таким образом, два процесса получают доступ к одной и той же структуре и, следовательно, имеют общий указатель чтения/записи для этого файла. Поэтому, когда процессы "отец" и "сын" пишут по дескриптору fd, они пользуются одним и тем же указателем R/W, т.е. информация от обоих процессов записывается последовательно. На принципе наследования и совместного использования открытых файлов основан также системный вызов pipe.

Порожденный процесс наследует также: реакции на сигналы (!!!), текущий каталог, управляющий терминал, номер владельца процесса и группу владельца, и.т.п.

При системном вызове exec() (который заменяет программу, выполняемую процессом, на программу из указанного файла) все открытые каналы также достаются в наследство новой программе (а не закрываются).

6.5.3. Процесс-копия это хорошо, но не совсем то, что нам хотелось бы. Нам хочется запустить программу, содержащуюся в выполняемом файле (например a.out). Для этого существует системный вызов exec, который имеет несколько разновидностей. Рассмотрим только две:

     char *path;
     char *argv[], *envp[], *arg0, ..., *argn;
     execle(path, arg0, arg1, ..., argn, NULL, envp);
     execve(path, argv, envp);
 

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

     /usr/bin/vi   a.out  ../mybin/xkick
 

Файл должен иметь код доступа "выполнение". Первые два байта файла (в его заголовке), рассматриваемые как short int, содержат так называемое "магическое число" (A_MAGIC), свое для каждого типа машин (смотри include-файл <a.out.h>). Его помещает в начало выполняемого файла редактор связей ld при компоновке программы из объектных файлов. Это число должно быть правильным, иначе система откажется запускать программу из этого файла. Бывает несколько разных магических чисел, обозначающих разные способы организации программы в памяти. Например, есть вариант, в котором сегменты text и data склеены вместе (тогда text не разделяем между процессами и не защищен от модификации программой), а есть - где данные и текст находятся в раздельных адресных пространствах и запись в text запрещена (аппаратно).

Остальные аргументы вызова - arg0, ..., argn - это аргументы функции main новой программы. Во второй форме вызова аргументы не перечисляются явно, а заносятся в массив. Это позволяет формировать произвольный массив строк-аргументов во время работы программы:

     char *argv[20];
     argv[0]="ls"; argv[1]="-l"; argv[2]="-i"; argv[3]=NULL;
     execv( "/bin/ls", argv);
             либо
     execl( "/bin/ls", "ls","-l","-i", NULL):
 

В результате этого вызова текущая программа завершается (но не процесс!) и вместо нее запускается программа из заданного файла: сегменты stack, data, text старой программы уничтожаются; создаются новые сегменты data и text, загружаемые из файла path; отводится сегмент stack (первоначально - не очень большого размера); сегмент user сохраняется от старой программы (за исключением реакций на сигналы, отличных от SIG_DFL и SIG_IGN - они будут сброшены в SIG_DFL). Затем будет вызвана функция main новой программы с аргументами argv:

     void main( argc, argv )
         int argc; char *argv[]; { ... }
 

Количество аргументов - argc - подсчитает сама система. Строка NULL не подсчитывается.

Процесс остается тем же самым - он имеет тот же паспорт (только адреса сегментов изменились); тот же номер (pid); все открытые прежней программой файлы остаются открытыми (с теми же дескрипторами); текущий каталог также наследуется от старой программы; сигналы, которые игнорировались ею, также будут игнорироваться (остальные сбрасываются в SIG_DFL). Зато "сущность" процесса подвергается перерождению - он выполняет теперь иную программу. Таким образом, системный вызов exec осуществляет вызов функции main, находящейся в другой программе, передавая ей свои аргументы в качестве входных.

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

Аргумент argv[0] обычно полагают равным path. По нему программа, имеющая несколько имен (в файловой системе), может выбрать ЧТО она должна делать. Так программа /bin/ls имеет альтернативные имена lr, lf, lx, ll. Запускается одна и та же программа, но в зависимости от argv[0] она далее делает разную работу.

Аргумент envp - это "окружение" программы (см. начало этой главы). Если он не задан - передается окружение текущей программы (наследуется содержимое массива, на который указывает переменная environ); если же задан явно (например, окружение скопировано в какой-то массив и часть переменных подправлена или добавлены новые переменные) - новая программа получит новое окружение. Напомним, что окружение можно прочесть из предопределенной переменной char **environ, либо из третьего аргумента функции main (см. начало главы), либо функцией getenv().

Системные вызовы fork и exec не склеены в один вызов потому, что между fork и exec в процессе-сыне могут происходить некоторые действия, нарушающие симметрию процесса-отца и порожденного процесса: установка реакций на сигналы, перенаправление ввода/вывода, и.т.п. Смотри пример "интерпретатор команд" в приложении. В MS DOS, не имеющей параллельных процессов, вызовы fork, exec и wait склеены в один вызов spawn. Зато при этом приходится делать перенаправления ввода-вывода в порождающем процессе перед spawn, а после него - восстанавливать все как было.

6.5.4. Завершить процесс можно системным вызовом

    void exit( unsigned char retcode );

Из этого вызова не бывает возврата. Процесс завершается: сегменты stack, data, text, user уничтожаются (при этом все открытые процессом файлы закрываются); память, которую они занимали, считается свободной и в нее может быть помещен другой процесс. Причина смерти отмечается в паспорте процесса - в структуре proc в таблице процессов внутри ядра. Но паспорт еще не уничтожается! Это состояние процесса называется "зомби" - живой мертвец.

В паспорт процесса заносится код ответа retcode. Этот код может быть прочитан процессом-родителем (тем, кто создал этот процесс вызовом fork). Принято, что код 0 означает успешное завершение процесса, а любое положительное значение 1..255 означает неудачное завершение с таким кодом ошибки. Коды ошибок заранее не предопределены: это личное дело процессов отца и сына - установить между собой какие-то соглашения по этому поводу. В старых программах иногда писалось exit(-1); Это некорректно - код ответа должен быть неотрицателен; код -1 превращается в код 255. Часто используется конструкция exit(errno);

Программа может завершиться не только явно вызывая exit, но и еще двумя способами:

  • если происходит возврат управления из функции main(), т.е. она кончилась - то вызов exit() делается неявно, но с непредсказуемым значением retcode;
  • процесс может быть убит сигналом. В этом случае он не выдает никакого кода ответа в процесс-родитель, а выдает признак "процесс убит".

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

     void exit(unsigned code){
       /* Добавленный мной дополнительный оператор: */
       printf("Закончить работу, "
              "код ответа=%u\n", code);
 
       /* Стандартные операторы: */
       _cleanup();  /* закрыть все открытые файлы.
                     * Это стандартная функция **/
       _exit(code); /* собственно сисвызов */
     }
 
     int f(){ return 17; }
     void main(){
       printf("aaaa\n"); printf("bbbb\n"); f();
       /* потом откомментируйте это:  exit(77); */
     }
 

Здесь функция exit вызывается неявно по окончании main, ее подставляет в программу компилятор. Дело в том, что при запуске программы exec-ом, первым начинает выполняться код так называемого "стартера", подклеенного при сборке программы из файла /lib/crt0.o. Он выглядит примерно так (в действительности он написан на ассемблере):

     ... // вычислить argc, настроить некоторые параметры.
     main(argc, argv, envp);
     exit();
 

или так (взято из проекта GNU* -):

     int errno = 0;
     char **environ;
     _start(int argc, int arga)
     {
     /* OS and Compiler dependent!!!! */
     char **argv = (char **) &arga;
     char **envp = environ = argv + argc + 1;
     /* ... возможно еще какие-то инициализации,
      * наподобие setlocale( LC_ALL, "" ); в SCO UNIX */
     exit (main(argc, argv, envp));
     }
 

Где должно быть

     int main(int argc, char *argv[], char *envp[]){
               ...
             return 0;  /* вместо exit(0); */
     }
 

Адрес функции _start() помечается в одном из полей заголовка файла формата a.out как адрес, на который система должна передать управление после загрузки программы в память (точка входа).

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

6.5.6. Процесс-отец может дождаться окончания своего потомка. Это делается системным вызовом wait и нужно по следующей причине: пусть отец - это интерпретатор команд. Если он запустил процесс и продолжил свою работу, то оба процесса будут предпринимать попытки читать ввод с клавиатуры терминала - интерпретатор ждет команд, а запущенная программа ждет данных. Кому из них будет поступать набираемый нами текст - непредсказуемо! Вывод: интерпретатор команд должен "заснуть" на то время, пока работает порожденный им процесс:

     int pid;  unsigned short status;
              ...
     if((pid = fork()) == 0 ){
             /* порожденный процесс */
              ...  // перенаправления ввода-вывода.
              ...  // настройка сигналов.
             exec(....);
             perror("exec не удался"); exit(1);
     }
     /* иначе это породивший процесс */
     while((pid = wait(&status)) > 0 )
       printf("Окончился сын pid=%d с кодом %d\n",
               pid, status >> 8);
     printf( "Больше нет сыновей\n");
 

wait приостанавливает* - выполнение вызвавшего процесса до момента окончания любого из порожденных им процессов (ведь можно было запустить и нескольких сыновей!). Как только какой-то потомок окончится - wait проснется и выдаст номер (pid) этого потомка. Когда никого из живых "сыновей" не осталось - он выдаст (-1). Ясно, что процессы могут оканчиваться не в том порядке, в котором их порождали. В переменную status заносится в специальном виде код ответа окончившегося процесса, либо номер сигнала, которым он был убит.

     #include <sys/types.h>
     #include <sys/wait.h>
             ...
     int status, pid;
             ...
     while((pid = wait(&status)) > 0){
         if( WIFEXITED(status)){
           printf( "Процесс %d умер с кодом %d\n",
                            pid,            WEXITSTATUS(status));
         } else if( WIFSIGNALED(status)){
           printf( "Процесс %d убит сигналом %d\n",
                            pid,             WTERMSIG(status));
           if(WCOREDUMP(status)) printf( "Образовался core\n" );
           /* core - образ памяти процесса для отладчика adb */
         } else if( WIFSTOPPED(status)){
           printf( "Процесс %d остановлен сигналом %d\n",
                            pid,            WSTOPSIG(status));
         } else if( WIFCONTINUED(status)){
           printf( "Процесс %d продолжен\n",
                            pid);
         }
     }
             ...
 

Если код ответа нас не интересует, мы можем писать wait(NULL).

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

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

Если процесс-отец завершился раньше своих сыновей, то кто же сделает wait и вычеркнет паспорт? Это сделает процесс номер 1: /etc/init. Если отец умер раньше процессов-сыновей, то система заставляет процесс номер 1 "усыновить" эти процессы. init обычно находится в цикле, содержащем в начале вызов wait(), то есть ожидаетокончания любого из своих сыновей (а они у него всегда есть, о чем мы поговорим подробнее чуть погодя). Таким образом init занимается чисткой таблицы процессов, хотя это не единственная его функция.

Вот схема, поясняющая жизненный цикл любого процесса:

         |pid=719,csh
         |
     if(!fork())------->--------* pid=723,csh
         |                      |                  загрузить
      wait(&status)           exec("a.out",...) <-- a.out
         :                    main(...){           с диска
         :                      |
         :pid=719,csh           | pid=723,a.out
       спит(ждет)             работает
         :                      |
         :                    exit(status) умер
         :                    }
      проснулся <---проснись!--RIP
         |
         |pid=719,csh
 

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

6.5.7. Кроме того, wait позволяет отслеживать остановку процесса. Процесс может быть приостановлен при помощи посылки ему сигналов SIGSTOP, SIGTTIN, SIGTTOU, SIGTSTP. Последние три сигнала посылает при определенных обстоятельствах драйвер терминала, к примеру SIGTSTP - при нажатии клавиши CTRL/Z. Продолжается процесс посылкой ему сигнала SIGCONT.

В данном контексте, однако, нас интересуют не сами эти сигналы, а другая схема манипуляции с отслеживанием статуса порожденных процессов. Если указано явно, система может посылать процессу-родителю сигнал SIGCLD в момент изменения статуса любого из его потомков. Это позволит процессу-родителю немедленно сделать wait и немедленно отразить изменение состояние процесса-потомка в своих внутренних списках. Данная схема программируется так:

     void pchild(){
             int pid, status;
 
             sighold(SIGCLD);
             while((pid = waitpid((pid_t) -1, &status, WNOHANG|WUNTRACED)) > 0){
               dorecord:
                     записать_информацию_об_изменениях;
             }
             sigrelse(SIGCLD);
 
             /* Reset */
             signal(SIGCLD, pchild);
     }
             ...
     main(){
             ...
             /* По сигналу SIGCLD вызывать функцию pchild */
             signal(SIGCLD, pchild);
             ...
             главный_цикл;
     }
 

Секция с вызовом waitpid (разновидность вызова wait), прикрыта парой функций sighold-sigrelse, запрещающих приход сигнала SIGCLD внутри этой критической секции. Сделано это вот для чего: если процесс начнет модифицировать таблицы или списки в районе метки dorecord:, а в этот момент придет еще один сигнал, то функция pchild будет вызвана рекурсивно и тоже попытается модифицировать таблицы и списки, в которых еще остались незавершенными перестановки ссылок, элементов, счетчиков. Это приведет к разрушению данных.

Поэтому сигналы должны приходить последовательно, и функции pchild вызываться также последовательно, а не рекурсивно. Функция sighold откладывает доставку сигнала (если он случится), а sigrelse - разрешает доставить накопившиеся сигналы (но если их пришло несколько одного типа - все они доставляются как один такой сигнал. Отсюда цикл вокруг waitpid).

Флаг WNOHANG - означает "не ждать внутри вызова wait", если ни один из потомков не изменил своего состояния; а просто вернуть код (-1)". Это позволяет вызывать pchild даже без получения сигнала: ничего не произойдет. Флаг WUNTRACED - означает "выдавать информацию также об остановленных процессах".

* - Процесс может узнать его вызовом pid=getpid();

* - cleanup() закрывает файлы, открытые fopen()ом, "вытряхая" при этом данные, накопленные в буферах, в файл. При аварийном завершении программы файлы все равно закрываются, но уже не явно, а операционной системой (в вызове _exit). При этом содержимое недосброшенных буферов будет утеряно.

* - программы, распространяемые в исходных текстах из Free Software Foundationtion (FSF). Среди них - C++ компилятор g++ и редактор emacs. Смысл слов GNU - "generally not UNIX" - проект был основан как противодействие начавшейся коммерциализации UNIX и закрытию его исходных текстов. "Сделать как в UNIX, но лучше".

* - "Живой" процесс может пребывать в одном из нескольких состояний: процесс ожидает наступления какого-то события ("спит"), при этом ему не выделяется время процессора, т.к. он не готов к выполнению; процесс готов к выполнению и стоит в очереди к процессору (поскольку процессор выполняет другой процесс); процесс готов и выполняется процессором в данный момент. Последнее состояние может происходить в двух режимах пользовательском (выполняются команды сегмента text) и системном (процессом был издан системный вызов, и сейчас выполняется функция в ядре). Ожидание события бывает только в системной фазе - внутри системного вызова (т.е. это "синхронное" ожидание). Неактивные процессы ("спящие" или ждущие ресурса процессора) могут быть временно откачаны на диск.

6.5.8. Как уже было сказано, при exec все открытые файлы достаются в наследство новой программе (в частности, если между fork и exec были перенаправлены вызовом dup2 стандартные ввод и вывод, то они останутся перенаправленными и у новой программы). Что делать, если мы не хотим, чтобы наследовались все открытые файлы? (Хотя бы потому, что большинством из них новая программа пользоваться не будет - в основном она будет использовать лишь fd 0, 1 и 2; а ячейки в таблице открытых файлов процесса они занимают). Во-первых, ненужные дескрипторы можно явно закрыть close в промежутке между fork-ом и exec-ом. Однако не всегда мы помним номера дескрипторов для этой операции. Более радикальной мерой является тотальная чистка:

     for(f = 3; f < NOFILE; f++)
             close(f);
 

Есть более элегантный путь. Можно пометить дескриптор файла специальным флагом, означающим, что во время вызова exec этот дескриптор должен быть автоматически закрыт (режим file-close-on-exec - fclex):

     #include <fcntl.h>
     int fd = open(.....);
     fcntl (fd, F_SETFD, 1);
 

Отменить этот режим можно так:

     fcntl (fd, F_SETFD, 0);
 

Здесь есть одна тонкость: этот флаг устанавливается не для структуры file - "открытый файл", а непосредственно для дескриптора в таблице открытых процессом файлов (массив флагов: char u_pofile[NOFILE]). Он не сбрасывается при закрытии файла, поэтому нас может ожидать сюрприз:

      ... fcntl (fd, F_SETFD, 1); ... close(fd);
      ...
      int fd1 = open( ... );
 

Если fd1 окажется равным fd, то дескриптор fd1 будет при exec-е закрыт, чего мы явно не ожидали! Поэтому перед close(fd) полезно было бы отменить режим fclex.

6.5.9. Каждый процесс имеет управляющий терминал (short *u_ttyp). Он достается процессу в наследство от родителя (при fork и exec) и обычно совпадает с терминалом, с на котором работает данный пользователь.

Каждый процесс относится к некоторой группе процессов (int p_pgrp), которая также наследуется. Можно послать сигнал всем процессам указанной группы pgrp:

    kill( -pgrp, sig );

Вызов

    kill( 0, sig );

посылает сигнал sig всем процессам, чья группа совпадает с группой посылающего процесса. Процесс может узнать свою группу:

    int pgrp = getpgrp();

а может стать "лидером" новой группы. Вызов

    setpgrp();

делает следующие операции:

     /* У процесса больше нет управл. терминала: */
     if(p_pgrp != p_pid) u_ttyp = NULL;
     /* Группа процесса полагается равной его ид-у: */
     p_pgrp = p_pid;  /* new group */
 

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

     /* часть процедуры открытия терминала */
     if( p_pid == p_pgrp // лидер группы
      && u_ttyp == NULL  // еще нет упр.терм.
      && t_pgrp == 0 ){  // у терминала нет группы
             u_ttyp = &t_pgrp;
             t_pgrp =  p_pgrp;
     }
 

Таким процессом обычно является процесс регистрации пользователя в системе (который спрашивает у вас имя и пароль). При закрытии терминала всеми процессами (что бывает при выходе пользователя из системы) терминал теряет группу: t_pgrp=0;

При нажатии на клавиатуре терминала некоторых клавиш:

     c_cc[ VINTR ]     обычно DEL или CTRL/C
     c_cc[ VQUIT ]     обычно CTRL/\
 

драйвер терминала посылает соответственно сигналы SIGINT и SIGQUIT всем процессам группы терминала, т.е. как бы делает

    kill( -t_pgrp, sig );

Именно поэтому мы можем прервать процесс нажатием клавиши DEL. Поэтому, если процесс сделал setpgrp(), то сигнал с клавиатуры ему послать невозможно (т.к. он имеет свой уникальный номер группы != группе терминала).

Если процесс еще не имеет управляющего терминала (или уже его не имеет после setpgrp), то он может сделать любой терминал (который он имеет право открыть) управляющим для себя. Первый же файл-устройство, являющийся интерфейсом драйвера терминалов, который будет открыт этим процессом, станет для него управляющим терминалом. Так процесс может иметь каналы 0, 1, 2 связанные с одним терминалом, а прерывания получать с клавиатуры другого (который он сделал управляющим для себя).

Процесс регистрации пользователя в системе - /etc/getty (название происходит от "get tty" - получить терминал) - запускается процессом номер 1 - /etc/init-ом - на каждом из терминалов, зарегистрированных в системе, когда

  • система только что была запущена;
  • либо когда пользователь на каком-то терминале вышел из системы (интерпретатор команд завершился).

В сильном упрощении getty может быть описан так:

     void main(ac, av) char *av[];
     {   int f; struct termio tmodes;
 
         for(f=0; f < NOFILE; f++) close(f);
 
         /* Отказ от управляющего терминала,
          * основание новой группы процессов.
          */
         setpgrp();
 
         /* Первоначальное явное открытие терминала */
         /* При этом терминал av[1] станет упр. терминалом */
             open( av[1], O_RDONLY ); /* fd = 0 */
             open( av[1], O_RDWR   ); /* fd = 1 */
          f = open( av[1], O_RDWR   ); /* fd = 2 */
 
         // ... Считывание параметров терминала из файла
         // /etc/gettydefs. Тип требуемых параметров линии
         // задается меткой, указываемой в av[2].
         // Заполнение структуры tmodes требуемыми
         // значениями ... и установка мод терминала.
         ioctl (f, TCSETA, &tmodes);
 
         // ... запрос имени и пароля ...
 
         chdir (домашний_каталог_пользователя);
 
         execl ("/bin/csh", "-csh", NULL);
         /* Запуск интерпретатора команд. Группа процессов,
          * управл. терминал, дескрипторы 0,1,2 наследуются.
          */
     }
 

Здесь последовательные вызовы open занимают последовательные ячейки в таблице открытых процессом файлов (поиск каждой новой незанятой ячейки производится с начала таблицы) - в итоге по дескрипторам 0,1,2 открывается файл-терминал. После этого дескрипторы 0,1,2 наследуются всеми потомками интерпретатора команд. Процесс init запускает по одному процессу getty на каждый терминал, как бы делая

             /etc/getty /dev/tty01 m &
             /etc/getty /dev/tty02 m &
                     ...
 

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

6.6. Трубы и FIFO-файлы.

Процессы могут обмениваться между собой информацией через файлы. Существуют файлы с необычным поведением - так называемые FIFO-файлы (first in, first out), ведущие себя подобно очереди. У них указатели чтения и записи разделены. Работа с таким файлом напоминает проталкивание шаров через трубу - с одного конца мы вталкиваем данные, с другого конца - вынимаем их. Операция чтения из пустой "трубы" проиостановит вызов read (и издавший его процесс) до тех пор, пока кто-нибудь не запишет в FIFOфайл какие-нибудь данные. Операция позиционирования указателя - lseek() - неприме- нима к FIFO-файлам. FIFO-файл создается системным вызовом

     #include <sys/types.h>
     #include <sys/stat.h>
        mknod( имяФайла, S_IFIFO | 0666, 0 );
 

где 0666 - коды доступа к файлу. При помощи FIFO-файла могут общаться даже неродственные процессы.

Разновидностью FIFO-файла является безымянный FIFO-файл, предназначенный для обмена информацией между процессом-отцом и процессом-сыном. Такой файл - канал связи как раз и называется термином "труба" или pipe. Он создается вызовом pipe:

     int conn[2];   pipe(conn);
 

Если бы файл-труба имел имя PIPEFILE, то вызов pipe можно было бы описать как

     mknod("PIPEFILE", S_IFIFO | 0600, 0);
     conn[0] = open("PIPEFILE", O_RDONLY);
     conn[1] = open("PIPEFILE", O_WRONLY);
     unlink("PIPEFILE");
 

При вызове fork каждому из двух процессов достанется в наследство пара дескрипторов:

                  pipe(conn);
                    fork();
 
     conn[0]----<----    ----<-----conn[1]
                     FIFO
     conn[1]---->----    ---->-----conn[0]
      процесс A                 процесс B
 

Пусть процесс A будет посылать информацию в процесс B. Тогда процесс A сделает:

     close(conn[0]);
     // т.к. не собирается ничего читать
     write(conn[1], ... );
 

а процесс B

     close(conn[1]);
     // т.к. не собирается ничего писать
     read (conn[0], ... );
 

Получаем в итоге:

     conn[1]---->----FIFO---->-----conn[0]
      процесс A                 процесс B
 

Обычно поступают еще более элегантно, перенаправляя стандартный вывод A в канал conn[1]

     dup2 (conn[1], 1); close(conn[1]);
     write(1, ... );   /* или printf */
 

а стандартный ввод B - из канала conn[0]

     dup2(conn[0], 0); close(conn[0]);
     read(0, ... );    /* или gets */
 

Это соответствует конструкции

          $   A | B
 

записанной на языке СиШелл.

Файл, выделяемый под pipe, имеет ограниченный размер (и поэтому обычно целиком оседает в буферах в памяти машины). Как только он заполнен целиком - процесс, пишущий в трубу вызовом write, приостанавливается до появления свободного места в трубе. Это может привести к возникновению тупиковой ситуации, если писать программу неаккуратно. Пусть процесс A является сыном процесса B, и пусть процесс B издает вызов wait, не закрыв канал conn[0]. Процесс же A очень много пишет в трубу conn[1]. Мы получаем ситуацию, когда оба процесса спят:

A потому что труба переполнена, а процесс B ничего из нее не читает, так как ждет окончания A;

B потому что процесс-сын A не окончился, а он не может окончиться пока не допишет свое сообщение.

Решением служит запрет процессу B делать вызов wait до тех пор, пока он не прочитает ВСЮ информацию из трубы (не получит EOF). Только сделав после этого close(conn[0]); процесс B имеет право сделать wait.

Если процесс B закроет свою сторону трубы close(conn[0]) прежде, чем процесс A закончит запись в нее, то при вызове write в процессе A, система пришлет процессу A сигнал SIGPIPE - "запись в канал, из которого никто не читает".

6.6.1. Открытие FIFO файла приведет к блокированию процесса ("засыпанию"), если в буфере FIFO файла пусто. Процесс заснет внутри вызова open до тех пор, пока в буфере что-нибудь не появится.

Чтобы избежать такой ситуации, а, например, сделать что-нибудь иное полезное в это время, нам надо было бы опросить файл на предмет того - можно ли его открыть? Это делается при помощи флага O_NDELAY у вызова open.

     int fd = open(filename, O_RDONLY|O_NDELAY);
 

Если open ведет к блокировке процесса внутри вызова, вместо этого будет возвращено значение (-1). Если же файл может быть немедленно открыт - возвращается нормальный дескриптор со значением >=0, и файл открыт.

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

     #include <stdio.h>
     #include <fcntl.h>
 
     /* Убрать больше не нужный O_NDELAY */
     void nondelay(int fd){
             fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NDELAY);
     }
     int main(int ac, char *av[]){
             int fd;
             char *port = ac > 1 ? "/dev/term/a" : "/dev/cua/a";
 
     retry:  if((fd = open(port, O_RDWR|O_NDELAY)) < 0){
                     perror(port);
                     sleep(10);
                     goto retry;
             }
             printf("Порт %s открыт.\n", port);
             nondelay(fd);
 
             printf("Работа с портом, вызови эту программу еще раз!\n");
             sleep(60);
             printf("Все.\n");
             return 0;
     }
 

Вот протокол:

     su# a.out & a.out xxx
     [1] 22202
     Порт /dev/term/a открыт.
     Работа с портом, вызови эту программу еще раз!
     /dev/cua/a: Device busy
     /dev/cua/a: Device busy
     /dev/cua/a: Device busy
     /dev/cua/a: Device busy
     /dev/cua/a: Device busy
     /dev/cua/a: Device busy
     Все.
     Порт /dev/cua/a открыт.
     Работа с портом, вызови эту программу еще раз!
     su#
 

6.7. Нелокальный переход.

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

     #include <signal.h>
     #include <setjmp.h>
     jmp_buf jmp;  /* контрольная точка */
 
     /* прыгнуть в контрольную точку */
     void onintr(nsig){ longjmp(jmp, nsig); }
 
     main(){
        int n;
        n = setjmp(jmp);  /* установить контрольную точку */
        if( n ) printf( "Рестарт после сигнала %d\n", n);
        signal (SIGINT, onintr);     /* реакция на сигнал */
        printf("Начали\n");
        ...
     }
 

setjmp возвращает 0 при запоминании контрольной точки. При прыжке в контрольную точку при помощи longjmp, мы оказываемся снова в функции setjmp, и эта функция возвращает нам значение второго аргумента longjmp, в этом примере - nsig.

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

6.7.1. Перепишите следующий алгоритм при помощи longjmp.

     #define FOUND    1 /* ответ найден    */
     #define NOTFOUND 0 /* ответ не найден */
     int value;         /* результат */
     main(){    int i;
       for(i=2; i < 10; i++){
           printf( "пробуем i=%d\n", i);
           if( test1(i) == FOUND ){
               printf("ответ %d\n", value); break;
           }
       }
     }
     test1(i){  int j;
       for(j=1; j < 10 ; j++ ){
           printf( "пробуем j=%d\n", j);
           if( test2(i,j) == FOUND ) return FOUND;
           /* "сквозной" return */
       }
       return NOTFOUND;
     }
     test2(i, j){
       printf( "пробуем(%d,%d)\n", i, j);
       if( i * j == 21 ){
           printf( "  Годятся (%d,%d)\n", i,j);
           value = j; return FOUND;
       }
       return NOTFOUND;
     }
 

Вот ответ, использующий нелокальный переход вместо цепочки return-ов:

     #include <setjmp.h>
     jmp_buf jmp;
     main(){   int i;
       if( i = setjmp(jmp))  /* после прыжка */
             printf("Ответ %d\n", --i);
       else  /* установка точки */
         for(i=2; i < 10; i++)
           printf( "пробуем i=%d\n", i), test1(i);
     }
     test1(i){ int j;
       for(j=1; j < 10 ; j++ )
           printf( "пробуем j=%d\n", j), test2(i,j);
     }
     test2(i, j){
       printf( "пробуем(%d,%d)\n", i, j);
       if( i * j == 21 ){
          printf( "  Годятся (%d,%d)\n", i,j);
          longjmp(jmp, j + 1);
       }
     }
 

Обратите внимание, что при возврате ответа через второй аргумент longjmp мы прибавили 1, а при печати ответа мы эту единицу отняли. Это сделано на случай ответа j==0, чтобы функция setjmp не вернула бы в этом случае значение 0 (признак установки контрольной точки).

6.7.2. В чем ошибка?

     #include <setjmp.h>
 
     jmp_buf jmp;
     main(){
          g();
          longjmp(jmp,1);
     }
     g(){ printf("Вызвана g\n");
          f();
          printf("Выхожу из g\n");
     }
     f(){
          static n;
          printf( "Вызвана f\n");
          setjmp(jmp);
          printf( "Выхожу из f %d-ый раз\n", ++n);
     }
 

Ответ: longjmp делает прыжок в функцию f(), из которой уже произошел возврат управления. При переходе в тело функции в обход ее заголовка не выполняются машинные команды "пролога" функции - функция остается "неактивированной". При возврате из вызванной таким "нелегальным" путем функции возникает ошибка, и программа падает. Мораль: в функцию, которая НИКЕМ НЕ ВЫЗВАНА, нельзя передавать управление. Обратный прыжок из f() в main() - был бы законен, поскольку функция main() является активной, когда управление находится в теле функции f(). Т.е. можно "прыгать" из вызванной функции в вызывающую: из f() в main() или в g(); и из g() в main();

     --        -|   f    |  стек      прыгать
      |   g    |  вызовов   сверху вниз
      |   main |  функций   можно - это соответствует
      ----------            выкидыванию нескольких
                            верхних слоев стека
 

но нельзя наоборот: из main() в g() или f(); а также из g() в f(). Можно также совершать прыжок в пределах одной и той же функции:

     f(){ ...
             A:   setjmp(jmp);
                  ...
                  longjmp(jmp, ...); ...
                  /* это как бы goto A; */
     }
 

6.8. Хозяин файла, процесса, и проверка привелегий.

UNIX - многопользовательская система. Это значит, что одновременно на разных терминалах, подключенных к машине, могут работать разные пользователи (а может и один на нескольких терминалах). На каждом терминале работает свой интерпретатор команд, являющийся потомком процесса /etc/init.

6.8.1. Теперь - про функции, позволяющие узнать некоторые данные про любого пользователя системы. Каждый пользователь в UNIX имеет уникальный номер: идентификатор пользователя (user id), а также уникальное имя: регистрационное имя, которое он набирает для входа в систему. Вся информация о пользователях хранится в файле /etc/passwd. Существуют функции, позволяющие по номеру пользователя узнать регистрационное имя и наоборот, а заодно получить еще некоторую информацию из passwd:

     #include <stdio.h>
     #include <pwd.h>
     struct passwd *p;
     int   uid;   /* номер */
     char *uname; /* рег. имя */
 
     uid = getuid();
     p   = getpwuid( uid   );
             ...
     p   = getpwnam( uname );
 

Эти функции возвращают указатели на статические структуры, скрытые внутри этих функций. Структуры эти имеют поля:

     p->pw_uid     идентиф. пользователя (int uid);
     p->pw_gid     идентиф. группы пользователя;
 
             и ряд полей типа char[]
     p->pw_name    регистрационное имя пользователя (uname);
     p->pw_dir     полное имя домашнего каталога
       (каталога, становящегося текущим при входе в систему);
     p->pw_shell   интерпретатор команд
       (если "", то имеется в виду /bin/sh);
     p->pw_comment произвольная учетная информация (не используется);
     p->pw_gecos   произвольная учетная информация (обычно ФИО);
     p->pw_passwd  зашифрованный пароль для входа в
        систему. Истинный пароль нигде не хранится вовсе!
 

Функции возвращают значение p==NULL, если указанный пользователь не существует (например, если задан неверный uid). uid хозяина данного процесса можно узнать вызовом getuid, а uid владельца файла - из поля st_uid структуры, заполняемой системным вызовом stat (а идентификатор группы владельца - из поля st_gid). Задание: модифицируйте наш аналог программы ls, чтобы он выдавал в текстовом виде имя владельца каждого файла в каталоге.

6.8.2. Владелец файла может изменить своему файлу идентификаторы владельца и группы вызовом

     chown(char *имяФайла, int uid, int gid);
 

т.е. "подарить" файл другому пользователю. Забрать чужой файл себе невозможно. При этой операции биты S_ISUID и S_ISGID в кодах доступа к файлу (см. ниже) сбрасываются, поэтому создать "Троянского коня" и, сделав его хозяином суперпользователя, получить неограниченные привелегии - не удастся!

6.8.3. Каждый файл имеет своего владельца (поле di_uid в I-узле на диске или поле i_uid в копии I-узла в памяти ядра А. Богатырев, 1992-95 Си в UNIX*). Каждый процесс также имеет своего владельца (поля u_uid и u_ruid в u-area). Как мы видим, процесс имеет два параметра, обозначающие владельца. Поле ruid называется "реальным идентификатором" пользователя, а uid "эффективным идентификатором". При вызове exec() заменяется программа, выполняемая данным процессом:

      старая программа  exec    новая программа
          ruid -->----------------->---> ruid
          uid  -->--------*-------->---> uid (new)
                          |
                     выполняемый файл
                      i_uid (st_uid)
 

Как видно из этой схемы, реальный идентификатор хозяина процесса наследуется. Эффективный идентификатор обычно также наследуется, за исключением одного случая: если в кодах доступа файла (i_mode) выставлен бит S_ISUID (set-uid bit), то значение поля u_uid в новом процессе станет равно значению i_uid файла с программой:

     /* ... во время exec ... */
     p_suid = u_uid;     /* спасти */
     if( i_mode & S_ISUID ) u_uid = i_uid;
     if( i_mode & S_ISGID ) u_gid = i_gid;
 

т.е. эффективным владельцем процесса станет владелец файла. Здесь gid - это идентификаторы группы владельца (которые тоже есть и у файла и у процесса, причем у процесса - реальный и эффективный).

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

     mode = i_mode & 0777;
                   /* rwx rwx rwx */
 

и владельца i_uid, то процесс, пытающийся открыть этот файл, будет "проэкзаменован" в таком порядке:

     if( u_uid == 0 )  /* super user */
          то доступ разрешен;
     else if( u_uid == i_uid )
          проверить коды (mode & 0700);
     else if( u_gid == i_gid )
          проверить коды (mode & 0070);
     else проверить коды (mode & 0007);
 

Процесс может узнать свои параметры:

     unsigned short uid  = geteuid();  /* u_uid  */
     unsigned short ruid = getuid();   /* u_ruid */
     unsigned short gid  = getegid();  /* u_gid  */
     unsigned short rgid = getuid();   /* u_rgid */
 

а также установить их:

     setuid(newuid);  setgid(newgid);
 

Рассмотрим вызов setuid. Он работает так (u_uid - относится к процессу, издавшему этот вызов):

     if(      u_uid == 0 /* superuser */ )
              u_uid = u_ruid =    p_suid =  newuid;
     else if( u_ruid == newuid || p_suid == newuid )
              u_uid = newuid;
     else     неудача;
 

Поле p_suid позволяет set-uid-ной программе восстановить эффективного владельца, который был у нее до exec-а.

Во-вторых, все это надо для следующего случая: пусть у меня есть некоторый файл BASE с хранящимися в нем секретными сведениями. Я являюсь владельцем этого файла и устанавливаю ему коды доступа 0600 (чтение и запись разрешены только мне). Тем не менее, я хочу дать другим пользователям возможность работать с этим файлом, однако контролируя их деятельность. Для этого я пишу программу, которая выполняет некоторые действия с файлом BASE, при этом проверяя законность этих действий, т.е. позволяя делать не все что попало, а лишь то, что я в ней предусмотрел, и под жестким контролем. Владельцем файла PROG, в котором хранится эта программа, также являюсь я, и я задаю этому файлу коды доступа 0711 (rwx--x--x) - всем можно выполнять эту программу. Все ли я сделал, чтобы позволить другим пользоваться базой BASE через программу (и только нее) PROG? Нет!

Если кто-то другой запустит программу PROG, то эффективный идентификатор процесса будет равен идентификатору этого другого пользователя, и программа не сможет открыть мой файл BASE. Чтобы все работало, процесс, выполняющий программу PROG, должен работать как бы от моего имени. Для этого я должен вызовом chmod либо командой

     chmod u+s PROG

добавить к кодам доступа файла PROG бит S_ISUID.

После этого, при запуске программы PROG, она будет получать эффективный идентификатор, равный моему идентификатору, и таким образом сможет открыть и работать с файлом BASE. Вызов getuid позволяет выяснить, кто вызвал мою программу (и занести это в протокол, если надо).

Программы такого типа - не редкость в UNIX, если владельцем программы (файла ее содержащего) является суперпользователь. В таком случае программа, имеющая бит доступа S_ISUID работает от имени суперпользователя и может выполнять некоторые действия, запрещенные обычным пользователям. При этом программа внутри себя делает всяческие проверки и периодически спрашивает пароли, то есть при работе защищает систему от дураков и преднамеренных вредителей. Простейшим примером служит команда ps, которая считывает таблицу процессов из памяти ядра и распечатывает ее. Доступ к физической памяти машины производится через файл-псевдоустройство /dev/mem, а к памяти ядра /dev/kmem. Чтение и запись в них позволены только суперпользователю, поэтому программы "общего пользования", обращающиеся к этим файлам, должны иметь бит set-uid.

Откуда же изначально берутся значения uid и ruid (а также gid и rgid) у процесса? Они берутся из процесса регистрации пользователя в системе: /etc/getty. Этот процесс запускается на каждом терминале как процесс, принадлежащий суперпользователю (u_uid==0). Сначала он запрашивает имя и пароль пользователя:

     #include <stdio.h>  /* cc -lc_s */
     #include <pwd.h>
     #include <signal.h>
     struct passwd *p;
     char userName[80], *pass, *crpass;
     extern char *getpass(), *crypt();
       ...
     /* Не прерываться по сигналам с клавиатуры */
     signal (SIGINT, SIG_IGN);
     for(;;){
       /* Запросить имя пользователя: */
       printf("Login: "); gets(userName);
       /* Запросить пароль (без эха): */
       pass = getpass("Password: ");
       /* Проверить имя: */
       if(p = getpwnam(userName)){
          /* есть такой пользователь */
          crpass = (p->pw_passwd[0]) ? /* если есть пароль */
                   crypt(pass, p->pw_passwd) : pass;
          if( !strcmp( crpass, p->pw_passwd))
                   break; /* верный пароль */
       }
       printf("Login incorrect.\a\n");
     }
     signal (SIGINT, SIG_DFL);
 

Затем он выполняет:

     // ... запись информации о входе пользователя в систему
     // в файлы /etc/utmp (кто работает в системе сейчас)
     // и       /etc/wtmp (список всех входов в систему)
             ...
     setuid( p->pw_uid ); setgid( p->pw_gid );
     chdir ( p->pw_dir ); /* GO HOME! */
     // эти параметры будут унаследованы
     // интерпретатором команд.
             ...
     // настройка некоторых переменных окружения envp:
     // HOME     = p->pw_dir
     // SHELL    = p->pw_shell
     // PATH     = нечто по умолчанию, вроде :/bin:/usr/bin
     // LOGNAME (USER) = p->pw_name
     // TERM     = считывается из файла
     //            /etc/ttytype по имени устройства av[1]
     // Делается это как-то подобно
     //   char *envp[MAXENV], buffer[512]; int envc = 0;
     //   ...
     //   sprintf(buffer, "HOME=%s", p->pw_dir);
     //   envp[envc++] = strdup(buffer);
     //   ...
     //   envp[envc] = NULL;
             ...
     // настройка кодов доступа к терминалу. Имя устройства
     // содержится в параметре av[1] функции main.
     chown (av[1], p->pw_uid, p->pw_gid);
     chmod (av[1], 0600 );  /* -rw------- */
     // теперь доступ к данному терминалу имеют только
     // вошедший в систему пользователь и суперпользователь.
     // В случае смерти интерпретатора команд,
     // которым заменится getty, процесс init сойдет
     // с системного вызова ожидания wait() и выполнит
     //  chown ( этот_терминал, 2 /*bin*/, 15 /*terminal*/ );
     //  chmod ( этот_терминал, 0600 );
     // и, если терминал числится в файле описания линий
     // связи /etc/inittab как активный (метка respawn), то
     // init перезапустит на этом_терминале новый
     // процесс getty при помощи пары вызовов fork() и exec().
             ...
     // запуск интерпретатора команд:
     execle( *p->pw_shell ? p->pw_shell : "/bin/sh",
                       "-", NULL, envp );
 

В результате он становится процессом пользователя, вошедшего в систему. Таковым же после exec-а, выполняемого getty, остается и интерпретатор команд p->pw_shell (обычно /bin/sh или /bin/csh) и все его потомки.

На самом деле, в описании регистрации пользователя при входе в систему, сознательно было допущено упрощение. Дело в том, что все то, что мы приписали процессу getty, в действительности выполняется двумя программами: /etc/getty и /bin/login.

Сначала процесс getty занимается настройкой параметров линии связи (т.е. терминала) в соответствии с ее описанием в файле /etc/gettydefs. Затем он запрашивает имя пользователя и заменяет себя (при помощи сисвызова exec) процессом login, передавая ему в качестве одного из аргументов полученное имя пользователя.

Затем login запрашивает пароль, настраивает окружение, и.т.п., то есть именно он производит все операции, приведенные выше на схеме. В конце концов он заменяет себя интерпретатором команд.

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

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

     char *getlogin();

Эта функция не проверяет uid процесса, а просто извлекает запись про данный терминал из файла /etc/utmp.

Наконец отметим, что владелец файла устанавливается при создании этого файла (вызовами creat или mknod), и полагается равным эффективному идентификатору создающего процесса.

     di_uid = u_uid;     di_gid = u_gid;
 

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

* - В некотором буфере запоминается текущее состояние процесса: положение вершины стека вызовов функций (stack pointer); состояние всех регистров процессора, включая регистр адреса текущей машинной команды (instruction pointer).

* - Это достигается восстановлением состояния процесса из буфера. Изменения, происшедшие за время между setjmp и longjmp в статических данных не отменяются (т.к. они не сохранялись).

* - При открытии файла и вообще при любой операции с файлом, в таблицах ядра заводится копия I-узла (для ускорения доступа, чтобы постоянно не обращаться к диску). Если I-узел в памяти будет изменен, то при закрытии файла (а также периодически через некоторые промежутки времени) эта копия будет записана обратно на диск. Структура I-узла в памяти - struct inode - описана в файле <sys/inode.h>, а на диске - struct dinode - в файле <sys/ino.h>.

6.9. Блокировка доступа к файлам.

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

  • Допустим, что процесс A изменяет некоторую область файла, в то время как процесс B пытается прочесть ту же область. Итогом такого соревнования может быть то, что процесс B прочтет неверные данные.
  • Допустим, что процесс A изменяет некоторую область файла, в то время как процесс C также изменяет ту же самую область. В итоге эта область может содержать неверные данные (часть - от процесса A, часть - от C).

Ясно, что требуется механизм синхронизации процессов, позволяющий не пускать другой процесс (процессы) читать и/или записывать данные в указанной области. Механизмов синхронизации в UNIX существует множество: от семафоров до блокировок областей файла. О последних мы и будем тут говорить.

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

6.9.1. Блокировка устанавливается при помощи вызова

     flock_t lock;
 
     fcntl(fd, operation, &lock);
 

Здесь operation может быть одним из трех:

F_SETLK

Устанавливает или снимает замок, описываемый структурой lock. Структура flock_t имеет такие поля:

          short  l_type;
          short  l_whence;
          off_t  l_start;
          size_t l_len;
 
          long   l_sysid;
          pid_t  l_pid;
 

l_type

тип блокировки:

          F_RDLCK - на чтение;
          F_WRLCK - на запись;
          F_UNLCK - снять все замки.
 

l_whence, l_start, l_len

описывают сегмент файла, на который ставится замок: от точки lseek(fd,l_start,l_whence); длиной l_len байт. Здесь l_whence может быть: SEEK_SET, SEEK_CUR, SEEK_END. l_len равное нулю означает "до конца файла". Так если все три параметра равны 0, то будет заблокирован весь файл.

F_SETLKW

Устанавливает или снимает замок, описываемый структурой lock. При этом, если замок на область, пересекающуюся с указанной уже кем-то установлен, то сперва дождаться снятия этого замка.

          Пытаемся   | Нет        Уже есть                   уже есть
          поставить  | чужих      замок                      замок
          замок на   | замков     на READ                    на WRITE
          -----------|--------------------------------------------------------------    READ
 		            | читать     читать                     ждать;запереть;читать
          WRITE      | записать   ждать;запереть;записать    ждать;запереть;записать
          UNLOCK     | отпереть   отпереть                   отпереть
 
  • Если кто-то читает сегмент файла, то другие тоже могут его читать свободно, ибо чтение не изменяет файла.
  • Если же кто-то записывает файл - то все остальные должны дождаться окончания записи и разблокировки.
  • Если кто-то читает сегмент, а другой процесс собрался изменить (записать) этот сегмент, то этот другой процесс обязан дождаться окончания чтения первым.
  • В момент, обозначенный как отпереть - будятся процессы, ждущие разблокировки, и ровно один из них получает доступ (может установить свою блокировку). Порядок кто из них будет первым - вообще говоря не определен.

F_GETLK

Запрашиваем возможность установить замок, описанный в lock.

  • Если мы можем установить такой замок (не заперто никем), то в структуре lock поле l_type становится равным F_UNLCK и поле l_whence равным SEEK_SET.
  • Если замок уже кем-то установлен (и вызов F_SETLKW заблокировал бы наш процесс, привел бы к ожиданию), мы получаем информацию о чужом замке в структуру lock. При этом в поле l_pid заносится идентификатор процесса, создавшего этот замок, а в поле l_sysid - идентификатор машины (поскольку блокировка файлов поддерживается через сетевые файловые системы). Замки автоматически снимаются при закрытии дескриптора файла. Замки не наследуются порожденным процессом при вызове fork.
     #include <stdio.h>
     #include <sys/types.h>
     #include <fcntl.h>
     #include <unistd.h>
     #include <time.h>
     #include <signal.h>
 
     char DataFile [] = "data.xxx";
     char info     [] = "abcdefghijklmnopqrstuvwxyz";
     #define OFFSET 5
     #define SIZE   12
 
     #define PAUSE 2
 
     int trial = 1;
     int fd, pid;
     char buffer[120], myname[20];
     void writeAccess(), readAccess();
     void fcleanup(int nsig){
             unlink(DataFile);
             printf("cleanup:%s\n", myname);
             if(nsig) exit(0);
     }
 
     int main(){
             int i;
 
             fd = creat(DataFile, 0644);
             write(fd, info, strlen(info));
             close(fd);
 
             signal(SIGINT, fcleanup);
 
             sprintf(myname, fork() ? "B-%06d" : "A-%06d", pid = getpid());
 
             srand(time(NULL)+pid);
             printf("%s:started\n", myname);
 
             fd = open(DataFile, O_RDWR|O_EXCL);
             printf("%s:opened %s\n", myname, DataFile);
 
             for(i=0; i < 30; i++){
                     if(rand()%2)    readAccess();
                     else            writeAccess();
             }
 
             close(fd);
 
             printf("%s:finished\n", myname);
 
             wait(NULL);
             fcleanup(0);
             return 0;
     }
     void writeAccess(){
             flock_t lock;
 
             printf("Write:%s #%d\n", myname, trial);
 
             lock.l_type   = F_WRLCK;
             lock.l_whence = SEEK_SET;
             lock.l_start  = (off_t)  OFFSET;
             lock.l_len    = (size_t) SIZE;
 
             if(fcntl(fd, F_SETLKW, &lock) <0)
                     perror("F_SETLKW");
             printf("\twrite:%s locked\n", myname);
 
             sprintf(buffer, "%s #%02d", myname, trial);
             printf ("\twrite:%s \"%s\"\n", myname, buffer);
 
             lseek (fd, (off_t) OFFSET, SEEK_SET);
             write (fd, buffer, SIZE);
 
             sleep (PAUSE);
 
             lock.l_type   = F_UNLCK;
             if(fcntl(fd, F_SETLKW, &lock) <0)
                     perror("F_SETLKW");
 
             printf("\twrite:%s unlocked\n", myname);
 
             trial++;
     }
 
     void readAccess(){
             flock_t lock;
 
             printf("Read:%s #%d\n", myname, trial);
 
             lock.l_type   = F_RDLCK;
             lock.l_whence = SEEK_SET;
             lock.l_start  = (off_t)  OFFSET;
             lock.l_len    = (size_t) SIZE;
 
             if(fcntl(fd, F_SETLKW, &lock) <0)
                     perror("F_SETLKW");
             printf("\tread:%s locked\n", myname);
 
             lseek(fd, (off_t) OFFSET, SEEK_SET);
             read (fd, buffer, SIZE);
 
             printf("\tcontents:%s \"%*.*s\"\n", myname, SIZE, SIZE, buffer);
             sleep (PAUSE);
 
             lock.l_type   = F_UNLCK;
             if(fcntl(fd, F_SETLKW, &lock) <0)
                     perror("F_SETLKW");
 
             printf("\tread:%s unlocked\n", myname);
 
             trial++;
     }
 

Исследуя выдачу этой программы, вы можете обнаружить, что READ-области могут перекрываться; но что никогда не перекрываются области READ и WRITE ни в какой комбинации. Если идет чтение процессом A - то запись процессом B дождется разблокировки A (чтение - не будет дожидаться). Если идет запись процессом A - то и чтение процессом B и запись процессом B дождутся разблокировки A.

6.9.2. UNIX SVR4 имеет еще один интерфейс для блокировки файлов: функцию lockf.

     #include <unistd.h>
 
     int lockf(int fd, int operation, size_t size);
 

Операция operation:

F_ULOCK
Разблокировать указанный сегмент файла (это может снимать один или несколько замков).
F_LOCK
F_TLOCK
Установить замок. При этом, если уже имеется чужой замок на запрашиваемую область, F_LOCK блокирует процесс, F_TLOCK - просто выдает ошибку (функция возвращает -1, errno устанавливается в EAGAIN).
  • Ожидание отпирания/запирания замка может быть прервано сигналом.
  • Замок устанавливается следующим образом: от текущей позиции указателя чтениязаписи в файле fd (что не похоже на fcntl, где позиция задается явно как параметр в структуре); длиной size. Отрицательное значение size означает отсчет от текущей позиции к началу файла. Нулевое значение - означает "от текущей позиции до конца файла". При этом "конец файла" понимается именно как конец, а не как текущий размер файла. Если файл изменит размер, запертая область все равно будет простираться до конца файла (уже нового).
  • Замки, установленные процессом, автоматически отпираются при завершении процесса.
F_TEST
Проверить наличие замка. Функция возвращает 0, если замка нет; -1 в противном случае (заперто).

Если устанавливается замок, перекрывающийся с уже установленным, то замки объединяются.

     было:     ___________#######____######__________
 
     запрошено:______________##########______________
 
     стало:    ___________#################__________
 

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

     было:     ___________#################__________
 
     запрошено:______________XXXXXXXXXX______________
 
     стало:    ___________###__________####__________
 

6.10. Файлы устройств.

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

  • блок для загрузчика - программы, вызываемой аппаратно при включении машины (загрузчик записывает с диска в память машины программу /boot, которая в свою очередь загружает в память ядро /unix);
  • суперблок - блок заголовка файловой системы, хранящий размер файловой системы (в блоках), размер блока (512, 1024, ...), количество I-узлов, начало списка свободных блоков, и другие сведения об FS;
  • некоторая непрерывная область диска для хранения I-узлов - "I-файл".

Файловые системы объединяются в единую древовидную иерархию операцией монтирования подключения корня файловой системы к какому-то из каталогов-"листьев" дерева другой FS.

Файлы в объединенной иерархии адресуются при помощи двух способов:

  • имен, задающих путь в дереве каталогов:
               /usr/abs/bin/hackIt
               bin/hackIt
               ./../../bin/vi
     

    (этот способ предназначен для программ, пользующихся файлами, а также пользователей);

  • внутренних адресов, используемых программами ядра и некоторыми системными программами.

Поскольку в каждой FS имеется собственная нумерация I-узлов, то файл в объединенной иерархии должен адресоваться ДВУМЯ параметрами:

  • номером (кодом) устройства, содержащего файловую систему, в которой находится искомый файл: dev_t i_dev;
  • номером I-узла файла в этой файловой системе: ino_t i_number;

Преобразование имени файла в объединенной файловой иерархии в такую адресную пару выполняет в ядре уже упоминавшаяся выше функция namei (при помощи просмотра каталогов):

     struct inode *ip = namei(...);
 

Создаваемая ею копия I-узла в памяти ядра содержит поля i_dev и i_number (которые на самом диске не хранятся!).

Рассмотрим некоторые алгоритмы работы ядра с файлами. Ниже они приведены чисто схематично и в сильном упрощении. Форматы вызова (и оформление) функций не соответствуют форматам, используемым на самом деле в ядре; верны лишь названия функций. Опущены проверки на корректность, подсчет ссылок на структуры file и inode, блокировка I-узлов и кэш-буферов от одновременного доступа, и многое другое.

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

     #include <sys/types.h>
     #include <sys/inode.h>
     #include <sys/file.h>
     int fd_read = open(имяФайла, O_RDONLY){
 
       int fd; struct inode *ip; struct file *fp; dev_t dev;
 
       u_error = 0;    /* errno в программе */
     // Найти файл по имени. Создается копия I-узла в памяти:
       ip = namei(имяФайла, LOOKUP);
     // namei может выдать ошибку, если нет такого файла
       if(u_error) return(-1);  // ошибка
 
     // Выделяется структура "открытый файл":
       fp = falloc(ip, FREAD);
       // fp->f_flag = FREAD; открыт на чтение
       // fp->f_offset = 0;   RWptr
       // fp->f_inode  = ip;  ссылка на I-узел
 
     // Выделить новый дескриптор
       for(fd=0; fd < NOFILE; fd++)
          if(u_ofile[fd] == NULL ) // свободен
              goto done;
       u_error = EMFILE; return (-1);
     done:
       u_ofile[fd] = fp;
 
     // Если это устройство - инициализировать его.
     // Это функция openi(ip, fp->f_flag);
       dev = ip->i_rdev;
       if((ip->i_mode & IFMT) == IFCHR)
         (*cdevsw[major(dev)].d_open)(minor(dev),fp->f_flag);
       else if((ip->i_mode & IFMT) == IFBLK)
         (*bdevsw[major(dev)].d_open)(minor(dev),fp->f_flag);
       return fd;  // через u_rval1
     }
 
     close(fd){
       struct file  *fp = u_ofile[fd];
       struct inode *ip = fp->f_inode;
       dev_t dev = ip->i_rdev;
 
       if((ip->i_mode & IFMT) == IFCHR)
         (*cdevsw[major(dev)].d_close)(minor(dev),fp->f_flag);
       else if((ip->i_mode & IFMT) == IFBLK)
         (*bdevsw[major(dev)].d_close)(minor(dev),fp->f_flag);
 
       u_ofile[fd] = NULL;
       // и удалить ненужные структуры из ядра.
     }
 

Теперь рассмотрим функцию преобразования логических блоков файла в номера физических блоков в файловой системе. Для этого преобразования в I-узле файла содержится таблица адресов блоков. Она устроена довольно сложно - ее начало находится в узле, а продолжение - в нескольких блоках в самой файловой системе (устройство это можно увидеть в примере "Фрагментированность файловой системы" в приложении). Мы для простоты будем предполагать, что это просто линейный массив i_addr[], в котором n-ому логическому блоку файла отвечает bno-тый физический блок файловой системы:

     bno = ip->i_addr[n];
 

Если файл является интерфейсом устройства, то этот файл не хранит информации в логической файловой системе. Поэтому у устройств нет таблицы адресов блоков. Вместо этого, поле i_addr[0] используется для хранения кода устройства, к которому приводит этот специальный файл. Это поле носит название i_rdev, т.е. как бы сделано

     #define i_rdev i_addr[0]
 

(на самом деле используется union). Устройства бывают байто-ориентированные, обмен с которыми производится по одному байту (как с терминалом или с коммуникационным портом); и блочно-ориентированные, обмен с которыми возможен только большими порциями блоками (пример - диск). То, что файл является устройством, помечено в поле тип файла

     ip->i_mode & IFMT
 

одним из значений: IFCHR - байтовое; или IFBLK - блочное. Алгоритм вычисления номера блока:

     ushort u_pboff;  // смещение от начала блока
     ushort u_pbsize; // сколько байт надо использовать
     // ushort  - это unsigned short, смотри <sys/types.h>
     // daddr_t - это long (disk address)
 
     daddr_t bmap(struct inode *ip,
                  off_t offset, unsigned count){
       int sz, rem;
 
       // вычислить логический номер блока по позиции RWptr.
       // BSIZE - это размер блока файловой системы,
       // эта константа определена в <sys/param.h>
       daddr_t bno = offset / BSIZE;
       // если BSIZE == 1 Кб, то можно offset >> 10
 
           u_pboff = offset % BSIZE;
           // это можно записать как offset & 01777
 
           sz = BSIZE - u_pboff;
           // столько байт надо взять из этого блока,
           // начиная с позиции u_pboff.
 
           if(count < sz) sz = count;
           u_pbsize = sz;
 

Если файл представляет собой устройство, то трансляция логических блоков в физические не производится - устройство представляет собой "сырой" диск без файлов и каталогов, т.е. обращение происходит сразу по физическому номеру блока:

           if((ip->i_mode & IFMT) == IFBLK) // block device
              return bno;       // raw disk
           // иначе провести пересчет:
 
           rem = ip->i_size /*длина файла*/ - offset;
           // это остаток файла.
           if( rem < 0 )  rem = 0;
           // файл короче, чем заказано нами:
           if( rem < sz ) sz = rem;
           if((u_pbsize = sz) == 0) return (-1); // EOF
 
           // и, собственно, замена логич. номера на физич.
           return ip->i_addr[bno];
     }
 

Теперь рассмотрим алгоритм read. Параметры, начинающиеся с u_..., на самом деле передаются как статические через вспомогательные переменные в u-area процесса.

     read(int fd, char *u_base, unsigned u_count){
         unsigned srccount = u_count;
         struct   file  *fp = u_ofile[fd];
         struct   inode *ip = fp->f_inode;
         struct   buf   *bp;
         daddr_t         bno; // очередной блок файла
 
         // dev - устройство,
         // интерфейсом которого является файл-устройство,
         // или на котором расположен обычный файл.
         dev_t dev = (ip->i_mode & (IFCHR|IFBLK)) ?
               ip->i_rdev : ip->i_dev;
 
         switch( ip->i_mode & IFMT ){
 
         case IFCHR:  // байто-ориентированное устройство
           (*cdevsw[major(dev)].d_read)(minor(dev));
           // прочие параметры передаются через u-area
           break;
 
         case IFREG:  // обычный файл
         case IFDIR:  // каталог
         case IFBLK:  // блочно-ориентированное устройство
           do{
              bno = bmap(ip, fp->f_offset /*RWptr*/, u_count);
              if(u_pbsize==0 || (long)bno < 0) break; // EOF
              bp  = bread(dev, bno);  // block read
 
              iomove(bp->b_addr + u_pboff, u_pbsize, B_READ);
 

Функция iomove копирует данные

     bp->b_addr[ u_pboff..u_pboff+u_pbsize-1 ]
 

из адресного пространства ядра (из буфера в ядре) в адресное пространство процесса по адресам

     u_base[ 0..u_pbsize-1 ]
 

то есть пересылает u_pbsize байт между ядром и процессом (u_base попадает в iomove через статическую переменную). При записи вызовом write(), iomove с флагом B_WRITE производит обратное копирование - из памяти процесса в память ядра. Продолжим:

              // продвинуть счетчики и указатели:
              u_count      -= u_pbsize;
              u_base       += u_pbsize;
              fp->f_offset += u_pbsize;  // RWptr
           } while( u_count != 0 );
           break;
         ...
         return( srccount - u_count );
     } // end read
 

Теперь обсудим некоторые места этого алгоритма. Сначала посмотрим, как происходит обращение к байтовому устройству. Вместо адресов блоков мы получаем код устройства i_rdev. Коды устройств в UNIX (тип dev_t) представляют собой пару двух чисел, называемых мажор и минор, хранимых в старшем и младшем байтах кода устройства:

     #define major(dev)  ((dev >> 8) & 0x7F)
     #define minor(dev)  ( dev       & 0xFF)
 

Мажор обозначает тип устройства (диск, терминал, и.т.п.) и приводит к одному из драйверов (если у нас есть 8 терминалов, то их обслуживает один и тот же драйвер); а минор обозначает номер устройства данного типа (... каждый из терминалов имеет миноры 0..7). Миноры обычно служат индексами в некоторой таблице структур внутри выбранного драйвера. Мажор же служит индексом в переключательной таблице устройств. При этом блочно-ориентированные устройства выбираются в одной таблице - bdevsw[], а байтоориентированные - в другой - cdevsw[] (см. <sys/conf.h>; имена таблиц означают block/character device switch). Каждая строка таблицы содержит адреса функций, выполняющих некоторые предопределенные операции способом, зависимым от устройства. Сами эти функции реализованы в драйверах устройств. Аргументом для этих функций обычно служит минор устройства, к которому производится обращение. Функция в драйвере использует этот минор как индекс для выбора конкретного экземпляра устройства данного типа; как индекс в массиве управляющих структур (содержащих текущее состояние, режимы работы, адреса функций прерываний, адреса очередей данных и.т.п. каждого конкретного устройства) для данного типа устройств. Эти управляющие структуры различны для разных типов устройств (и их драйверов).

Каждая строка переключательной таблицы содержит адреса функций, выполняющих операции open, close, read, write, ioctl, select. open служит для инициализации устройства при первом его открытии (++ip->i_count==1) - например, для включения мотора; close - для выключения при последнем закрытии (--ip->i_count==0). У блочных устройств поля для read и write объединены в функцию strategy, вызываемую с параметром B_READ или B_WRITE. Вызов ioctl предназначен для управления параметрами работы устройства. Операция select - для опроса: есть ли поступившие в устройство данные (например, есть ли в clist-е ввода с клавиатуры байты? см. главу "Экранные библиотеки"). Вызов select применим только к некоторым байтоориентированным устройствам и сетевым портам (socket-ам). Если данное устройство не умеет выполнять такую операцию, то есть запрос к этой операции должен вернуть в программу ошибку (например, операция read неприменима к принтеру), то в переключательной таблице содержится специальное имя функции nodev; если же операция допустима, но является фиктивной (как write для /dev/null) - имя nulldev. Обе эти функции-заглушки представляют собой "пустышки": {}.

Теперь обратимся к блочно-ориентированным устройствам. UNIX использует внутри ядра дополнительную буферизацию при обменах с такими устройствами| -. Использованная нами выше функция bp=bread(dev,bno); производит чтение физического блока номер bno с устройства dev. Эта операция обращается к драйверу конкретного устройства и вызывает чтение блока в некоторую область памяти в ядре ОС: в один из кэш-буферов (cache, "запасать"). Заголовки кэш-буферов (struct buf) организованы в список и имеют поля (см. файл <sys/buf.h>):

b_dev
код устройства, с которого прочитан блок;
b_blkno
номер физического блока, хранящегося в буфере в данный момент;
b_flags
флаги блока (см. ниже);
b_addr
адрес участка памяти (как правило в самом ядре), в котором собственно и хранится содержимое блока.

Буферизация блоков позволяет системе экономить число обращений к диску. При обращении к bread() сначала происходит поиск блока (dev,bno) в таблице кэш-буферов. Если блок уже был ранее прочитан в кэш, то обращения к диску не происходит, поскольку копия содержимого дискового блока уже есть в памяти ядра. Если же блока еще нет в кэш-буферах, то в ядре выделяется чистый буфер, в заголовке ему прописываются нужные значения полей b_dev и b_blkno, и блок считывается в буфер с диска вызовом функции

     bp->b_flags |= B_READ;  // род работы: прочитать
     (*bdevsw[major(dev)].d_startegy)(bp);
     // bno и минор - берутся из полей *bp
 

из драйвера конкретного устройства.

Когда мы что-то изменяем в файле вызовом write(), то изменения на самом деле происходят в кэш-буферах в памяти ядра, а не сразу на диске. При записи в блок буфер помечается как измененный:

     b_flags* B_DELWRI;  // отложенная запись
 

и на диск немедленно не записывается. Измененные буфера физически записываются на диск в таких случаях:

  • Был сделан системный вызов sync();
  • Ядру не хватает кэш-буферов (их число ограничено). Тогда самый старый буфер (к которому дольше всего не было обращений) записывается на диск и после этого используется для другого блока.
  • Файловая система была отмонтирована вызовом umount;

Понятно, что не измененные блоки обратно на диск из буферов не записываются (т.к. на диске и так содержатся те же самые данные). Даже если файл уже закрыт close, его блоки могут быть еще не записаны на диск - запись произойдет лишь при вызове sync. Это означает, что измененные блоки записываются на диск "массированно" - по многу блоков, но не очень часто, что позволяет оптимизировать и саму запись на диск: сортировкой блоков можно достичь минимизации перемещения магнитных головок над диском.

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

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

     sync();  // synchronize
 

Вызов sync делается раз в 30 секунд специальным служебным процессом /etc/update, запускаемым при загрузке системы. Для работы с файлами, которые должны гарантированно быть корректными на диске, используется открытие файла

     fd = open( имя, O_RDWR | O_SYNC);
 

которое означает, что при каждом write блок из кэш-буфера немедленно записывается на диск. Это делает работу надежнее, но существенно медленнее.

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

     #include <sys/sysmacros.h>
     dev_t dev = makedev(major, minor);
                     /* (major << 8) | minor */
     mknod( имяФайла, кодыДоступа|тип, dev);
 

где dev - пара (мажор,минор) создаваемого устройства; кодыДоступа - коды доступа к файлу (0777)*; тип - это одна из констант S_IFIFO, S_IFCHR, S_IFBLK из include-файла <sys/stat.h>.

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

Можно создать файл устройства с мажором и/или минором, не отвечающим никакому реальному устройству (нет такого драйвера или минор слишком велик). Открытие таких устройств выдает код ошибки ENODEV.

Из нашей программы мы можем вызовом stat() узнать код устройства, на котором расположен файл. Он будет содержаться в поле dev_t st_dev; а если файл является специальным файлом (интерфейсом драйвера устройства), то код самого этого устройства можно узнать из поля dev_t st_rdev; Рассмотрим пример, который выясняет, относятся ли два имени к одному и тому же файлу:

     #include <sys/types.h>
     #include <sys/stat.h>
     void main(ac, av) char *av[]; {
       struct stat st1, st2; int eq;
       if(ac != 3) exit(13);
       stat(av[1], &st1); stat(av[2], &st2);
       if(eq =
         (st1.st_ino == st2.st_ino && /* номера I-узлов */
          st1.st_dev == st2.st_dev))  /* коды устройств */
     printf("%s и %s - два имени одного файла\n",av[1],av[2]);
       exit( !eq );
     }
 

Наконец, вернемся к склейке нескольких файловых систем в одну объединенную иерархию:

           ino=2
           *------      корневая файловая система
          / \    /\     на диске /dev/hd0
         /  /\    /\
              \
               *-/mnt/hd1
               :
               * ino=2    FS на диске /dev/hd1
              / \         (removable FS)
             /\  \
 

Для того, чтобы поместить корневой каталог файловой системы, находящейся на диске /dev/hd1, вместо каталога /mnt/hd1 уже "собранной" файловой системы, мы должны издать сисвызов

     mount("/dev/hd1", "/mnt/hd1", 0);
 

Для отключения смонтированной файловой системы мы должны вызвать

     umount("/dev/hd1");
 

(каталог, к которому она смонтирована, уже числится в таблице ядра, поэтому его задавать не надо). При монтировании все содержимое каталога /mnt/hd1 станет недоступным, зато при обращении к имени /mnt/hd1 мы на самом деле доберемся до (безымянного) корневого каталога на диске /dev/hd1. Такой каталог носит название mount point и может быть выявлен по тому признаку, что "." и ".." в нем лежат на разных устройствах:

     struct stat st1, st2;
     stat("/mnt/hd1/.", &st1); stat("/mnt/hd1/..", &st2);
     if( st1.st_dev != st2.st_dev) ... ; /*mount point*/
 

Для st1 поле st_dev означает код устройства /dev/hd1, а для st2 - устройства, содержащего корневую файловую систему. Операции монтирования и отмонтирования файловых систем доступны только суперпользователю.

И напоследок - сравнение структур I-узла.

             на диске        в памяти        в вызове stat
             <sys/ino.h>     <sys/inode.h>   <sys/stat.h>
             struct dinode   struct inode    struct stat
 
          // коды доступа и тип файла
     ushort  di_mode         i_mode          st_mode
          // число имен файла
     short   di_nlink        i_nlink         st_nlink
          // номер I-узла
     ushort   ---            i_number        st_ino
          // идентификатор владельца
     ushort  di_uid          i_uid           st_uid
          // идентификатор группы владельца
     ushort  di_gid          i_gid           st_gid
          // размер файла в байтах
     off_t   di_size         i_size          st_size
          // время создания
     time_t  di_ctime        i_ctime         st_ctime
          // время последнего изменения (write)
     time_t  di_mtime        i_mtime         st_mtime
          // время последнего доступа (read/write)
     time_t  di_atime        i_atime         st_atime
          // устройство, на котором расположен файл
     dev_t     ---           i_dev           st_dev
          // устройство, к которому приводит спец.файл
     dev_t     ---           i_rdev          st_rdev
          // адреса блоков
     char    di_addr[39]     i_addr[]
          // счетчик ссылок на структуру в ядре
     cnt_t                   i_count
          //                 и кое-что еще
 

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

* - Следует отличать эту системную буферизацию от буферизации при помощи библиотеки stdio. Библиотека создает буфер в самом процессе, тогда как системные вызовы имеют буфера внутри ядра.

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

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье